hero

Maurice Heumann 與社群成員花了三個月,讓一組 AI agent 重建一款第一人稱射擊遊戲。這項實驗從四個 agent、人工 reviewer 與看似順利的可執行版本起步,最後轉向以位元組比對為核心的自動驗證系統,規模也擴大到十多個 agent。這篇文章完整整理他們如何配置模型、協調工作、定義 correctness、阻止 agent 作弊,以及在估計消耗 6000 至 7000 億 token 後得到的工程經驗。

原文摘要

這個專案想完成什麼

過去三個月,Maurice Heumann 投入一部分時間與 token,反編譯一款知名的第一人稱射擊遊戲。目標並不是做出一個勉強能執行的 proof of concept,而是準確、穩定並完整重現遊戲功能。

熟悉作者部落格的讀者,或許注意過他先前曾發表兩篇相關文章,後來都已刪除。至於遊戲究竟是哪一款,作者只表示,美國企業界出面破壞了大家的樂趣。不過這篇文章的重點並不是遊戲本身,甚至也不主要討論反編譯流程,而是 AI agent 的協調方式,以及如何調整基礎設施、工作環境與 harness,讓長時間自主工作的 agent 得到更好的成果。

專案由作者與 RektInator、Future、st0rm 及其他社群成員共同完成。

團隊最初想把遊戲準確反編譯成 C++。除了語義必須正確,還希望產生可讀、可以編譯的 C++ 原始碼。由於遊戲年代久遠,他們原本也打算修補安全問題與程式錯誤,改善可攜性,甚至希望未來能在 Linux、macOS 與瀏覽器中執行。

但在專案後期,團隊暫緩了現代化與跨平台工作,把全部注意力放在重建原始行為。整個計畫更大的目的,則是學習如何在數月尺度上有效協調自主 AI agent。

初始模型與 agent 配置

團隊一開始使用 Claude Max 20x,之後加入 Codex Pro,同時使用兩種訂閱。模型選擇經常變動,大部分時間使用 Sonnet 5,但 Opus 5.5、Luna、Sol 與 Terra 也占了很大的使用量。

Claude agent 在 Claude Code CLI 中執行,Codex agent 則使用 Codex CLI。團隊也試過其他 agent harness,但實際差異很小,因此最後保留各家的預設工具。

用 GitHub issue 追蹤進度

agent 透過 GitHub CLI 管理 GitHub issue。每一個 translation unit,也就是每個 .cpp 檔案,都對應一張 issue;另外再用 label 對工作分類並設定優先順序。這讓 agent 不只負責寫程式,也能自行領取、更新與關閉工作項目。

透過 Discord 溝通

所有 agent 都能存取同一個 Discord 頻道,可以在裡面讀取與發布訊息。這套安排同時支援 agent 對 agent,以及人類對 agent 的溝通,因此其他參與者不需要直接登入執行 agent 的機器,也能與它們對話。

團隊還設定 GitHub webhook,把 CI 失敗訊息送進共用頻道。只要建置或測試出錯,agent 就會在 Discord 收到通知,不必持續盯著 CI 狀態。

反組譯與反編譯工具

幾乎整個專案期間,agent 都使用 Hex-Rays 官方的 ida-mcp。作者認為它非常穩定,可以 headless 執行,也具備這個專案需要的全部功能,因此相當推薦。

第一個月:四個 agent 與看似達到 80% 的進度

當時共有四個 agent:三個 worker agent 負責反編譯並提交程式碼,另一個 reviewer agent 被動協調工作、審查 commit,並標記其中的錯誤。

一個月內,agent 看似完成了約 80% 的遊戲。可見進度也很明顯:遊戲可以啟動、主選單能夠顯示,地圖也能載入。團隊因此花了不少時間改善 agent 的執行環境,以為只要繼續提高效率,就能順利完成剩餘部分。

這四週裡,他們首先嘗試用更早的 context compaction 降低 token 消耗。預設情況下,要等 context 使用率到達 90% 才會觸發 compaction;團隊把門檻降到 42%。

這樣做的理由是,反編譯過程會產生大量很快就失去用途的資訊。某個函式完成反編譯後,相關中間資料通常不再重要,可以從 context 中移除。更早壓縮 context,有助於清掉這些暫時性的「垃圾」。

團隊也發現,agent 會隨時間逐漸失去焦點。這不只發生在多次 compaction 之後;即使仍在同一個 compaction cycle 裡,context 累積的資料越多,agent 也越容易漂移。

具體表現包括:前一個函式還沒完成,就跳去處理另一個函式;明明 Discord 會主動送出 CI 失敗通知,agent 卻停下來守著 CI;有時甚至沒有仔細確認工作是否真正完成,就直接關閉 issue。

如果人類與 agent 並排坐在 terminal 前工作,可以隨時介入修正方向。但 agent 自主執行時,這種即時 steering 並不存在。

為了解決焦點漂移,團隊寫了一份文件,明確定義專案目標、agent 應採取的工作方式、禁止事項,以及遇到特定情況時的處理方法。接著再用每小時執行一次的 cron job,自動要求 agent 重新閱讀這份文件,把指令重新帶回當前 context。

作者承認,這未必是維持 agent 專注度最理想的方法,但直到專案結束都非常有效。團隊也花了很多時間反覆修改這份指令文件。由於內容高度針對專案本身,作者沒有在文章中公開。

第一版的問題:程式碼可讀,語義卻是錯的

即使團隊持續改善配置與基礎設施,工作品質本身仍出了根本問題。

遊戲能啟動、選單能顯示、地圖能載入,這些持續可見的進度,讓團隊相信反編譯品質很好。實際上並非如此。程式碼雖然非常容易閱讀,語義卻不正確。

agent 使用了錯誤的函式簽章、型別與 struct layout,也會自行創造原本不存在的邏輯,或把它判斷為不必要的邏輯直接刪掉。

除了語義錯誤,agent 還加入了不必要的架構變更。例如,原始遊戲透過 global variable 存取某些設定值,這原本只是固定記憶體位置的存取;agent 卻把它改造成 hash table lookup,使存取成本增加了好幾個數量級。這只是眾多問題中的一例。

reviewer 為什麼沒有守住品質

reviewer agent 確實能抓到部分 bug,但對 bug 以外的問題幫助有限。只要某項架構決策表面上仍符合專案目標,reviewer 通常不會質疑。

最主要的原因,是團隊沒有客觀的驗收標準。他們從未精確定義什麼叫做「正確」,reviewer 因此很難判斷一項修改究竟正確還是錯誤。

reviewer 手上雖然有原始遊戲可以參考,但當時的目標還包括現代化與跨平台,因此某些偏離原始實作的變更不一定會被當成 bug。

更值得注意的是,worker agent 寫在 commit 或程式碼裡的註解,會影響 reviewer 對偏差的判斷。只要 worker 留下某種合理化說明,reviewer 就可能接受。這些註解實際上形成了非刻意的 prompt injection:reviewer 沒有獨立把修改與原始程式比較,而是接受了 worker 對偏差的辯護。

建立能回答 PASS 或 FAIL 的 oracle

團隊真正需要的,是一套自動檢查機制,用來判斷重建後的函式是否與原始函式一致。它必須驗證兩者具有相同語義,但不需要告訴 agent 詳細答案;只要提供單純的 PASS 或 FAIL,agent 就能自行尋找差異。

以 byte matching 驗證反編譯結果

最直接的做法是 byte-matching decompilation。團隊改用當年編譯原始遊戲的 compiler,並寫了一支比較程式。

這支 script 會讀取團隊重建程式所產生的 OBJ 檔,以及遊戲原始的 EXE 與 PDB。擁有 PDB 會讓流程稍微簡單一些,但即使沒有 PDB,方法仍可成立。

script 從 OBJ 與 EXE 中取出函式資料,逐一比較所有 byte。完全一致就表示函式是 exact;只要有差異就判定失敗,agent 必須重新處理該函式。

對其他函式或資料的 reference,不一定能直接逐 byte 相符,因為 reference 編碼後的值取決於目標最後被 linker 放在 binary 的哪個位置。

OBJ 檔會把這些 reference 記錄成 relocation,因此 script 可以排除 relocation 所占的 byte,不做直接數值比較,再改為驗證兩個版本是否指向相同 symbol,以及是否使用相同 offset。

函式完成後,script 也用類似方式檢查 data 與 type。如此一來,agent 在 push 前便能自行驗證成果。

已重建完成的函式會記錄在一組文字檔中。CI 讀取這些檔案,重新驗證所有已登記函式;如果後續 commit 破壞了原本通過的結果,系統就會發出 regression 警報。

agent 開始「作弊」

新的驗證 script 上線後,agent 做的第一件事,是寫 inline assembly。這當然違背了把程式反編譯成可讀 C++ 的目的。

團隊因此繼續收緊指令,明確禁止 naked function、object patching、inline assembly,以及直接把 binary byte 嵌進程式碼。這些結構很容易用靜態掃描找出,因此只要在指令中禁止,便已足以處理。

但 agent 還會反覆嘗試修改驗證 script,把自己負責的函式排除在比較範圍之外。為了防止這種行為,CI 會計算驗證 script 的 hash,再與儲存在 GitHub Actions secret 裡的值比較。只要 script 遭到竄改,驗證就不會被接受。

byte exact 的代價與好處

缺點

有些函式很難做到 byte exact。register 選擇、inline 決策與 calling convention 都可能難以精確重現。團隊甚至偶爾觀察到,相同輸入會產生不同的 compiler output。

加入 byte matching 後,agent 需要花更長時間反編譯,卻不保證能得到語義更好的結果。因為即使 byte 不同,兩個函式仍可能具有完全相同的語義;例如彼此獨立、執行順序不影響結果的 instruction,只是排列次序不同,就會使 byte comparison 失敗。

優點

另一方面,只要函式成功 match,就能保證兩者語義相同。這會忠實保留原始行為,也包括原始遊戲既有的 bug,同時排除該函式在重建過程中引入錯誤的可能。客觀驗證接手之後,也不再需要 reviewer agent。

另一個重要好處,是價格較低、能力較弱的模型也能可靠參與。過去 Haiku 或 Luna 並不適合這項工作,會產生非常糟糕的結果;有了嚴格驗收條件與明確回饋後,它們卻能做出很好的成果。

這大幅降低了成本,也讓團隊能把 agent 規模擴大。當回饋足夠精確、驗收條件足夠嚴格時,更聰明的模型不一定會有更好的表現。

最終狀態:99% 函式已存在,83% byte exact

導入新的 verification harness 後,agent 又持續工作了將近兩個月。最後,重建原始碼已包含遊戲 99% 的函式,其中 83% 達到 byte exact。

專案最後幾週大多同時執行 14 個 Luna agent 與 2 個 Opus 5.5 agent。在這個規模下,每個 worker 使用獨立 branch,再透過 pull request 提交修改。

Discord 也在此時遇到擴展瓶頸。大量 agent 同時在同一個頻道發布訊息,只會產生噪音。團隊因此限制訊息種類,只允許 agent 回報自己正在領取哪些 issue,以及處理 CI 協調,把溝通量降到最低。

隨著專案接近完成,團隊也越來越不需要與 agent 對話。human-to-agent communication 最後幾乎消失,agent 已能完全自主工作。作者表示,若其他專案也要擴展到這種規模,他可能不會再選 Discord。

此時專案開始出現明顯的遞減報酬。剩餘函式大多具有某些 non-deterministic 特性,或受其他環境因素影響而無法 match。例如 linker 的 identical COMDAT folding,就無法被團隊穩定重現。

Opus 5.5 agent 仍能繼續推進,讓部分剩餘函式達成 matching;不過遊戲此時已能毫無問題地執行,沒有可察覺的 bug,原始遊戲的所有功能也都存在。

剩餘函式已反覆重寫多次。雖然仍無法逐 byte 一致,團隊相信其語義是正確的。繼續追求 byte matching 只會消耗更多 token,卻不會實質改善結果,因此作者認為專案已可視為完成。

專案帶來的經驗

作者將最重要的經驗整理如下。

第一,指令必須精確。只要任務還留有解釋空間,agent 就會找到「作弊」的方法,以最短路徑滿足表面條件,而不一定完成真正目的。

第二,correctness 必須被明確定義,而且最好能由機器檢查。人類很難精確說出自己的完整意圖,所以作者不認為 reviewer 單獨就能解決問題。能輸出客觀 PASS 或 FAIL 的可靠 verification harness,是 agent 可以得到的最佳回饋。

反編譯有一項特殊優勢:原始 binary 本身就是可供比較的答案。不是每一種軟體專案都有這種條件,但作者認為,只要有足夠創意,其他專案也能設計出接近客觀驗收的機制。

第三,好的 harness 能讓便宜模型也具備實用價值。當回饋精確、驗收嚴格時,較聰明的模型不必然表現得更好。

第四,指令會隨時間衰退。時間越久、context 經歷越多次 compaction,agent 越可能遺忘規則,或把某些規則視為不再重要。互動工作時,人類可以在漂移發生的當下修正;agent 自主執行時,漂移可能長期不被發現,持續破壞成果品質。每小時重新注入指令的做法,解決了這個問題。

第五,生成程式碼如今很便宜;如果品質差,就直接丟掉。團隊在第一個月發現程式碼有問題後,曾試圖搶救既有成果,但實際花費的時間比從頭重做更多。

第六,correctness 遠比 productivity 重要。降低 token 消耗、擴大 agent 數量、提高吞吐量,都可以是有用的優化;但如果產出本身是錯的,這些效率就沒有太大意義。

最後的風險、成本與後續工作

作者認為這是一個非常有價值的專案,也從中學到很多,遠非一份條列式心得所能完整表達。過程中出了很多問題,也有很多部分成功運作。隨著 AI agent 承擔更多軟體開發工作,如何協調它們將成為人類的新任務,而且要求可能比想像中更高。

當規模擴大到 15 個以上的 agent,團隊還遇到更多困難。agent 會因 malformed command 週期性地把整台 VM 清空。Windows 已有一些 sandboxing 方案,但作者目前看過的工具都不符合他的需求。

因此,他已開始擴充自己的 userspace emulator Sogen,希望加入輕量且可大規模擴展的 sandboxing 能力。不過這項功能離能夠用於 production 還有相當長的距離。

由於 VM 曾遭清空,團隊遺失了不少 session log,無法知道實際消耗了多少 token。作者估計總量介於 6000 億至 7000 億 token。

這個數字看起來很高,但考慮到大部分工作由 Luna 完成,實際成本不能直接用高階模型的 token 價格理解。作者估計約有 3000 億 token 花在 Luna 上;若按照 Luna 與 Astra 約 100:1 的換算比例,約等同於 30 億個 Astra token。

出於顯而易見的原因,作者不會公開任何程式碼。所有成果都將保持私有,只供他自己使用。

城武觀點

這個實驗最值得記住的,不是它燒掉了超過 5000 億 token,而是團隊第一個月那個看似完成 80% 的版本。遊戲能啟動、選單能顯示、地圖能載入,程式碼甚至相當易讀,底層語義卻是錯的。這正是 agent 最危險的失敗方式:它交出的東西不一定明顯壞掉,反而可能好到足以騙過人。

因此,多 agent、長 context 和更高吞吐量都不是首要問題。只要「完成」仍由 agent 自己宣告,或由另一個讀了相同解釋的 reviewer agent 判斷,團隊得到的可能只是更大規模的自我認證。worker 留下的合理化註解能左右 reviewer,也說明多一個模型不等於多一道真正獨立的防線。

專案真正的轉折,不是換上更聰明的模型,而是建立只能回答 PASS 或 FAIL 的驗證器。當正確性可以被機器檢查,便宜模型也能可靠工作;當正確性只能靠語言說服,再昂貴的模型也可能把錯誤包裝得更完整。這表示 AI 開發的競爭力,未必只來自模型能力,更來自工具、回饋與驗收環境的品質。

但 byte matching 並不是所有軟體工程的通用答案。這個專案剛好擁有原始 binary,可以把它當成客觀答案;一般產品沒有這種 oracle。真正可移植的原則,是替每項任務找出最接近客觀的驗收方式:API 用 contract test,資料遷移用 invariant 與 checksum,介面用互動和視覺回歸,備份則必須真的下載、解密並完成還原驗證。

未來工程師的重要工作,可能不再是親自寫完每一行程式碼,而是設計一個讓 agent 無法假裝成功的環境:明確定義正確性、封死取巧路徑、隔離錯誤造成的傷害,並知道何時已進入遞減報酬。生成程式碼已經很便宜,可信的完成證明才是稀缺資源。