今天測的是 codex——Claude Code 官方市集裡橋接 OpenAI Codex(GPT-5.4)的套件,讓你在 Claude Code 裡直接叫一個完全不同供應商的模型來審查或協助。前九天測的都是同一個生態系裡的工具,這是第一次測「跨廠牌找第二意見」這件事本身值不值得。
會想測這個,是因為「找一個不同的模型當第二意見」聽起來是個很直覺的做法——同一個模型審查自己的東西,總有一種球員兼裁判的疑慮;換一個完全不同公司、不同訓練資料、不同傾向的模型來看一眼,感覺上應該能補上盲點。這個直覺對不對,今天想拿實際案例測一次,不是憑印象下結論。
這個直覺背後其實藏著一個沒有講清楚的假設:「不同模型會看到不同的東西」,跟「不同模型看到同一件事、但表達或驗證的方式不同」,是兩種完全不同的差異,混在一起講的時候,聽起來都叫「第二意見有幫助」,但對使用者的實際意義完全不一樣。前者代表你真的需要多一雙眼睛才能看全;後者代表你原本那雙眼睛已經夠用,差別只在於多一個角度去佐證。今天的測試想拆開這兩種可能,看實際發生的是哪一種。結果不是單純的誰比較強,是兩種審查風格的具體差異,這個差異本身比輸贏更有用。
codex,官方市集出品(openai-codex),底層打的是 OpenAI 的 Codex CLI,模型是 GPT-5.4/codex:adversarial-review——文件裡明講定位是「挑戰目前的實作做法對不對、設計選擇合不合理、有什麼假設、在真實條件下哪裡會失敗」,不只是抓一般的程式碼缺陷;跟它並列的 /codex:review 才是比較標準的一般程式碼審查codex-rescue:專門在 Claude 自己卡住、想要第二種實作方式,或需要更深入診斷時,把整個任務轉交給 Codex 處理,不是單純的審查——今天沒有測到這個,只測了審查這一塊i-have-adhd「開頭給動作」的邏輯其實有點像,只是這裡的「動作」換成了「先看結論再看細節」值得多講一句這個橋接是怎麼運作的。Codex CLI 本身是一支獨立安裝、獨立登入的命令列工具,跟 Claude Code 沒有任何關係;這個套件做的事,是在 Claude Code 裡包一層腳本,把使用者下的指令轉譯成呼叫 Codex CLI 的參數,等 Codex 跑完,再把它的原始輸出整段搬回 Claude Code 的對話裡,不經過摘要、不經過二次詮釋。這個「原文照搬、不做二次詮釋」的設計本身值得注意——如果 Claude 把 Codex 的發現先讀過一遍、用自己的話重新講一次,中間就有機會把 Codex 原本的判斷不小心改寫掉、或者夾帶自己的意見進去,讀者會分不清楚這句話到底是誰講的。文件裡特別強調要保留 Codex 標記的「這是推論」「這是不確定」「這是待確認的問題」這些邊界,不能在轉述過程中把語氣拉得更篤定,這是一個認真在意「不要污染另一個意見」的設計選擇。
寫了一個報表快取服務:查詢過的報表存進記憶體字典,同樣的請求第二次直接從快取拿,不用重算。程式邏輯完全正確,兩個單元測試都會過。真正的問題藏在一個地方:這個快取沒有任何上限、沒有淘汰機制——只要進來的資料集內容有一點點不同(哪怕多一個空白),就會被當成新的鍵永久留在記憶體裡,不會自動清掉。這種問題不會讓測試變紅,只會讓正式環境跑久了、跑到夠多不同的請求之後,慢慢把記憶體吃光——這正是「功能正確」跟「設計沒問題」之間的落差,也是 adversarial-review 這種強調「挑戰設計選擇」的審查角度該抓到的東西。
具體想像一下這個問題會怎麼發生:如果這支服務每天處理幾千個不同使用者、每個人查詢的資料集內容都不完全一樣,快取字典裡的鍵就會每天新增幾千筆,而且沒有任何一筆會被清掉。跑一天可能感覺不出差異,跑一個禮拜、一個月,記憶體用量會持續往上爬,跟流量、跟有沒有 bug 都沒有關係,純粹是時間的函數。這種問題最麻煩的地方,是它不會在開發環境、也不會在測試階段暴露——測試通常只會送幾次固定的請求,資料量小到不可能撐爆記憶體;只有真的上線、跑過一段夠長的時間,才會在某個深夜悄悄把服務榨乾。這也是為什麼這類問題特別考驗審查者的功力:不是眼前的程式碼有沒有錯,是要在腦中模擬「這支程式跑三個月會變成什麼樣子」。
選這種問題當案例是刻意的。前面幾天測過的資安類漏洞(SQL injection、路徑穿越),本質上都是「這行程式碼寫錯了」,靜態分析工具靠比對已知的危險模式就能抓到;今天這個問題不是「寫錯」,程式碼每一行單獨看都合理、都正確,問題出在「這個設計選擇長期跑下去會怎樣」,這種問題沒有一個固定的語法模式可以比對,得靠真的理解這段程式碼想解決什麼問題、部署在什麼環境下、會被呼叫多少次,才看得出來——這正是「審查設計」跟「掃描漏洞」的本質差異,也是今天想測 adversarial-review 而不是普通 review 的原因。
adversarial-review 跑完,結論是「暫不建議上線」,理由直接寫「程序共用快取無容量限制,會永久保留每份不同的輸入資料,存在記憶體耗盡風險」。抓到的問題跟我設計的一致。
真正讓我意外的是它怎麼證明這件事——它沒有只憑讀程式碼就下結論,是自己寫了一段程式,真的送了 1,000 次不同的請求進去,量出快取確實留住了 1,000 筆記錄、總共超過一千萬個字元的資料留在記憶體裡,才把這個數字寫進報告。這不是空口說「可能會有記憶體問題」,是有一組可以重複驗證的實測數字撐著。它的完整發現只有這一條,標成「高風險」,附上具體建議(要嘛加一個有總位元組預算的淘汰機制,要嘛對超大資料集跳過快取),然後在「下一步」直接寫「補上快取容量與淘汰測試後再審查」,沒有節外生枝去講別的。
這個「不只講理論、還真的跑一次」的習慣,其實在它一開始的執行紀錄裡就看得出來——它做的第一件事是跑 git diff 看真實的變更內容,第二件事是直接執行程式碼、印出檔案內容跟行號,確認自己看到的程式碼跟實際狀態一致,才開始下判斷。這種「先核對現場、再下結論」的順序,跟這系列從 Day 1 開始一直強調的方法論剛好對上——不是因為它讀過這系列的文章才這樣做,是巧合地示範了同一套紀律。
不用任何工具的審查也抓到了同一個核心問題(無上限快取),但接下來的內容明顯更廣,而且依嚴重度分成「必須擋下」跟「該修但不用擋」兩層,講得很有條理。原文是這樣寫這條核心問題的:
「Cache 沒有上限、沒有 TTL、沒有 eviction——這是正式環境的 OOM 風險。
ReportCache._cache是一個永遠只增不減的 dict……這是典型會在生產環境半夜把 process 撐爆的 memory leak。」
除了記憶體風險,它還挑出一件我覺得最犀利的事:那支測試 test_same_request_uses_cache 其實不管有沒有真的做快取都會通過——因為 _render_report 本身是決定性函式,同樣輸入本來就會得到同樣輸出,這支測試沒有真正證明「快取生效、沒有重算」這件事,是一個套套邏輯式的假保障。原文的講法是:
「
_render_report是純函式、deterministic,就算完全不做 cache,r1 == r2一樣會成立……這支測試在『拔掉整個 cache 機制、直接每次都重算』的情況下依然會通過——換句話說它沒有真正鎖住這支程式要保證的行為。」
另外還提到 dataset_json 這個參數名跟文件字串都暗示會解析 JSON,但實作只是把字串用逗號切開,完全沒有 json.loads——如果之後真的丟進符合 JSON 格式的字串(陣列、物件屬性、字串值裡面本來就常常帶逗號),會產生一個「看起來有跑、但結果完全錯」的結果,比直接報錯更危險;測試直接戳私有屬性、共用全域單例造成的測試間依賴風險;多執行緒下兩個請求同時 miss 同一個鍵時可能重複計算的競爭條件;多行程部署下每個 process 各自維護一份快取、快取形同虛設;以及完全沒有清除快取的對外介面。七個問題,一次性列完。
這七個問題裡,「參數名不符實」跟「套套邏輯測試」這兩條特別值得多講一句,因為它們都不是「這段程式碼會不會壞掉」層次的問題,是「這段程式碼會不會誤導下一個接手的人」層次的問題——一個叫 dataset_json 的參數卻沒有真的處理 JSON,一個叫 test_same_request_uses_cache 的測試卻沒有真的測到快取有沒有生效,兩者的共通點是「名字承諾的事,實作沒有兌現」。這種問題不會讓系統當機,但會讓維護這段程式碼的人建立錯誤的心智模型,之後在這個錯誤的假設上疊加更多程式碼,錯誤會越滾越大——這正是那種只讀程式碼、願意花時間細看每一行意圖跟實作有沒有對齊的審查方式,比較容易抓到的類型。
把兩份報告並排看,不是一邊比較準、一邊比較不準——是兩種完全不同的審查風格。codex 這次做的事,範圍窄,但那一條發現有真正的實驗數據撐著,不是憑印象推測;基線做的事,範圍廣,涵蓋了設計、測試品質、部署環境好幾個層面,但每一條都停在「讀程式碼推論出來的判斷」,沒有一條像 codex 那樣附一組實測數字。
這個差異值得記住,因為它推翻了我原本設計這篇時的預設——我以為找一個跨廠牌的「第二意見」,賣點會是「補上另一邊沒看到的東西」,範圍上互補。今天測出來的比較像是兩種審查方法論的差異,不是誰看得比較全面:一個選擇窄而深、拿證據說話;一個選擇廣而快、靠經驗判斷。這兩種都有真實的價值,但適合的情境不一樣,不能簡單說裝了 codex 就等於多一雙眼睛幫你看得更廣。
換個角度想這件事:如果今天的任務是「說服一個懷疑論者這個設計真的有問題」,codex 那份帶著實測數字的報告會更有說服力——你可以直接把那組「1,000 筆、一千萬字元」的數字拿去開會,沒有人能反駁「這只是你的猜測」。如果今天的任務是「盡量不要漏掉任何一個值得討論的地方」,基線那份廣而完整的清單會更有用,代價是每一條都還需要自己再花時間確認是不是真的成立。兩種需求在真實工作裡都會出現,只是很少同時出現在同一個當下——這也是為什麼「兩邊都跑一次、拿兩份報告互相對照」本身是一個合理的工作方式,不是浪費,是刻意讓兩種審查風格互補彼此的弱點。
還有一個角度值得補充:codex 選擇「只講一件事」,也可能不是它能力上限,是它對「adversarial review」這個任務類型的自我定位——文件裡講得很明白,這個指令的核心任務是挑戰「做法對不對」,不是條列所有大小毛病。如果它把七個問題不分輕重全部列出來,反而可能稀釋掉最關鍵的那一個。今天看到的「窄而深」,某種程度上是它刻意收斂範圍、把火力集中在它判斷最致命的那一點,這是一種設計選擇,不是廣度不夠的缺陷——只是身為使用者,如果你沒有意識到這個設計選擇存在,很容易誤以為「它只找到一個問題」等於「它只看得到一個問題」,這兩件事其實不一樣。
adversarial-review 這類定位成「挑戰設計」的審查,適合拿來驗證一個你已經有懷疑、但需要證據才敢下決定的地方,不適合當成一次性的全面體檢。 今天它只挑了一件事出來講清楚,這是它的設計傾向,不是它的能力上限——如果你要的是全面掃過一輪,這不是它的強項。dataset_json 沒真的解析 JSON)確實成立,其餘幾個(執行緒安全、多行程部署風險)屬於合理的工程判斷,沒有像今天核心問題那樣另外找方法驗證過。adversarial-review 這一個子指令,codex 套件裡還有標準版的 /codex:review、以及專門處理棘手任務的 codex-rescue,今天都沒有測到。昨天測 CodeQL 的時候發現,案例規模不夠大,沒能真正逼出「進階工具才做得到」的優勢;今天延續類似的主題,但角度不一樣——不是規模的問題,是審查方法本身有沒有拿出證據的問題。同一個問題,一邊用讀程式碼推論出來,一邊用真的跑一次實驗量出來,兩者說的是同一件事,但可信的重量不一樣。這系列走到第十天,反覆出現的其實是同一個提醒的不同面貌:講得篤定、講得全面,都不能取代「這件事有沒有真的被驗證過」——不管是工具的宣稱、審查的發現,還是今天這種「找誰來審查比較好」的選擇本身,都適用同一個問法。
十天測下來,累積的評分尺沒有變過:有沒有實跑、有沒有對照、有沒有留下可以重新檢查的證據。今天多驗證的一件事是,這把尺不只能拿來量 Skill 本身做得準不準,也能拿來量一份「審查報告」寫得可不可信——同一套標準,量的對象換了,方法完全沒變。
寫到第十天,回頭看這系列一開始立下的規矩——固定任務、有無對照、留下證據、只寫已驗證的結果——今天算是把這套規矩用在一個新的地方:不是拿去測一個 Skill 有沒有做到它宣稱的事,是拿去比較兩份審查報告誰講得比較站得住腳。方法沒變,用途卻自然而然地延伸出去了,這大概也是「同一把尺」這個系列名稱真正想講的事——尺本身很單純,值錢的是拿它去量過的次數夠不夠多、夠不夠老實。
今天也順帶留一個沒答完的問題:如果案例裡藏的不是一個問題、而是三四個同等重要的問題,codex 這種「挑一件事講清楚」的風格,會不會漏掉其中幾個?今天的案例只埋了一個核心問題,沒辦法回答這個問題。這也是下次如果要更嚴謹驗證這個工具時,該補的一塊。
明天想換一個方向測,這系列接下來還是每篇一個獨立的技能卡,累積起來變成一份你自己就能查的 skill 選用清單。