Claude Code 內建的 /code-review 很好用,我天天在跑。但它有一個結構性限制:寫程式的和審程式的是同一個模型。
同一個模型有同一組盲點。它誤解需求的地方,審查時會用同樣的誤解去驗收;它認為「這樣寫沒問題」的模式,審查時也不會覺得有問題。這不是模型不夠強的問題,是共同前提的問題——就像自己校對自己剛寫完的文章,錯字看十遍還是看不見。
於是我做了一支 skill:把這次的 diff 連同需求規格與專案雷區,丟給另一家的 CLI 獨立審一遍,再由主 agent 逐條驗證、篩掉誤報後回報。
跑了一段時間,它抓到過我自己漏掉的真 bug,也產生過需要反駁的誤報。這篇把設計、實作與兩類真實案例都攤開。
核心論點:價值不在「多一個 reviewer」
先講清楚這支 skill 的價值主張,因為很容易誤解成「找兩個人看總比一個人看好」。
不是的。價值在換一個模型就換一組盲點。
具體來說,另一個模型帶進來的東西有三種:
- 對規格的不同解讀。它沒有參與實作,不知道我當初怎麼想的,只能照規格字面檢查——這正是驗收該有的姿態。
- 不同的訓練分布。它對某些 pattern 的警覺度和 Claude 不一樣,會指出一些 Claude 覺得理所當然的東西。
- 一個沒有沉沒成本的視角。剛寫完程式的模型(和人)都會傾向維護自己的實作。
代價是它不懂你的專案慣例,所以誤報率高。這個代價要靠設計去壓——方法是把「專案慣例」也一起餵給它。
流程:四步,第三步是重點
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 status 和 git 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 篩選——這步不能委派
拿到輸出之後,鐵則有四條:
- 它指的每個
檔案:行,親自讀過。能舉出「輸入 → 錯誤結果」才算真問題。 - 明顯誤報(不懂專案慣例、看錯上下文)→ 標「誤報」並說明理由,不照單全收。
- 真問題按嚴重度排序,回報格式是「它指出什麼 + 我驗證的結論 + 建議修法」。
- 它說通過、抽查也沒問題 → 明講通過,不硬找東西。
鐵則:這步不委派、不照抄。另一個模型的輸出是「線索」,最終判斷屬主 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 的漏報消化成本特別低:因為它的行為是被一份文件驅動的,補一句話就等於改了審查器。 這是設計時就該追求的性質——讓工具的行為盡量由資料決定,而不是由程式碼決定。
相關文章: