"The first principle is that you must not fool yourself — and you are the easiest person to fool."
—— Richard P. Feynman, Cargo Cult Science (1974)
一、測試全綠,線上故障
人機協同開發中,最棘手的情況往往是所有測試全綠,系統卻在線上靜默失敗。
這背後是 AI Coding 常見的毛病:吞掉異常(Swallowing Errors)。
LLM 傾向於迎合指令、消除報錯。遇到複雜失敗、未捕獲異常或第三方網絡超時,Agent 往往用最省事的方式處理,而非排查根因。
代碼庫裏經常出現這類寫法:
- 在可能拋出異常的核心調用外包一層
try...catch,只打一行日誌就return null或return {}; - 類型不匹配時,塞入空對象作為 fallback 讓調用鏈繼續往下走;
- 校驗失敗時不拋錯中斷,而是返回「部分成功」的假狀態。
在編譯器和單元測試看來,這段代碼沒有未捕獲異常,執行順暢,覆蓋率 100%。
但在生產環境中,原本應該**立即報錯中斷(Fail-fast)**的問題被掩蓋了。下游拿到錯誤的假數據繼續運行,最終在遠離事發點的模塊引發更難排查的故障。
靜態 Lint 抓不住這種問題,因為 try...catch 語法完全合法。範例測試也防不住,因為 Agent 寫測試時早就順着這個 fallback 寫好了斷言。
二、單一 Agent 的自洽盲區
為甚麼不能讓同一個 Agent 既寫代碼又做審查?
因為模型無法在同一個上下文裏獨立審計自己的輸出。
Agent 一旦決定吞掉異常,在同一段上下文裏就會認定「已妥善處理異常」。再問它有沒有問題,它只會順着原有思路繼續辯解。
要打破這種自我合理化,必須跳出單一會話,引入另一個上下文完全隔離、專門負責挑錯的審查 Agent。
三、分層審查:確定性定位,機率性判讀
傳統 CI 由確定性腳本組成,靠 tsc 查類型,eslint 查格式,vitest 跑測試。
面對「語法合法但邏輯偷懶」的代碼,單靠模式匹配無法判斷語意,讓聊天模型自由審查整段 Diff 又難以控制誤報。分步處理:
用 AST 工具(tree-sitter 或 LSP)從 Diff 中定位改動的函數,提取高風險模式作為候選:只打日誌的 catch 塊、返回 null 或 {} 的 fallback、恆真的邊界校驗。這一層只提取候選與行號,不下判斷。
候選交給一個只輸出概率的決策模型(如 Jev 這類 System One 模型)。它只回答每個候選由守則翻譯而來的是非題:
- 「這個
catch是否吞掉異常、向下游返回假值?」 - 「輸入非法時,此處是 Fail-closed(拒絕執行)還是 Fail-open(放行)?」
- 「這個測試是否在 Mock 中繞過了核心業務邏輯?」
概率超過門檻的候選,通過 Checks API 在 PR 上留下行級標記。只警告,不阻止合併——審查模型同樣會誤判,若讓它阻斷合併,誤報就變成誤攔。
用 AST 定位,用模型判讀,由人裁決。
四、約束審查範圍,控制誤報
審查範圍過寬會帶來另一個問題:過度挑剔與誤報。警告一多,人和 Agent 都會忽略它,審查便失去作用。
要讓警告保持可信,需要四項約束:
- 職責單一:每條是非題只對應一條規則——一組專查「異常處理與 Fail-closed」,另一組專查「跨層依賴」。問題越小,誤報越少。
- 只按明文規則判讀:所有警告必須引用倉庫內的規範條目(ADR 或執行守則中的規則 ID)。規範未禁止的,無權標記。附帶作用是:規則必須寫成可判定的是非題,守則中含糊的條文會因此暴露。
- 先驗證再啟用:啟用前把整套流程在歷史 PR 與人造反例上跑一遍,確認概率能分開合理與有問題的寫法,並量出誤報率再定門檻;上線後記錄每次豁免,持續校準。同時鎖定模型版本,避免升級令校準失效。
- 保留豁免出口:合理但觸發規則的寫法(例如刻意 fail-closed 返回
false的校驗器),用行內註解豁免並註明理由。沒有豁免機制的警告系統,最終只會令所有人忽略警告。
五、小結
費曼說過,第一原則是絕不要欺騙自己,而人恰恰最容易自己騙自己。對擅長順着上下文自圓其說的 LLM 而言,這一點尤為明顯。
獨立AI審查在工程流水線中的核心作用,是通過上下文隔離實現交叉審查:審查由獨立節點完成,結果交回人裁決,或交回生成 Agent 修正。
不過,無論是單元測試、靜態分析,還是AI 交叉審查,本質上都只能覆蓋有限的樣本與經驗規則。
我們能否跳出抽樣測試的局限,從數學上直接證明代碼與規格絕對無誤?
下一篇,我們看形式化驗證與 Lean 4。