Bun 花了 16.5 萬美元、最多同時 64 支 Claude、11 天,把 53 萬多行程式碼從 Zig 改寫成 Rust,還讓它通過三種作業系統上的所有既有測試。而差不多同一個月,我自己一個小得多的系統,示範了同一招防不住哪一種錯——而且防不住的方式,剛好是規模越大、跑得越快,代價就越貴的那一種。
先講清楚我不主張什麼。我不是要說 Bun 這次重寫有問題——目前看不出任何跡象顯示它有。我也不是要主張「把審查和實作分開」或「找不同模型來審」是我發明的概念,這兩件事業界討論很久了,我只是剛好在自己的系統裡,用真實數字撞見了它的邊界。我要主張的只有一件事:把實作和審查分開,防得住的是「AI 自己幫自己蓋章」;它防不住的是「實作跟審查用的其實是同一顆腦袋,連盲點都是同一個」。而規模——不管是 64 支平行 Claude,還是我自己那套小得多的流程——只會讓這種防不住的錯,用更快的速度跑到更下游。
還有一件事先標好:這篇是論證文,不是像上一篇(LLM Wiki 那篇)那樣的純實測報告——但我會帶一組真實讀數進來,因為這條主張如果沒有讀數撐著,就只是一個聽起來合理的猜測。順便交代一句系列的來歷:上一篇談敏捷 vs 瀑布時,代價表裡我丟了一句話沒有展開——「換一個 AI 來審,消不掉它們天生一樣的盲點。這種事情上多一層不叫過度工程。」 這篇,就是那句話的完整案例,而且它比我當初寫下那句話時想的還要複雜一點。
一、先把 Bun 的故事講完,最短篇幅
Bun 是一套執行、打包、測試 JavaScript/TypeScript 的開發工具,2021 年由 Jarred Sumner 用 Zig 寫成。Zig 給了他底層控制力,但控制力的代價是記憶體要自己管——洩漏、重複釋放、越界讀寫,隨著規模擴大反覆出現。傳統做法重寫成 Rust,估計要 3 位資深工程師投入一年,而且期間團隊得暫停新功能開發,這個念頭因此擱置了很久。
直到 Claude Code 成熟到讓他先花一週驗證可行性——看到通過的測試越來越多,他才拍板。正式重寫前,先花 3 小時和 AI 整理 Zig 轉 Rust 的規則,挑 3 個檔案試跑;確認流程可行後,設計了 50 套 AI 工作流程,分工改寫、編譯、除錯、測試、審查。關鍵設計是:實作的 Agent 和審查的 Agent 刻意分離——審查者不看實作者的推理過程,收到的指令不是「檢查看看」,而是直接假設這裡有錯,去把它找出來。流程穩定後擴大到全部 1,448 個檔案,拆 4 個 Git 工作區、每個 16 支 Claude,最高峰 64 支平行作業。
驗收靠的是 Bun 原本就有的 TypeScript 自動測試——不管底層語言是什麼,只要行為跟 Zig 版一致就算過。第一次跑,972 個測試檔失敗;兩天後降到 23 個;最終在 Linux、Windows、macOS 上全數通過,沒有刪除或跳過任何一個測試。效能提升 2%~5%,記憶體大幅下降,修復了 128 項舊問題。
這篇報導本身,順帶留下一個有意思的小事故:它寫「目前仍有 19 項已知功能退步」,但一手原文其實是「這 19 項迴歸問題都已修復」——二手轉述漏掉了「已修復」,方向整個反過來。我不是要挑這篇報導的錯,這種漏字任何人來寫都可能發生;我要說的是:抓到這個落差的方法,不是「再讀一次」,是「去對一手來源」。 跟我接下來要講的事,是同一個結構。也順帶說一句誠實話:除了這三個核心數字(53 萬行、2,200 萬月下載、16.5 萬美元)我對過一手來源,測試規模、commit 數、64 支這個峰值,都是廠商自陳,我沒有獨立複核的管道。
二、我自己那套一模一樣的設計,跑了 8 輪
我自己維護一套規格,用來治理我平常怎麼跟 AI 一起做軟體工程。裡面有一條規則,管的是「自主執行時誰來把關」——設計是:做事的是一個 AI,審查的是另一個 AI,兩者用不同的 context、不同的指令,審查者甚至被明確告知「直接假設這裡有錯,你的任務是把它找出來」。這跟前面那次改寫用的「實作 Agent 與審查 Agent 刻意分離」,是同一個結構,連「審查者不看實作者的推理過程」這個設計都幾乎一樣。
這套機制跑起來,確實有用——直到它撞上一次真實案例。
我手上有一個真實在跑的專案,一份規格文件正走一套四道品質關卡(各自對應文件品質的不同面向)。用上面那套「同模型、不同 context」的審查機制,連續跑了 8 輪——每一輪都重新起一個獨立的審查者,不看前面幾輪的結論,從頭讀一遍。8 輪跑下來,結論收斂了:四道關卡全部通過,分數 85、89、94、90,零個阻礙性問題,判定可以封存。
這組讀數先擺在這裡,但它的作用不是給你一個「AI 審查要跑幾輪才夠」的公式——是給你一個形狀:同模型重複審查,不是每一輪都獨立地在檢查真相,它會收斂到一個「大家都覺得合理」的共識,而那個共識可能剛好就是所有輪次共享的那個盲點。8 輪不是 8 次獨立驗證,它更像是同一雙眼睛看了 8 次,而每一次都更確信自己看到的是對的。
於是我做了跟前面那套設計不太一樣的一件事——不是再分出一個角色,是換一整個模型家族。找了一個不是 Claude 的模型,一樣唯讀、一樣不准動任何東西,但這次連「打分數」的權力都不給它——它只准列異議,通不通過還是原本那套機制說了算。
同一批文件、同一份被判定「四道全過」的東西,換了家族的模型看一遍,直接把其中三道翻成不通過,最大的一道分數落差是 20 分。
三、換模型不是換成先知——我親自核對了每一條
而「別的模型說的話」,我也沒有照單全收——這正是這條規則的另一半:換模型是多一雙眼睛,不是多一個先知。 我拿著它列出的每一條異議,回去直接對真實檔案內容做核對,不是看它的轉述。結果是:8 條它講的問題,我親手核對後確認屬實;1 條,它講錯了——它說某個功能「完全沒有對應的設計」,但那個設計其實存在,只是它沒找到,不代表不存在;另外 10 條,當下沒逐一驗證,誠實標成「未查」——既不算通過,也不算失敗。
把那 8 條攤開來看,有一個很扎眼的規律:好幾條的根因,不是「某一份文件寫錯了」,是「兩份文件分開看都對,疊在一起才矛盾」——這種問題,任何一輪只讀一份文件、按存在性 checklist 打勾的審查,結構上就是抓不到,因為它從沒有「同時攤開兩份文件、做交集判斷」這個動作。更難堪的是另一個規律:至少兩條問題,是我自己在前幾輪「把修正結果同步到其他文件」時,親手加進去的——為了讓幾份文件對得上,反而把不該出現在這裡的東西寫了進去。而接下來每一輪的審查,審的都是同一個我,連盲點都是我自己種下去的那個。
四、照理說故事該結束了——但沒有
照理說,找到盲點、修掉、再驗一次、乾淨了、可以封存,故事該結束了。我原本也是這樣想的。
但接下來又跑了兩輪修正,再做一次完整重審——這次連審查方式都換了:除了原本的獨立審查者重審一遍,還額外找了 8 個一樣是同模型、但拿到的指令極其具體的窄範圍審查員平行跑,每一條指令都是我自己手寫的「去查某條規則有沒有被某個具體東西承接住」,不是籠統的「幫我看看有沒有問題」。
結果是:又找到了 3 個從沒被任何一輪、包含前面那次跨模型審查在內,抓到過的全新阻礙性問題——這些不是這兩輪修正造成的新錯,是本來就在那裡、直到這次才第一次被完整讀到。其中 2 個,還是上一輪修正自己造出來的:修 A 問題的時候,順手在 B 的地方埋了一個新坑。
同一輪裡還有一個對照特別值得記:那 8 個窄範圍、拿到具體指令的同模型審查員,抓到了 5 個阻礙性問題、8 個次要問題;而同一輪跑的、廣讀全文再給摘要判斷的跨模型審查,一個都沒抓到。
這兩件事合起來,逼出一個我原本沒有的判準:「審查夠不夠獨立」至少有兩條軸,不是一條。 一條是模型家族——同一家還是換一家;另一條是範圍與指令的具體度——廣讀全文後給摘要判斷,還是窄範圍、拿著寫死的具體檢查點去對。前面那套設計,清楚地變動了第一條軸的其中一半(角色分離),但從公開報導看不出它有沒有系統性地也變動第二條軸;而我自己這個小得多的案例告訴我,兩條軸各自抓到的東西不重疊——換模型抓到的,窄範圍抓不到;窄範圍抓到的,換模型的廣讀掃描也沒抓到。
五、一句對它公平的話
這裡有一件我必須老實承認、對前面那個案例不利公平的事:我這套稽核,審的是規格文件——文字、邏輯一致性、有沒有互相矛盾——沒有一次真的起資料庫、跑遷移、貼一行程式碼進去執行。 那次改寫的驗收,是讓改寫後的程式碼跑過一套本來就存在、真實可執行、覆蓋六萬多個案例的自動化測試——這是一個客觀的、會真的失敗給你看的裁判,比我這種「兩個 AI 互相讀對方寫的字」紮實得多。
所以如果你問「我這套發現,對那個規模的專案還適用嗎」,誠實的答案是分兩半:適用的部分,是「同模型重複審查會收斂到共享盲點」這個機制本身——這跟審的是規格還是程式碼無關;不確定的部分,是「這個盲點有多容易被自動化測試攔下來」——它有一個我沒有的客觀裁判,這個裁判至少能攔住「行為跟以前不一樣」這一類錯,但攔不住「兩邊都覺得這樣寫是對的,而且測試剛好也沒測到這裡」這一類——而這正是我那 8 條裡好幾條的形狀。順帶一提,那篇報導自己的結尾就留了這個口子:19 項迴歸問題雖然都修復了,但仍有約 4% 程式碼用 unsafe 銜接既有 C/C++ 函式庫,記憶體操作無法完全交給編譯器檢查,需要人工確認安全性。連他們自己都沒有宣稱「測試全過」等於「沒有盲點」。
六、代價
我不打算給你一份沒有帳單的主張。每一條都要付錢。
| 主張 | 它會怎麼壞 |
|---|---|
| 換模型審查抓到同模型抓不到的東西 | n=1,而且那次換模型的報告本身就有一條 overclaim;跨模型不是先知,它一樣會把「沒找到」講成「不存在」 |
| 窄範圍具體指令比廣讀摘要更會抓局部缺陷 | 那次窄範圍的檢查指令,是我自己一條條手寫的,寫之前已經知道要往哪裡查;把「我給的指令夠具體」跟「窄範圍這個做法本身有效」混在一起,是這整條推論最脆弱的一環 |
| 兩條軸(模型家族/範圍具體度)各自抓不同東西 | 只有兩個資料點:一次抓到、一次沒抓到,還沒有「兩軸同時換」的實測,不知道會不會有第三種盲點在等著 |
| 8 輪同模型審查會收斂到共享盲點 | 8 這個數字,是這個專案剛好跑了 8 輪,不是「8 輪是臨界值」;第 9 輪剛好抓到也不是不可能,我沒有排除這個可能 |
| 這件事在大規模平行(像前面那個案例那樣的規模)一樣會發生 | 我完全沒有實測過大規模版本——那個案例有自動化測試當客觀裁判,我沒有;這條類比能不能從「文件審查」跨到「程式碼審查」,是整篇最大的未知數 |
七、立即可做的 3 件事
每一件都要在今天做完,而且必須產出一個讀數。沒有讀數的不算。
-
去找你上一次讓 AI 審查 AI 自己產出的地方,問一句:審查的那個,跟寫的那個,是不是同一家模型? 讀數=是或不是,一句話。是,代表你現在的把關防得住「自己蓋章」,但沒有測過「集體盲點」這一關。
-
挑一份你已經判定「沒問題」的東西,換一個不同家族的模型,只讓它列異議、不給它打分數,拿去對照原始內容逐條核對。 讀數=它列出幾條、你回頭核對後有幾條屬實、有幾條是它自己 overclaim。
-
回頭看你最近一次「修正」的動作——不是修之前那個問題,是修完之後有沒有人重新掃過「這次修正本身有沒有製造新問題」。 讀數=有沒有做這一步,有/沒有,就這麼直接。
長期培養的習慣
-
把「誰審查」跟「用什麼範圍/指令審查」,當成兩個獨立要調的旋鈕,不要只調一個就以為換手了。 換模型不等於換範圍,換範圍也不等於換模型,兩者各自需要驗證。
-
每一次「這次真的過了」的結論,附上「這是第幾輪、用了哪把尺」。 因為下一輪換尺,結論很可能會變——「乾淨」從來不是一個絕對狀態,是那一把尺量出來的相對結果。
查證與證據等級
本文的主張分三類,這裡分開交代。
外部事實(可公開查核)
- Jarred Sumner〈Bun in Rust〉——原廠一手 blog,本文已核對「53 萬多行程式碼」「每月下載量超過 2,200 萬次」「約 16.5 萬美元」三則數字與一手原文相符,並確認「19 項迴歸問題」一手原文為「均已修復」,二手報導的轉述方向相反
- iThome〈Bun創辦人用AI重寫53萬行程式,11天就搞定〉——本文轉述所本的二手報導;效能細項、記憶體用量、測試規模、commit 與平行 Claude 數這幾組數字,屬廠商自陳且未見獨立第三方複測,我沒有管道逐條核對,讀者宜視為「廠商自陳」而非「外部驗證」
論證(不靠新讀數,靠推理鏈)
第二節到第四節的主體是一條推論鏈:同模型重複審查會收斂到共享盲點 → 換模型能抓到一部分、但不是全部 → 換審查範圍與指令具體度能抓到另一部分、且跟前者不重疊 → 因此「獨立性」至少是兩條軸而非一條。這條鏈可以被純邏輯推翻,不需要任何新數據。 最脆弱的一環是「窄範圍具體指令」這一項——那批檢查指令是我自己手寫的,已經知道方向,把「我的指令夠具體」跟「窄範圍這個做法本身有效」分不清楚,是整篇論證裡最容易站不住的地方(我在代價表第二列自己標了這一點)。
自證主張(n=1,不可複現)
第二節到第四節提到的分數、輪次與缺陷計數,全部來自我自己一個真實專案,由我自己判讀,未經第三方驗證、無對照組。這套稽核審的是文字規格,不是可執行程式碼,跟前面那個案例有既有自動化測試當客觀裁判的處境不同——第五節已就地標明這個差距。
明示不主張
本文不主張 Bun 這次重寫存在任何實際缺陷;不主張「對抗式審查」或「跨模型審查」是我提出的概念;不主張 8 輪是任何意義上的臨界值;不主張本文發現的「兩條獨立性軸」適用於有既有自動化測試把關的程式碼審查場景——這條類比是否成立,本文誠實承認不知道。
如果這篇讓你想去查一次「上次審查我東西的那個 AI,跟寫的那個,是不是同一家」,歡迎訂閱我的 Newsletter。每週一篇,我會把自己系統裡量到的東西——包含那些量出來很難看的——寫給你看。
特色圖片:Photo by SHVETS production on Pexels