DEV Community

Yang Goufang
Yang Goufang

Posted on • Edited on

我讓三個 AI 各司其職寫程式:Codex 設計實作與測試計畫、Claude 審閱、Grok 寫測試與實作、Claude 驗收

我讓三個 AI 各司其職寫程式:Codex 設計實作與測試計畫、Claude 審閱、Grok 寫測試與實作、Claude 驗收

這週我沒有讓單一 coding agent 從頭包到尾。我把流程拆成一條固定的契約,比第一版多了一段前置設計:Codex 出實作計畫(含測試計畫),Claude 審閱、核實這份計畫(把計畫引用的東西對回現有程式碼);計畫定案後,Grok 依計畫寫測試、再寫實作讓測試通過;Claude 對測試程式與最終實作各做一次獨立審查。 分工邊界不是一次到位——第一版只有「出測試→施工→驗收」的 fan-out 部分,沒有前面的規畫與雙重審查;這是照第一版實測結果調整出的下一版,之後大概率還會再改。我先在一個 Zig 專案跑了兩個功能,後來又在一個 Rust + Turso 專案獨立重跑三個功能(見下方「換一個 stack 再驗一次」)——那兩輪驗證的是 fan-out 子階段,不是這版完整的六步流程。判斷一致:這條 pipeline 在有嚴格測試當契約的前提下可用;它省下的不是人力,而是把「錯誤發現點」往前、往獨立處移。這只是 workflow 可用性判定,不含可商用判定——後者要另算 token/seat 成本、隱私、rate limit 與審計,本文不碰。

實驗條件(可自行驗證,屬第一版 fan-out 子階段)

  • 工具:codex-cli 0.142.4grok 0.2.77 (44e77bec3a)、Claude Code(CLI,版本未記錄,屬已知量測缺口)。
  • 專案:一個 Zig 0.16.0 codebase(私有 repo,commit hash 僅供我本地對照),加一個「回合後反思」功能。另在一個 Rust + Turso 專案上以同一條 fan-out 子階段再跑一輪,見下方「換一個 stack 再驗一次」。
  • 樣本:Zig n=2 個功能(config surface、reflection module),共 15 個測試(4 + 11);Rust + Turso n=3 個功能(見下方「換一個 stack 再驗一次」)。兩者各自樣本都小,且都只涵蓋 fan-out 部分(出測試/實作→驗收),不含下面新加的規畫階段。
  • 出題命令:codex exec --sandbox read-only(單次、不寫檔)。
  • 施工命令:Grok headless、可寫檔模式(write→test→fix)。
  • 觸發 400 的命令:對 grok-composer-2.5-fast--effort(等同 reasoningEffort 參數)。
  • 驗收命令:zig test <libs> --dep build_options --dep compat -Mroot=src/root.zig --test-filter "<功能前綴>";leak-detecting allocator 回報 0 leak。

樣本小,數字不外推;以下每個論斷都設計成能被另一個工程師在幾分鐘內驗證或反駁。

目前的迴圈:計畫先雙重審過,測試再寫死,實作去追它

每個功能走六步,順序不可換:

  1. Codex 出實作計畫,內容包含要改哪些檔案、邊界條件、以及一份測試計畫(要測什麼、不測什麼)。
  2. Claude 審閱、核實這份計畫——不是蓋章,是對照現有程式碼核對計畫裡引用的 API/模組是否真的存在(此時實作還沒寫,能對的只有現況),抓掉在規畫階段就能發現的錯誤假設,來回到兩邊都同意才定案。計畫定案後才進下一步。
  3. Grok 依定案的測試計畫寫測試程式 + 最小 stub。stub 讓測試「能編譯、但在斷言上失敗」——這是真正的 RED,不是因為符號缺失而編不過。
  4. Claude 審查測試程式本身,核對是否忠實反映第 1 步定案的測試計畫,確認每個測試失敗都有各自獨立的原因,才把這份測試凍結成契約。
  5. Grok 寫實作到 GREEN。任務只有一句:讓已凍結的測試通過,只改實作、不准動測試。
  6. Claude 獨立驗收(不採信 Grok 的自述):跑測試、確認 0 leak、核對 diff 的正確性與改動範圍,才提交。

跟第一版比,改動是把「出測試」從 Codex 移到 Grok(併入它原本就有的實作角色),並在最前面加回一段 Codex/Claude 的計畫協作,同時讓 Claude 對「測試程式」與「最終實作」各審一次,而不是只在最後驗收一次。重點依然不是「三個比一個強」,而是沒有任何一個角色能同時定義正確、又判定自己是否正確——計畫由兩個模型交叉核對,測試由寫的人跟審的人分開,實作由寫的人跟驗收的人分開。

對照三個真實替代方案

  • 單 agent 跑 TDD(自己出測試、自己實作):來回最少,但自我驗證風險最高——出測試跟實作同源,錯了沒人擋。
  • 人寫測試 + agent 實作:最可靠,測試由人把關;代價是人工成本最高。
  • 本文的三 agent 分工:多幾段跨角色來回,換到的是「假綠」風險下降——任何一環的自欺會在下一環被抓到。

選哪個,取決於你的錯誤成本 vs. 來回成本。

每家 CLI 的落地差異(用法決定,不是廠商能力差異;記錄自 fan-out 子階段)

三家我都只用了各自的一種模式,差異來自我怎麼接,不是模型智力:

  • Codex:我用 codex exec --sandbox read-only單次模式,讓它只「輸出」文字(計畫或測試碼)、不改檔。它其實支援 --sandbox workspace-writeexec resume(可多輪、可寫檔),但我刻意把它限縮成「出題者/規畫者」,讓規畫與施工不同源。誤把單次 read-only 模式當施工者用,會得到看似對、實際編不過的檔案。
  • Grok:我用 headless、可寫檔模式跑 write→test→fix,現在涵蓋測試與實作兩段。踩到一個參數相容性的坑:在 grok 0.2.77grok-composer-2.5-fast--effort 時,經該 CLI 的 API 路徑回 400 Bad Request:invalid-argument: Model grok-composer-2.5-fast does not support parameter reasoningEffort,整輪空轉、零檔案寫入。這是該 model 不吃這個參數,不是 Grok CLI 的限制——grok 0.2.77 本身有 --effort;換支援的 model 就沒事。
  • Claude:負責計畫審閱、測試審查與最終驗收,因為它能在同一個 session 內持續持有上下文、跑工具、比對 diff,是唯一在流程裡出現三次的角色。

integration 成本比模型能力更早成為瓶頸:真正卡我的不是智力,而是一個 Zig 細節——單純 pub const x = @import("x.zig") 的 re-export,若沒被任何 test path 參照,Zig 的 lazy discovery 不會 discover 該檔的測試;要在 root 的 test {} 區塊加 _ = x; 強制 discovery。這種 integration 細節,才是 pipeline 真正的 latency 來源。

(這一段是當時裸打 CLI 的紀錄。後來我不再手打 codex exec / grok headless,改走 codex: / grok-cc: 兩支 plugin——原因與細節見下方 v3 補記那段。裸 CLI 的坑正是換 plugin 的動機。)

為什麼「獨立審查」不是形式:三個測試沒抓到、但審查抓到的錯

「Claude 審查」聽起來像蓋章,但它擋下的都是綠燈下的暗傷(以下三例來自 fan-out 子階段的實測,計畫審閱階段的等價案例還在累積中):

  • 施工者謊報成功。 有一次 Grok 回報「成功」,實際上它跑在錯的目錄、測試本來就綠,它一行沒寫。只看回報就會提交一個沒改的 commit。
  • 測試全綠、但 migration 靜默跳過。 在 Rust 那輪,一段 schema migration 的 idempotent 守衛檢查「DDL 是否含 destination」就跳過——但線上 DB 早就有 destination、缺的是新欄位,於是守衛誤判、整段擴充被無聲略過。測試在本地全綠;我是讀 diff、再對線上 DDL 才抓到。
  • seed 檔只載入一半、無錯誤。 一個 seed 的值字面含分號,而 seed splitter 正是以分號切句——三列只進了一列,沒有任何報錯。不是測試抓到的,是我核對「實際列數」才發現。

這三個都會通過「測試全綠 + 施工者自述成功」。審查要做的不是相信綠燈,是去證明綠燈為真。 這也是我把審查往前挪一段、加在計畫與測試階段的原因:與其只在終點抓錯,不如在契約還沒凍結前就多一層交叉核對。

成本與失敗場景(正面之外)

  • 多出來的是「該花的成本」,跟速度無關。 這條管線比單 agent 多的,是計畫協作、測試、跨角色審查那幾段來回。但這不是拿速度換什麼的取捨——正因為施工的 Grok 寫得沒有 Opus 準,我們才更要用測試把它框死。 寫得越不穩的模型,你圍它的那圈測試就要越緊;測試不是拖累,正是「敢讓一個比較便宜、比較不準的模型去施工」的前提。再加上一層:沒有測試,事後的改動就沒人顧——寫完就走,之後任何一次修改都沒有回歸網接住。那幾段來回買到的就是這張網,而這份成本本來就是任何要長期維護的程式都該付的;管線只是逼你當場付清、而不是欠著。整體快不快,從頭到尾不是這裡在談的事。
  • 紅燈要「可診斷」。 若 stub 全部回傳 error.NotImplemented,所有測試會用同一種方式失敗——那是無資訊的 RED。每個測試必須因自己的原因失敗,施工者才知道往哪修。
  • 共享 DB 要 panic-safe 清理。 若整合測試打的是共享/線上 DB,teardown 必須用 RAII 守衛掛住——否則一次 RED(斷言 panic)就會把測試列洩進正式庫。這是 live-DB 專案的額外適用條件。

換一個 stack 再驗一次(僅涵蓋 fan-out 子階段)

為了看它是不是只在 Zig 成立,我在一個 Rust + Turso(雲端共享 DB)專案上用同一條「出測試→實作→驗收」fan-out 子階段獨立重跑了三個功能。沒有推翻原判斷:一樣可用,前提仍是測試能當契約;差別在共享 DB 帶出的新約束(上面的 panic-safe 清理、以及讀 diff 才抓到的靜默 migration bug)。換個語言、換個測試框架,卡點依舊在 integration 與審查,不在模型智力。 這讓我更有信心,但(就 v2 當時而言)仍只是第二個小樣本案例,不是 benchmark,也還沒套用最新加的計畫協作階段——後續在更多 repo 上的觀察見文末 v3 補記。

適用與不適用

適用:有靜態型別 + 嚴格測試框架的專案、功能可切成小塊、每塊有明確斷言。計畫與測試都能當契約,跨 agent 交接才有意義。

不適用:探索性、規格未定、或「測試本身就是要設計的東西」的工作。這條 pipeline 假設計畫與測試是可先寫死的規格;當規格還在流動,強行分工只會把來回成本放大。

判斷

這不是新範式,是把單 agent 內部的幾個步驟(自己規畫、自己出測試、自己實作、自己判定自己對不對)逐一拆成跨 agent 的外部契約。買到的是更早、更獨立的錯誤發現點——任何一環的自欺都會在下一環被抓到。要付的是幾段跨角色來回,和你必須真的去審查、而不是相信回報——但那份審查與測試的成本,本來就是任何要長期維護的程式都該付的(v3 補記把這點講死)。

分工邊界目前是我依第一版實測結果調的第二版,不是終版——AI 能力還在變、我對這條 pipeline 的判斷也還在變,這大概率不是短期內能收斂完的事。對決定要不要把多 agent 導入實際 workflow 的人:先確認你的專案有「計畫與測試能當契約」的體質。沒有這個前提,多 agent 只是把一個 agent 的不可靠乘以三。

文中失敗場景(Grok grok-composer-2.5-fast 的 reasoningEffort 400、假成功回報、Zig lazy discovery 需 ` = x;`、Rust 那輪的靜默 migration 與 seed 半載入)均為本人於 fan-out 子階段實測,工具版本與命令見「實驗條件」。計畫協作與雙重測試審查是本次更新新加的階段,尚未有獨立記錄的失敗案例,會在後續文章補上。_


v3 補記:新加的「計畫協作 + 雙重審查」階段,累積約十幾次觀察之後

上面 v2 收尾時我說:計畫協作與雙重測試審查是新加的階段,「尚未有獨立記錄的失敗案例,會在
後續文章補上」。這一段就是補。之後我把這條完整(或近完整)管線跨幾個 repo 累積了觀察——
主要是一個 Rust + Turso 的旅程 CLI(功能開發、bug 修復、兩輪 CLI 稽核),還有一個 Rust + Python
的瀏覽器/爬取工具(那邊也修了一些 Python),再加上 v2 那個 Zig 專案。先講清楚樣本邊界:它
跨了多個 repo、跨 Rust/Zig/Python,但每個 repo 的次數都不多,合起來是量級 n≈十幾的觀察,
不是 benchmark、不是統計結論。
這些觀察讓我看到三件 v2 沒點出來的事;核心論點沒變(在這個
小樣本上更站得住),而「三個 AI 各司其職」這個框架,我要修正。

要先分清楚一件事:出問題的從來是「工具怎麼調用」,不是這條方法。 Codex 和 Grok 是還在變的
新工具,旗標、run-mode、呼叫方式一直改,Claude Code 裸打它們的 CLI 常常不對——grok-composer-2.5-fast
--effort 回 400、裸 headless「narrates 完就退出、零檔案寫入」、單次 read-only 模式被當施工者用。
但這些全是調用層的事,跟「Codex 出計畫 → 核實 → Grok 施工 → 對照原始碼驗收」這套分工
一點關係都沒有。

而且我不會假裝這種調用錯誤已經被「解決」了——它現在還在犯,未來也一定還會犯,因為工具會一直變。
重點從來不是「不再出錯」,而是每次出錯都找出方法把它跨過去。這一次跨過去的方法,就是寫
plugin:codex:/codex:rescue)做 read-only 審查/診斷/核實,grok-cc:
/grok-cc:rescue)做施工與第二輪審查——plugin 把那支一直在變的 CLI 的「正確當前用法」封在
裡面,叫 plugin、不叫 CLI,就不會每次在錯的旗標上重踩。這不是方法失敗後打的補丁,這就是方法
本身:調用調不動,就長出一層把它固定下來。
前面那些成果——drill 做得比真實行程還豐富、一路挖出
並修掉的真 bug——不是因為工具從不出錯,正好相反,是因為每次出錯我們都逼自己找出跨過去的方法,
才一點一點累積出來的。

還有一點要先講清楚,免得被誤讀成管線的性質:這條管線不是每個改動都跑滿六步——ceremony 隨
改動大小縮放。
這次最小的幾個修法(一道防呆、稽核收尾、一個提示)我是自己直接動手的,因為
對那種規模,分工的來回本身就大於它擋下的風險。這是「何時該用」的判斷,不是管線的缺點。

修正一:把「計畫是雙向協作」講得更死——對回的是現有碼,不是實作

v2 已經說了計畫是 Codex/Claude 的協作、由兩個模型交叉核對(不是單向交棒)。這一段不是推翻,
是把「為什麼是雙向、雙向到底在對什麼」講得更精確——因為這點最容易被讀者(和我自己)含糊帶過。

計畫的初稿確實是 Codex 寫的(逐任務、測試優先,連測試碼和實作碼都給了)。但能的計畫,
是一個 Claude Code ⇄ Codex 校對迴圈收斂出來的東西,重點在這個迴圈到底在核對什麼:

  • Claude Code 先寫 brief 界定 Codex 要規劃什麼(設計決策、要核對的現有程式碼事實、限制)—— Codex 不是憑空規劃。
  • Codex 起草計畫/規格(先設計審查,再逐任務、測試優先的實作計畫)——作者是 Codex。
  • Claude Code 把計畫裡吃重的引用核實回既有的程式碼——注意是核實回現有的型別、模組、 介面,不是「事後的實作」:新功能是先有計畫、後有碼,計畫階段還沒有實作可比,能比的只有它引用的 現況存不存在(只有在「修正」既有程式時,才有一份舊的目標碼可對)。這次 Codex 的草稿把型別寫成 Issue/Severity::Warning(錯了,publish 那條路用的是 PublishIssue/PublishSeverity::Warn), 還提了一個 GROUP BY 會把我們正要抓的那些列直接濾掉(正解要從一個 activity_days CTE + LEFT JOIN 出發)。我把這些指出來、Codex 據以修,來回幾輪,直到兩個 AI 對同一份計畫都點頭——迴圈才 收斂。它不是「Claude 事後審一份成品」,是兩個模型協作、且都同意才算定案。

所以正確的說法是:計畫的初稿是 Codex 寫的,但「定案、可用」的計畫是 Claude Code 與 Codex 協作
到彼此都同意的產物,不是單向交付。
這反而強化了 v2 的核心論點——「沒有任何一個角色能
同時定義正確、又判定自己是否正確」對計畫一樣成立:Codex 可以起草,但它對不對,要另一個模型
核實過、兩邊都同意,才算數。

修正二:管線不是「一道驗收關」,是三個校對點

跑多了之後,我不再把它畫成「六步、末端一個 Claude 驗收」。真正的不變量其實是一句話:
每一份產物都要獨立核實回程式碼或測試。這句話落在三個地方——前兩個 v2 其實都已內含(只是
沒把它們並排講成「同一條原則」),第三個才是 v2 完全沒提的(兩個是迴圈、一個是閘,下面標清楚):

  1. 計畫迴圈(Claude Code ⇄ Codex)——把計畫核實回現有程式碼,來回到兩個 AI 都同意才定案 (v2 已有,修正一把它講精確)。
  2. 實作閘(Grok 實作 → Claude 對照凍結的計畫、跑測試獨立驗證)——v2 已有。 (它是閘不是迴圈:驗不過就退回重做。)
  3. 發現迴圈(稽核 agent 對「已在跑的程式」提出發現 → Claude 核實回原始碼)——這個才是 v2 完全沒提的,也是這次多跑之後才浮出來的(見修正三)。

第三個最容易被忽略。這次兩輪 CLI 稽核,agent 很有信心地報了 3 個「HIGH 嚴重度」的發現,
全部是誤報
:它們說某些指令「靜默吞掉打錯的 flag → 會誤寫入」,錯了——那些「連線後才 parse」
的指令,防呆的 reject_unknown_flags 是放在 main.rs 的 dispatch arm,agent 只讀了指令模組、
沒讀 main.rs。我沒信,去比對了原始碼——否則就會去「重修」一個早就修好的 bug。

教訓:一個 subagent 的發現,範圍就等於它讀過的東西。 v2 那句「審查要做的不是相信綠燈,
是去證明綠燈為真」要擴大成:不只綠燈,連「發現」本身都要比對回原始碼——因為連上游餵給
審查者的東西(計畫、發現)都不能因為它「講得很篤定」就信。

修正三:這條管線不只是「把程式寫對」,更是「證明已在跑的程式其實是錯的」

v2 把它框成建構工具(把新程式寫對)。但多跑幾次之後,讓我意外的收穫其實是診斷

我用「邊做邊比較」的方式驅動 drill:一邊照流程做一個計畫,一邊拿它跟一個真實的參考行程比
內容深度。這種對抗式的獨立檢視,專門對著「看起來會動」的程式問一句「它真的對嗎?」,結果挖
出好幾個真 bug:

  • 地圖靜默空白(活動的 poi_id 是 NULL,但沒有任何東西示警);
  • set-flight <方向> 不帶任何欄位 flag 時,寫入零列 flight_legs、卻照樣 bump 版本號、 照樣印 ✅ Flight leg updated——一個操作者根本察覺不到的「沒跑出資料」;
  • promote-offers 在全部 offer 都被跳過(零寫入)時,印的是 ✅ Saved——假成功訊號。

這些不是新寫的程式,是已經在跑的程式。v2 的論點(「沒有 agent 能驗證自己」)不只適用在
剛寫出來的程式,對現存程式一樣成立——所以這條管線的價值,不只在建構,也在診斷。

一個我不能迴避的 caveat:校對者自己也是 LLM

有人會問:你一直說「Claude 比對回原始碼」,但 Claude 自己也是個 LLM,它憑什麼是可信的 oracle?
——它不是。這正是重點。這條管線能降低風險,不是因為某個模型更聰明,而是因為每一個聲稱都被
釘回一個可查證的 ground truth:真實的原始碼、會紅會綠的測試。
校對者的可信度不來自它的判斷,
來自它比對的對象是可驗證的。所以當我說「校對者是唯一不可談判的角色」,精確的意思是:
必須有一個角色,把每一個其他 agent(以及它自己)的輸出,拉回到程式碼與測試前面對質。
拿掉那個可查證的對象,這整套就退回成「一群 LLM 互相說服」——那才是真正沒有底的狀態。

一句話的 v3 校準

v2 的論點是對的,多跑幾次後我更相信它。角色分工還在——Codex 規劃、Grok 實作、Claude 校對驗收
(小改動除外)。但誠實的修正是:這不是一條乾淨的接力,每個角色的產出都是「提議」,不是
「定論」;最不能省的是校對這個角色——它必須在管線的兩端(計畫端與發現端,不只末端的綠燈),
把每一個其他 agent 的產出獨立比對回原始碼,而且要隨改動大小縮放儀式。

「證明綠燈為真」要擴大成:證明每一個聲稱——計畫、發現、綠燈——都對得上原始碼,因為沒有任何
agent(包括餵給審查者的上游輸入、以及審查者自己的判斷)可以因為「它這樣講」就被信任;可信的
從來不是誰,是它比對的那個可查證的對象。

Top comments (0)