跨模型交叉審查:Claude 寫的碼,換一個模型從規格角度再審一次


Claude Code 內建的 /code-review 很好用,我天天在跑。但它有一個結構性限制:寫程式的和審程式的是同一個模型

同一個模型有同一組盲點。它誤解需求的地方,審查時會用同樣的誤解去驗收;它認為「這樣寫沒問題」的模式,審查時也不會覺得有問題。這不是模型不夠強的問題,是共同前提的問題——就像自己校對自己剛寫完的文章,錯字看十遍還是看不見。

於是我做了一支 skill:把這次的 diff 連同需求規格與專案雷區,丟給另一家的 CLI 獨立審一遍,再由主 agent 逐條驗證、篩掉誤報後回報。

跑了一段時間,它抓到過我自己漏掉的真 bug,也產生過需要反駁的誤報。這篇把設計、實作與兩類真實案例都攤開。


核心論點:價值不在「多一個 reviewer」

先講清楚這支 skill 的價值主張,因為很容易誤解成「找兩個人看總比一個人看好」。

不是的。價值在換一個模型就換一組盲點

具體來說,另一個模型帶進來的東西有三種:

  1. 對規格的不同解讀。它沒有參與實作,不知道我當初怎麼想的,只能照規格字面檢查——這正是驗收該有的姿態。
  2. 不同的訓練分布。它對某些 pattern 的警覺度和 Claude 不一樣,會指出一些 Claude 覺得理所當然的東西。
  3. 一個沒有沉沒成本的視角。剛寫完程式的模型(和人)都會傾向維護自己的實作。

代價是它不懂你的專案慣例,所以誤報率高。這個代價要靠設計去壓——方法是把「專案慣例」也一起餵給它。


流程:四步,第三步是重點

Step 1  確認 scope 與對應的需求單
Step 2  跑 helper(讀規格 → 組 prompt → 呼叫另一家 CLI → 輸出導檔)
Step 3  主 agent 逐條驗證,篩掉誤報          ← 價值在這
Step 4  誤報/漏報 pattern 寫進 RUNLOG

Step 3 才是這支 skill 真正的工作。前面兩步只是在準備一份「線索清單」。

Step 1:scope 三選一

--base <branch>     # 分支 vs base 的整體差異 —— 已 commit 到分支時的預設
--uncommitted       # staged/unstaged/untracked —— 還沒 commit、想先審再 commit
--commit <SHA>      # 只審單一 commit

選錯 scope 是最常見的流程失誤。判準很簡單:看 git statusgit log——剛 commit 完就 --base,有未提交改動要先審就 --uncommitted


Step 2:prompt 怎麼組,決定審查品質

這支 helper 的核心是 build_prompt()。它組出來的 prompt 有五段,每一段都是為了壓誤報而存在。

第一段:角色與技術棧

parts.append(
    "你是 AppSystem 醫療照護系統(PHP 7.4、多租戶、Docker:nginx+php-fpm+MySQL 5.7)的資深審查者。"
    "請審查本次變更,判斷「是否正確且完整實作了下面的需求規格」,並找出程式問題。"
    "用繁體中文回覆;程式識別字/檔名維持原文。"
)

技術棧要寫死。少了「PHP 7.4」,它會建議一堆 8.x 的語法;少了「多租戶」,它看不出跨資料庫查詢的問題。

第二段:需求規格(從 issue 系統抓)

if spec:
    parts.append("\n# 需求規格(審查時對照它檢查有無漏做/做錯)\n" + spec)
else:
    parts.append("\n#(未取得規格,僅就程式碼品質與正確性審查)")

這是這支 skill 和一般 code review 工具最大的差別:它審的第一件事不是「程式有沒有 bug」,而是「有沒有照規格做」。

抓不到規格時明確降級並告知,不假裝有。

第三段:檢查優先序 + 整份雷區清單

parts.append(
    "\n# 重點檢查項(依嚴重度排序回報,每筆標出 檔案:行 + 為什麼會出錯 + 觸發情境)\n"
    "**第一優先:需求對照** —— 規格逐點是否都實作、有沒有漏掉的 case 或條件分支。\n"
    "\n接著對照下面這份「專案雷區清單」逐條檢查本次 diff:\n"
    "\n---\n" + pitfalls + "\n---\n"
    "\n請務必先驗證再回報:能舉出具體「輸入/狀態 → 錯誤結果」的才列為問題,"
    "區分「確認」與「推測」。沒有問題就明講通過。"
)

兩個設計值得說:

一、雷區清單整段塞進 prompt,來源是 repo 裡的一份文件。

PITFALLS_REL = os.path.join("doc", "claude", "pitfalls.md")

helper 執行時從 git repo 根找這個檔,整段讀進 prompt;找不到就退回內建精簡版,並在輸出印一行 # 雷區清單=(內建 fallback) 標明降級。

這帶來一個很舒服的性質:要調整「審查器會抓什麼」,改那個檔就好,skill 本身一個字都不用動。 那份文件同時也被 CLAUDE.md、專案記憶引用——它是雷區的單一真相來源,改一處全部跟上。

二、要求「先驗證再回報」,並區分確認與推測。

不加這句,你會收到一份「這裡可能有問題、那裡建議檢查」的清單,每條都要人去查,等於沒篩。要求它舉出「輸入 → 錯誤結果」之後,一大半推測性質的東西它自己就不寫了。

第四段:告訴它這台機器的 shell 怎麼用

這段是被誤報逼出來的:

parts.append(
    "\n# 搜尋指令寫法(本機 shell 是 Windows PowerShell,不照做那次搜尋等於沒跑)\n"
    "- PowerShell 雙引號內的 `$` 會被當變數展開,而本專案幾乎每個 pattern 都要搜 PHP 變數\n"
    "  → **pattern 一律用單引號包**\n"
    "- **禁止在同一個 pattern 內混用單雙引號做多層跳脫**:字串會被拆成多個位置參數,\n"
    "  rg 把碎片當成路徑,回 `os error 123`,而且**不會有任何命中**——\n"
    "  零命中與「真的沒有」表象相同,會讓你把沒跑成的搜尋當成證據。\n"
    "- 搜字面量優先 `rg --fixed-strings`;路徑一律用正斜線。\n"
)

最後那句是重點:零命中與「真的沒有」表象相同。

一個審查器如果搜尋指令寫錯、拿到零命中,它會很有信心地說「這個函式沒有其他呼叫者,可以安全修改」。指令壞掉產生的是錯誤的自信,不是錯誤訊息。


Step 3:主 agent 篩選——這步不能委派

拿到輸出之後,鐵則有四條:

  1. 它指的每個 檔案:行親自讀過。能舉出「輸入 → 錯誤結果」才算真問題。
  2. 明顯誤報(不懂專案慣例、看錯上下文)→ 標「誤報」並說明理由,不照單全收
  3. 真問題按嚴重度排序,回報格式是「它指出什麼 + 我驗證的結論 + 建議修法」。
  4. 它說通過、抽查也沒問題 → 明講通過,不硬找東西。

鐵則:這步不委派、不照抄。另一個模型的輸出是「線索」,最終判斷屬主 agent。

為什麼強調不能原文貼給使用者?因為那等於把篩選成本轉嫁出去。使用者要的是「有幾個真問題、分別是什麼」,不是一份需要自己驗證的原始輸出。


真實案例一:它抓到了我漏掉的東西

這是這支 skill 目前最有價值的一次命中。

背景:我改了自費項目的取價邏輯,commit 訊息寫「比照繳費單」。

它指出:取價的時點與繳費單不同。

我驗證後的結論——它是對的

  • 我的寫法是逐筆服務日期去查身分別資料表,取「日期 ≤ 服務日」的最新一筆來決定計價類型。
  • 而繳費單的實際邏輯是先取該月最新一筆(該月無資料才退回月底以前),再統一決定全月的計價類型。

差在哪:個案月中改計價類型時,兩邊金額會不一致。我宣稱「比照繳費單」,實際上沒有。

更糟的是我原本的驗證測不出來。我做了 744 筆對帳測試,全數相同——因為測試資料裡三家機構的計價類型欄位全部是 0,另一個分支根本走不到。

補救是造測試情境:把設定打開、把某筆個案資料改成會走另一分支的值,做改前/改後對照。舊版把某個自費項目拆成兩種單價、新版合併成一種,證實問題與修法皆成立,測完還原資料庫。

這一筆進 RUNLOG 後歸納出兩條規則:

「比照既有功能 X」的實作,不能只比對「取哪個欄位」,還要比對「取哪一筆/哪個時點」。

當對照驗證的資料讓某分支恆不觸發時(設定關閉/欄位全 0),那個分支等於沒被測到,要另外造情境——不能因為對帳全中就宣告一致。

第二條後來被寫進調查用 skill 的自檢清單。這就是跨模型審查最實在的產出:它抓到的不只是一個 bug,是一個我會重複犯的驗證盲點。


真實案例二:一則需要反駁的誤報

它指出:某個清單頁對日期欄位套了 month()day() 函式,會「停用索引」,警告大量資料時變慢。

我實測 EXPLAIN 的結果:查詢仍走複合索引,type=ref,實際掃 331 列。驅動索引是資料庫名 + 刪除旗標,日期函式只是後置過濾。那張表有 128 萬列,沒有效能問題。

它錯在哪:把「該欄無法當範圍索引使用」誤推成「整個查詢失去索引」。

這條誤報的消化方式不是去改 skill,而是在雷區文件對應的那一條補一句反面說明

⚠️ 反面:函式套在非驅動欄時只是後置過濾,不構成全表掃描,判定一律以 EXPLAIN 為準。

補完之後,因為 helper 每次執行都會重讀那份文件,下次審查自動就不會再報這條

這是我很喜歡的一個性質:修正審查器行為的成本 = 在一份文件裡加一句話。


兩個會讓審查「靜默失效」的坑

這兩個都不會報錯,只會讓你收到一份看起來正常、實際上沒做事的審查結果。

坑一:額度用盡時,它會先把完整 prompt 吐完才報錯

第一次跑就踩到:帳號額度用盡,回 You've hit your usage limit

但 helper 在報錯之前,已經把完整 prompt(規格 + 整份雷區文件 + diff,約 10k+ 字元)串流到 stdout 了

在 Claude Code 裡這代表:那 10k 字元全部進了主對話的 context,換來一則錯誤訊息。

修法是把「輸出導檔」寫成預設用法:

python codex_issue_review.py <> [scope 旗標] \
  > "<scratchpad>/review_<單號>.txt" 2>&1

跑完只讀檔尾的結論段:

Get-Content <> -Tail 120

要細節再往前翻。

這個 pattern 值得推廣到所有會產生大量輸出的外部工具:預設導檔、只讀尾段。失敗路徑的輸出量往往比成功路徑還大,而你完全無法預期什麼時候會走到失敗路徑。

坑二:它的 shell 全被擋掉,審查靜默降級成「只看 diff 猜」

這個更陰險。

某次審查它的每一次指令執行都回:

windows sandbox: CreateProcessWithLogonW failed: 1385

(1385 = ERROR_LOGON_TYPE_NOT_GRANTED

於是它讀不到相關檔案、查不到資料庫,但它沒有停下來——它把「無法驗證一致性」寫成待確認項,然後繼續憑 diff 推測,並產生了一條誤報:說某個陣列鍵未 isset 會噴 Notice 污染匯出檔——而那支檔第 14 行早就把 Notice 關掉了。如果它讀得到檔就不會這樣說。

根因:設定檔裡的 sandbox 模式會用 CreateProcessWithLogonW 切換到專用帳號跑子行程,而那些帳號缺少「以批次工作登入」權限。唯讀與可寫模式一樣掛,與登入狀態無關——認證是好的、模型呼叫正常、tokens 照算,只有 shell 起不來。

解法:那個設定只有兩個值,改成不切帳號的那個,以目前使用者身分執行,沙箱層級仍是唯讀。一行就通,helper 現在會在 Windows 上自動帶這個參數:

if os.name == 'nt':
    exec_args += ["-c", "windows.sandbox=unelevated"]

教訓寫進了 skill

⚠️ 輸出裡出現「無法驗證/唯讀環境無法啟動檔案讀取工具」時,先當成工具壞了去查,不要當成它的正常保留——沒有 shell 的審查等於零上下文猜測,誤報率大增。

這兩個坑有共同結構:外部工具的失敗不一定長得像失敗。一個是失敗但輸出很大,一個是失敗但流程繼續跑。設計整合時要主動問一句:「這個工具壞掉的時候,我看得出來嗎?」


一個實作細節:為什麼不用內建的 review 子指令

那家 CLI 有專門的 review 子指令,但它不允許 --base 與自訂 prompt 併用。而這支 skill 的全部價值都在自訂 prompt(規格 + 雷區)。

所以改用通用的非互動執行模式:吃完整 prompt(規格 + 雷區 + diff),唯讀沙箱只讀不改,prompt 由 stdin 進:

exec_args += ["-"]   # prompt 由 stdin 進

用 stdin 而不是命令列參數,是因為 prompt 有 10k+ 字元,而且含中文與各種引號——走命令列必定會在某個 shell 上炸掉。

另外附了 --dry-run,只印出將送出的 prompt 不真的呼叫。調 prompt 時很省事,也省額度。


跟內建 /code-review 怎麼分工

內建 /code-review跨模型審查
審查者同一個模型另一家的模型
對照基準程式碼本身需求規格 + 專案雷區
速度慢(數分鐘,通常要背景跑)
誤報率高,需要人篩
時機隨時實作完成/commit 後

互補,不是取代。 我的實際節奏是:寫的過程中用內建的隨手審,一張單做完、commit 之後跑一次跨模型審查當驗收。


每次必做:把誤報與漏報寫進 RUNLOG

最後一步是這支 skill 能持續變好的原因:

  • 誤報 → 累積後決定要不要在雷區文件補一句「這不是 bug」
  • 漏報(事後才發現的真問題它沒抓到)→ 那條雷區補進文件,一補進去下次自動跟上
  • 流程失誤(scope 挑錯、該背景跑卻前景 timeout)→ 改 SKILL 或 helper

一次到位就不用記。累積到門檻再一起收斂——這套機制我寫在另一篇:RUNLOG:讓 Skill 自己進化的失敗軌跡消化機制

值得注意的是,這支 skill 的漏報消化成本特別低:因為它的行為是被一份文件驅動的,補一句話就等於改了審查器。 這是設計時就該追求的性質——讓工具的行為盡量由資料決定,而不是由程式碼決定。


相關文章: