同一個 PR,兩種不同的 review

我認識的一位開發者,把 Claude Code 設成 CI 裡的程式碼 reviewer。「就讓 Claude 幫我看 PR,」他對我說,「它會抓到一些我可能漏掉的東西。」我請他把同一個 PR 丟給 Claude 跑兩次。

第一次 review 說 error handling 看起來很完整。第二次 review 卻說它不夠完整,還提出三個修改建議。他盯著螢幕看了五分鐘,搞不清楚到底哪一個 Claude 才是對的。

兩個都不算對,也兩個都不算錯。 LLM 做的是機率性的猜測,不是在套用確定性的規則。而這正是用 AI 做 verification 的核心問題:verification 需要兩個 LLM 沒有的特性。確定性,也就是相同輸入永遠產生相同輸出。完備性,也就是所有 failure mode 每次都能被抓住。

確定性的問題

同一段程式碼丟給 Claude Code 跑兩次,你就可能得到兩份不同的 review。temperature、context window、prompt phrasing,都會帶來變異。對腦力激盪來說,變異是 feature。對 verification 來說,變異就是 bug。你沒辦法把可靠的軟體建立在機率性的 quality gate 上。

確定性檢查不會有這個問題。tsc --noEmit 每次都會產生相同的錯誤。相同設定下的 ESLint,每次都會標出同一批問題。這些工具套用的是 規則,不是機率。它們很無聊。也正因為如此,它們才可靠。

O(1) Guard Stack

這就是我們的 pre-commit stack。每一個檢查都是確定性的。每一個檢查都在毫秒級完成。它們完全不在乎程式碼是人寫的還是 Claude Code 寫的:

# Pre-commit (< 1 second total)
TypeScript strict mode      # tsc --noEmit
ESLint boundary rules       # module isolation
Import restrictions         # dependency direction
dependency-cruiser          # graph analysis
Unit tests                  # contract verification

# CI (< 30 seconds)
Full test suite
Bundle size checks
Fresh clone launch test

真正重要的指標,是 相對於程式碼庫規模的 O(1)。type checking 是增量式的。linting 是逐檔案的。dependency-cruiser 會線性分析 import graph。你的 Vibe-coded app 再怎麼長大,它們都不會像 LLM review 那樣變得越來越慢、越來越不準,直到整個 codebase 超出 context window。

確定性檢查到底抓得到什麼

上週,dependency-cruiser 抓到一個 PR:某個 screen component 沒有從 barrel import,而是直接 import 了 service implementation。這個 PR compile 完全沒問題。測試也都過了。那個 import 還是 Cursor 自動補進去的。但它違反了我們的架構規則:只有 composition root 可以 import implementation。

LLM review 也許抓得到這個問題。也可能因為 PR 太大、那行 import 藏在檔案中段,就直接漏掉。dependency-cruiser 永遠不會漏,因為它不是在「讀程式碼」,它是在分析 graph:

// .dependency-cruiser.cjs
// 8 lines. 100ms. Catches this every time, forever.
{
  name: 'no-deep-service-imports',
  comment: 'Only service barrels are public.',
  severity: 'error',
  from: { pathNot: '^src/services/' },
  to: {
    path: '^src/services/[^/]+/',
    pathNot: '^src/services/[^/]+/index\.ts$',
  }
}

八行。不到一百毫秒。每一個 PR。永遠有效。Claude Code review 會花 token,要花幾秒,還可能直接漏掉。這筆帳根本不用算。

AI 真正該待的位置

我不是反 AI。我每天都在用 Cursor 和 Claude Code,拿來起草程式碼、探索方案、寫測試、除錯。但我不會把它們放進 verification 的角色裡。verification 必須是確定性的。必須是無聊的。必須每一次都一模一樣。

護欄應該是看不見、卻又不可避免的。它們應該在沒人特別記得去跑它們的情況下,就把問題攔下來。這正是確定性檢查能給你的東西:一張永遠不睡覺、不會累、也不會在程式碼庫變大時失去上下文的安全網。

Autotomy Expo Starter Pack 內建了 TypeScript strict mode、ESLint boundary rules、dependency-cruiser 設定,以及 pre-commit hooks,而且都已經預先配置好。你用 Cursor 享受生成速度。你用機器等級的精準度做 verification。這才是 Vibe coding 又不打壞 production 的方法。