多輪 AI 審查為什麼停不下來:不塞進審查員 prompt、只裝在本地最後一關的過濾閘
很多人以為找 AI 審完程式碼、修好再審一輪,總有一輪會看到「零問題」收工;但實測下來,只要你每輪都乖乖照改,每一段新寫進去的防禦程式碼都會變成下一輪審查的新靶子。
多輪審查停不下來,壞就壞在外部審查員挑出來的極端邊角在技術上往往完全為真。在我的工作流程裡,平常跟我對話、負責派工與最後把關的是本地主 session(也就是扮演本地總機的角色),而真正動手寫碼或跑專項檢查的則是它派出去的各個 agent。如果本地驗收機制只有「對就改程式碼、錯就附反證駁回」兩種選擇,審查流程就會演變成一場越修越爛的修補連鎖。
越修越壞的二審現場
前陣子在一個後端簡訊服務的 PR 審查裡,我就撞上了這種停不下來的連鎖現場。
當時跑完初審,程式碼審查挑出了一個問題:收訊前沒有等待資料庫唯一索引建立完成。
所謂唯一索引(unique index),是資料庫用來確保特定欄位不重複的約束規則。審查意見主張,若服務節點剛啟動而唯一索引還沒建好,正好有同一個號碼連發兩筆請求,資料庫可能會漏擋重複的訂閱資料。這個說法在程式碼邏輯上確實成立,但要在現實中發生,必須同時滿足兩個極端條件:第一,服務必須是首次部署而且唯一索引剛好還在建立中;第二,同一個電話號碼必須在約 60 毫秒內連發 2 則訂閱訊息。在正式環境裡,這兩個前提同時撞在一起的機率近乎零。
然而,初審報告將這個理論競態列為應修項目(Should Fix),我看到報告裡寫得言之鑿鑿就點頭讓實作 agent 照改。
改完之後,修正內容送交第二輪雙模型交叉審查。平常我們由 Claude Code 與 Codex CLI 各自擔任不同視角的審查員,如同之前在《一個模型不夠:五軸交叉審的 code review 工作流》聊過的架構。二審報告很快退了回來,新抓出 2 個真問題,而且全數出在剛剛為了防禦 60 毫秒邊角新加的那段程式碼裡。
第一個問題是,若在首次部署等待索引建立那段極短時間窗內剛好遇到資料庫暫時性網路錯誤,會導致該服務節點停止收訊;第二個問題是,實作 agent 為了讓新加的等待邏輯通過測試,在改測試時順手把原本檢查結果數量的嚴格斷言放寬成了只檢查下限。
我看到報告當場質疑:這是不是在鑽牛角尖?這兩個新問題是不是上一輪修補新引入的?
比對版本歷史後確認,這兩個問題確實全都是剛剛為了修那個 60 毫秒邊角才寫進去的。這其實是把一個近乎零機率的部署期邊角,換成另一個同樣發生在首次部署時間窗內的低機率風險,而且只要繼續為了這個新風險加寫重試程式碼,下一輪又會長出新的修補連鎖分支。當下我決定踩煞車停止再送外部審查,把這個部署期行為風險記為已知限制(萬一真撞上,交由既有的訊息佇列積壓告警與重啟節點恢復),程式碼只補回被放寬的那 1 行測試斷言下限就結束這回合;而這場事故也促使我後來在正式標準(M6)裡把顧問流程的撤回比較原則收編進來:只要再審問題全出在為低機率邊角新加的防禦程式碼,就先比較直接撤回那段防禦與做最小更正,不准再疊加第二層防禦程式碼。
只要判定只分對錯,極端邊角就會一路綠燈
回頭檢視那個 60 毫秒的唯一索引問題,它在第一輪為什麼沒有被攔下來?
因為它在技術上是真的。程式碼邏輯確實存在那 60 毫秒的時間差,這不是模型幻覺,也不是胡亂編造。
問題出在本地驗收機制本身。在過去的設計裡,本地主 session 在接收審查意見時,實質裁決只有 2 種出口:
- 成立:既然技術上站得住腳,那就改程式碼。
- 不成立:除非拿得出客觀反證,才能附上依據駁回。
原本本地這 4 個終局關卡接收審查與顧問意見時都只看真假:程式碼審查的最終分級關卡對本地確認為真的外部意見一律排進修改清單,沒有依現實發生機率降級的條款;顧問諮詢的逐條裁決關卡規定拒絕必須附反證;外部審查意見接收關卡只看技術上對不對;規格審查的意見分流關卡只要意見為真且不改動範圍,就直接修進規格。
遇到「技術上成立、拿不出反證,但現實中根本不會發生」的邊角時,二元裁決機制沒有別的退路,只能判定為成立並動手修改。
另一個現實限制,是人類閱讀審查報告的注意力底線。我看審查報告的注意力底線只到應修(Should Fix)為止,排在 Nice to Have(參考項)的條目實質上等於不會細看。只要這個極端邊角因為「技術為真」被排進 Should Fix 以上,人類往往就順理成章點頭修改。
我在《GPT 不是停不下來,是看不見停止線:Review 隧道視野與停止條件治理》分析過審查模型自己看不見停止線的問題,但當時處理的是審查員的無效循環。這裡的問題更棘手:審查員找出的問題技術上完全站得住腳,是本地接收端缺乏判定重要性的能力,才讓二元機制直接轉成了無止境的修補連鎖。
為什麼不能把過濾規則塞進審查員 prompt
發現缺了一道重要性過濾時,最直覺的想法通常是把標準直接交給審查員。
之前散落在 Claude Code 個人記憶檔裡的 2 條舊規則(審查迴圈停止規則、委外自我審查只修嚴重錯誤),雖然寫在記憶檔裡,但在所有 workflow、skill 與 command 的實際引用次數是零。看到 FanXing 公開的攔截標準(前段模型找問題、中段模型依規則剔除雖為真但不值得修的意見、後段模型動手改)後,按照《抄 awesome 清單不難,難的是決定不抄什麼》「只抽核心分類、不照單全收外部架構」的原則,我只借鑑他判斷重要性的分類概念(不照搬他塞進外部流程的做法,也不收他為了服務每小時自動重跑修復迴圈而設計的待決策帳本條款),連同那 2 條舊記憶一起收編成一套共用的重要性過濾標準。
一開始,AI 提議把過濾規則濃縮後直接貼進外部審查員的派工 prompt 裡,叫外部模型「不要挑那些不重要的邊角」。
我當場把這個提議打了回去。把標準塞給外部審查員,存在 2 個方向的失效風險: 第一個方向是「外部無視」。外部模型會不會乖乖遵守 prompt 裡的這段提示,根本沒有機制能保證。如果外部模型無視標準照樣挑出極端邊角,而本地端因為信任外部審查員而缺乏過濾能力,本地端照樣會全盤買單。 第二個方向是「提前自我審查」。如果讓找問題的審查員在開工前就讀「什麼不用報」的寬鬆標準,模型往往會在找問題階段就過度放鬆,反而把真正嚴重的架構大洞當成邊角直接吃掉。
這就像之前在《規則寫下來了,agent 真的有收到嗎》聊過的,機制的責任邊界必須切乾淨。負責找問題的一方(不管是外部模型、本地負責審程式碼的 subagent 還是外部顧問)一律不讀這份重要性標準;這份標準既不塞進派工 prompt,也不放進審查員開工時用來挑選領域規則的標準目錄。
過濾閘唯一的合法位置,是裝在本地主 session 呈現結果給人類或動手修改前的最後一關。具體來說,就是本地的 4 個終局關卡:
- 程式碼審查的最終分級關卡(
/pr-review:自己主動派多軸 AI 審查員審完 PR 後,由本地主 session 彙整出報告的最終分級步驟) - 顧問諮詢的逐條裁決關卡(
/advisor:把單一設計或除錯難題外包給外部強模型當顧問後,本地逐條決定採不採納的步驟,做法見《Advisor:換顆腦袋,不用換整個 session》) - 外部審查意見接收關卡(衍生自 obra/superpowers 的
receiving-code-reviewskill:當收到人類同事或外部審查者對 PR 留下的修改意見、準備動手改碼前的評估步驟) - 規格審查的意見分流關卡(在 OpenSpec 規格流程下跑
/review-spec:在動工寫程式前,派對抗式審查員挑規格書漏洞後的分流步驟)
成立但不重要的六條標準與四道防線
這套裝在本地最後一關的過濾閘,有嚴格的執行順序:
本地主 session 必須先跑完原本的真假查證。只有在意見已經確定為真(含部分成立)的情況下,才進入重要性過濾;如果意見本身真假未定,絕對不准提早套用。而且判定時不看提出方標註的嚴重度,只看程式位置、觸發路徑與實際影響。
接著,我們定義了 6 類可以判定為「成立但不重要」的情況(記為 M1 到 M6):
- M1 對外無差異:問題觸發後,在系統邊界上的表現與正常行為完全一樣,外部呼叫端根本感受不到差別。但有一個例外:如果底層已經寫入錯誤資料、只是讀取端剛好有過濾遮住,這依然算重大問題,不適用 M1。
- M2 現實觸發不了:需要多個極端前提同時成立,在正式環境中觸發機率近乎零(就像前面 60 毫秒連發的案例)。例外是偵測不到且不可逆的資料損壞,就算機率低也絕不放過。
- M3 超出目前部署規模的架構假設:脫離現行部署架構去空想未來多節點並發或超大規模擴容;或是雖然指出的程式現況屬實,卻在沒有新需求依據下要求推翻現有的契約測試。
- M4 測試潔癖:當生產程式碼改錯時,既有的測試早就已經會變紅報警,審查員卻堅持要跨層重複測試、重複斷言或追求極端的資源清理。
- M5 風格與措辭偏好:純文字修飾或個人編碼寫法喜好。不過有個適用例外:M5 不套用於程式碼審查(
/pr-review)的最終分級關卡,因為那個關卡本來就有專門檢查程式碼品質與設計退化的審查軸。 - M6 修正輪的連鎖:在修正後重新送審時,如果新抓出的問題只出在上一輪為低機率邊角新加的防禦程式碼裡,就不再為它疊加新的行為程式碼。套用 M6 停止追修前,本地主 session 必須先比對修正前的版本確認問題確由上一輪修正 commit 引入,接著優先比較直接撤回那段新加的防禦程式碼與只做不新增行為分支的最小更正(如果撤回防禦後原邊角風險可接受,就撤回防禦;若保留原寫法,則在依既有權限確認接受殘餘風險後直接記為已知限制)。但如果修改過程把既有的測試斷言改弱了,這種保護退化依然必須照修不誤。
有放行條件,就得有守住的底線。我們同時立了 4 道不可降級的硬防線(G1 到 G4):
- G1 必報清單項目:屬於專案明文列為零容忍、抓到必報的硬性違規清單(例如在程式碼中硬編碼憑證或金鑰),無論機率多低一律必修。
- G2 方向性批評:質疑整體架構方向或根本做法的意見,絕不能當作無關緊要的小事隨手放掉。
- G3 交付的核心功能不會執行:經審查證實,原本應該交付的行為在執行時根本不會跑起來。把過去記憶收編進來時,我們只搬入了「功能不會動必修(G3)」與「低機率邊角不追修(M2、M6)」兩項原則;至於早期我對委外實作 agent(把整包工單外包給另一個獨立 agent 實作)設過「交件前自我審查只修最嚴重錯誤、高嚴重度以下一律記已知限制」的寬鬆門檻,依然只限委外 agent 交件前的自檢,沒有擴大成通用的審查政策。
- G4 確定性嚴重缺陷或安全漏洞:包括資料寫壞、越權、死鎖、資源洩漏、違背既有契約,以及資訊外洩、注入、路徑穿越等已確認的安全漏洞,不論呼叫端暫時看不看得出差異,一律必修。
配合這套標準,本地 4 個終局關卡各自擴充了處置格:
- 程式碼審查的最終分級關卡:一旦在前面不看原始嚴重度的評估中符合 M1–M6,在決定報告最終建議等級時就將它封頂於
Nice to Have(原本高於Nice to Have的降下來,原本更低的維持原級,只降不升)。 - 顧問諮詢關卡:在原本的接受、拒絕、待驗證之外,新增第 4 種裁決「成立但不採納」。
- 外部審查意見接收關卡:新增「值不值得改」檢查,不值得改者不實作並記為已知限制。
- 規格審查關卡:在意見分流中新增
immaterial分類,記錄理由但不改動規格。
怎麼防止本地總機偷懶蓋章
開放了「成立但不重要」這個出口之後,馬上會面臨另一個現實問題:本地主 session 會不會為了省事交差,把所有看起來麻煩的問題通通蓋上「不重要」的印章蒙混過關?
為了防止本地總機把真問題蓋章放生,我們加上了 2 道硬約束:
第一道約束是「強制附上可查證依據」。 本地主 session 判定不重要時,所有 M1–M6 判定都必須寫出至少 1 條條目編號與具體理由,其中涉及程式碼與測試現況的條目更強制附上第一手位置依據:判 M1 必須寫出系統邊界位置,並證明讀取路徑沒有遮蔽錯誤資料;判 M2 必須指明既有保護機制的位置,或是列出必須同時成立的極端前提組合;判 M4 必須指明生產程式碼改壞時哪一個既有測試會變紅(而 M6 則如前文所述必須附上與修正前版本的比對依據)。只要主 session 寫不出具體的第一手依據,就強制退回各流程原本的處理方式,不准任意降級。
第二道約束是「照列不刪與防橡皮圖章計數」。
所有判為「成立但不重要」的意見,依然會完整保留在報告、裁決表或結案未做清單中,清清楚楚標記理由供人類抽查反駁。
在規格審查關卡(/review-spec)裡,規格書常因為補寫背景說明(消除審查員讀錯題目的誤會)或拍板完架構取捨題而進入下一輪重審。系統裡原本就設有一道防範審查流於形式、只會蓋章說沒問題的橡皮圖章警報:每次開審前會自動掃描前幾輪紀錄,只要發現審查員明明提出了有實質內容的批評(「實質意見」,排除單純雜訊或讀錯題目),但本地主 session 卻連續 2 輪以上把所有意見都判成 0 條需要修改的「可行動項」,警報就會響起。接線時我們特別規定:被列為 immaterial 的項目依然要算進實質意見、但不算進可行動項;這樣萬一主 session 連續 2 輪把所有真問題都塞進 immaterial 逃避修改(0 條可行動項),就會立刻觸發橡皮圖章警報,強制停下來把分類理由攤給人類檢查。想靠蓋章假裝沒事,在機制上行不通。
四個關卡的改前改後對照
這套標準做完後,我們用 5 條固定測試意見,在 4 個終局關卡做了改前與改後的對照測試。這 5 條是模擬審查員或顧問在各關卡丟出的標準測試意見:例如第 1 條要求在整合測試或規格驗收條件裡重複加上既有單元測試已涵蓋的斷言(M4);第 2 條是前面提到的首次部署加 60 毫秒內連發 2 則訂閱的極端邊角(M2);第 3 條是在設定檔或規格範例裡寫入真實明文 API 金鑰(G1);第 4 條是質疑整體同步架構應改為事件佇列(G2);第 5 條是退訂流程無交易導致半寫資料(G4)。
對照結果非常乾脆:
在改前,第 2 條(60 毫秒邊角)在 4 個關卡全數放行採納並轉為修改動作。而第 1 條指出「整合測試裡沒有重複斷言這個欄位」,這句話在技術現況上確實為真(整合測試裡真的沒寫那行),所以改前那 3 個只查「這句話是不是事實」的關卡拿不出反證,只能照單全收列為應修或直接動手修改;只有顧問諮詢關卡在改前把「既有單元測試已經測過」擴張當成反證予以拒絕。
接上新標準後,4 個關卡就能統一用更精確的邏輯處理:承認審查員指出的現況為真,但因為既有單元測試在改錯時已經會變紅,對第 1 條依 M4、第 2 條依 M2 全數改判為 Nice to Have、成立但不採納、不實作記已知限制或不改動規格,並且精確附上了 M4 與 M2 的前提依據;而第 3、4、5 條則全數由 G1、G2、G4 防線擋下,維持原本的必修處置。
我們另外測試了 2 條專門找退路的施壓意見:第 6 條聲稱快取失效可能讀到不一致狀態,但指不出保護機制也寫不出觸發前提;第 7 條換了個溫和的說法批評資料模型根本做錯。結果 4 個關卡皆因「寫不出 M2 依據」與「命中 G2 方向性批評」而拒絕判為不重要,證明這套標準不會淪為逃避修改的漏洞。
除了測試題,這套標準很快在真實開發中發揮了作用。後續在一次跨服務 PR 中,我們收到人類同事留下的 10 條審查意見(屬於接收外部審查意見的評估場景,而非 /pr-review 內部品質軸分級)。在檢討中,我們直接依據這套標準,將其中 3 條(2 條正常資料根本碰不到的極端邊角依 M2、1 條把兩個正規表示式合成一條的純寫法偏好依 M5)判定為成立但不重要,明確記錄理由而不改動程式碼,省下了好幾次無謂的防禦性修改。
最後照慣例疊個甲,把適用邊界說清楚: 這 4 個關卡的改前改後行為比較,是在每個情境執行的單次盲測,證明規則文字確實能引導本地主 session 做出預期判定,還不是長期累積的採用率統計;甚至在新標準上線當天的收尾驗收裡,本地主 session 還是發生過 1 次沒先自己下判斷、把本來能按規則直接裁決的項目拿去逐題問人類的舊慣性。此外,目前這套過濾閘第一版只先接線了上述 4 個主 session 匯總關卡,其他不經過這四處的跨 session 派工驗收流程尚未納入。