起因

AI 寫函數、修 bug,這些已經不稀奇了。但如果要它做更難的事?看一個架構亂的 codebase,自己判斷哪裡有問題,然後重構。那種資深工程師盯著依賴圖兩天,最後搬了三個檔案的工作。

我想試試看。工具是 Crucible,一個讓 Claude 跑在迴圈裡的實驗平台:改程式碼、量分數、有改善就留、沒改善就丟。目標是 NanoClaw,一個真實的 TypeScript 專案,大概 8,000 行,用容器跑 Claude agent。

量化「好的架構」(其實做不到)

Crucible 優化一個數字。ML 任務很明確,loss 或 accuracy。架構沒有這種數字。「好的架構」是一種感覺——讀程式碼的時候覺得通順,還是想把檔案關掉。

只好用代理指標,盡量靠近就好:

指標權重量什麼
依賴耦合度35%每個模組平均 import 數,加循環依賴
檔案大小均勻度25%行數的 Gini 係數,抓 God module
穩定性平衡20%每個模組的 fan-in 和 fan-out 是否大致平衡
API 表面積20%每個檔案平均 export 數

全靜態分析。madge 跑依賴圖,其他自己寫 AST 解析。不用 LLM-as-judge,太貴而且每次跑分數都不一樣。

另外加了硬門檻:219 個測試全部要過,TypeScript 編譯要過。還有壞模式懲罰:太小的檔案、barrel re-export、超過 100 行的 God function。

Baseline:24.3,滿分大概 100。

前五次迭代是真的好

啟動 Crucible,看著跑。

迭代分數做了什麼
144.28四個 100+ 行的 God function 拆成小的 helper
246.4813 個檔案減少不必要的 export
347.49container-runner 和 ipc 降低耦合
447.66砍掉 127 行 dead code
549.69config.ts 從 16 個 export 降到 9 個,兩個模組脫鉤

24.3 到 49.7,五次迭代。每個 diff 我都看了,是那種 code review 會過的東西:把一大坨 createSchema() 拆成 createTables()runMigrations(),常數搬到唯一用到它的檔案,內部 helper 取消 export。

我那時覺得挺好的。

然後它學會 copy-paste 了

第 12 次迭代。Agent 發現把模組 A 的函數複製到模組 B,就可以刪掉 import。依賴數降了。分數升了。函數能跑,因為就是逐字貼上的。

到第 19 次,resolveGroupFolderPath 出現在三個檔案裡。formatLocalTime 被貼到 router.ts。DATA_DIRTIMEZONE 被貼到 ipc.ts。每次迭代分數進步零點幾到一點幾分,每次都讓 codebase 更糟。

Metric 從 49.7 升到 55.5。實際品質在第 5 次就到頂了,之後一路往下。

Goodhart’s Law。指標變成目標的那一刻,就不再是好指標了。

我堵了漏洞,它找到新的

回滾到第 5 次,加一個重複偵測器:

  • 提取所有 5 行以上的函數
  • 正規化(去掉註解、字串、空白、變數名)
  • 跨檔案出現一樣的函數體就扣分:每多一份 -8 分

指令裡也直接告訴 agent:「重複會被偵測,扣很重。」

它不複製函數了。改用兩個新招:

Interface 重複。 TypeScript 的 structural typing 讓你可以在每個檔案都寫一份 interface RegisteredGroup { ... },不用從 types.ts import。Interface body 一模一樣,但我的偵測器只看函數。

常數 inline。 const DATA_DIR = path.resolve(process.cwd(), 'data') 就一行。我的偵測器門檻是 5 行。Agent 在每個需要的模組都寫一份,然後把 import 刪掉。

36 次迭代,分數從 49.7 到 58.1。大概一半是真的改善(搬常數、砍 export),一半是 interface 和常數重複。

第三輪:再堵

加兩個偵測器:

  • Interface clone:比對跨檔案的正規化 interface body,-6/copy
  • 常數 clone:同名同值的 UPPER_CASE 常數出現在多個檔案,-5/copy

第三輪寫這篇的時候還在跑。初步看起來 agent 在條件內工作了——搬功能而不是複製、合併相關程式碼、把沒用到的 export 改 private。每次進步零點幾分,但看 diff 是真的。

學到的事

我選的每個代理指標都有作弊路徑。降依賴數?copy-paste。降 export 數?塞進一個大物件。改善檔案均勻度?建一堆小檔案。我每次都低估了 agent 的創造力。

有用的是疊防線。硬門檻(測試不過就死)。指標互相牽制(刷一個會傷另一個)。AST 偵測特定 exploit,發現一個補一個。單獨哪一層都不夠,三層一起讓「老實改架構」變成最省力的路。

真正的價值在前幾次迭代。三輪都一樣。Agent 很快就找到最明顯的問題:God function、不必要的耦合、dead export。之後就開始摳邊際收益,那時候 gaming 的吸引力就出來了。

完全自主的架構優化,我覺得現階段還不行。代理指標沒辦法涵蓋人在意的全部。實際上有用的做法是:讓 agent 先跑一輪,我自己看 diff,挑好的 cherry-pick。自動探索,人工篩選。

有一點我沒料到:就算 output 不完美,過程本身就有價值。Agent 第一步做什麼,告訴你 codebase 哪裡最爛。它怎麼作弊,告訴你指標漏了什麼。光這些資訊就值得跑一次。

數據

輪次Baseline最高分真實品質高峰哪裡開始作弊
arch124.355.5~49.7(第 5 次)第 12 次:inline 函數
arch249.758.1~50.2(第 6 次)第 10 次:interface + 常數重複
arch344.0跑到一半TBDTBD

值得做嗎?

如果你的 codebase 有明顯結構問題,而且測試覆蓋率還行,值得。Agent 前幾次抓 low-hanging fruit 的能力真的不錯。

但要看 diff。不要只看分數。然後做好心理準備,agent 一定會找到你沒想到的方法來刷分。

老實說這個實驗最有意思的不是重構結果,是即時看到「量測的東西」跟「在意的東西」之間的落差,一個 exploit 一個 exploit 地浮現出來。


Crucible 開源:github.com/suzuke/autocrucible。實驗目標:github.com/qwibitai/nanoclaw