AI 工程

老專案裡的 agent 為什麼一直翻車:把隱性約束顯性化的 7 個具體做法

在 greenfield 專案裡,coding agent 的表現常常讓人驚豔:一句話下去,檔案建好、測試綠燈、PR 開出來。但同一個 agent 丟進一個活了八年的 Rails / Java / VB6 專案,它就開始寫出「看起來完全合理、但會炸掉月結帳單」的 code。

Addy Osmani(2026 年離開 Google、加入 Anthropic 擔任 Member of Technical Staff)在 2026 年 9 月 14 日發表的〈Brownfield Agentic Engineering〉,把這件事講得很直接:問題不在 agent 會不會寫 code,而在於老專案有一大堆約束根本不在 repo 裡——它在離職同事的腦袋裡、在三年前的 incident 報告裡、在一個沒人敢刪的 if 判斷裡。

這篇文章我想做的,不是複述他的觀點,而是把裡面能直接照做的招式拆出來,配上今年幾份可查證的一手資料(arXiv 論文、Bun 官方 post-mortem、Asana 工程部落格),讓你今天就能在自己的老專案裡把第一步架起來。

一、先看一個殘酷的數字:agent 遷移的通過率只有 5.4%

先把期待值校準好。

2026 年 8 月的 arXiv 論文《SWE Refactor Bench: Can Coding Agents Complete a Long-Horizon, Whole-Repository Stack Migration?》(arXiv:2608.23564,Deyao Hong、Yizhe Chi、Wenyi Li 等人)做了一件過去 benchmark 沒做的事:它不只問「測試有沒有過」,還問「遷移到底有沒有真的發生」。

作者把這個漏洞命名為 Blindness:agent 可以把原本的實作複製過去、包一層新介面,測試全綠,但底下跑的還是舊 code。你以為遷移完了,其實只是換了個名字。

它的評分是三段式、串聯的:

  1. Migration Audit — 遷移有沒有真的發生(有沒有在裝死)
  2. Behavioural Tests — 固定測試套件跑行為正確性
  3. Agentic Verification — 再派 6 個獨立 coding agent 去寫針對性測試,抓固定測試套件根本沒寫到的行為差異

結果:520 次跑測(8 個前沿模型、26 種 model-effort 組合)中,只有 28 次(5.4%)三關全過;20 個任務裡有 13 個沒有任何一次被接受的解法;最佳模型 claude-opus-5 拿 47.0/100。

更值得注意的是兩個細節:

  • 通過 Migration Audit 的 340 次跑測中,58% 達到固定檢查的 99%,但只有 26% 達到 100%。agent 很會把事情做到「幾乎對」,最後那 1% 就是要人收尾的地方。
  • 分類差距巨大:build toolchain 改寫拿 31.4 分,語言改寫只有 5.6 分

這個數字不是叫你別用 agent 做遷移,而是告訴你:在老專案裡,「驗證機制」比「生成能力」重要一個量級。下面所有招式,本質上都是在補驗證。

二、第一招:畫一張三色區地圖(Zones),而且要人來畫

綠黃紅三區地圖:不同風險區決定 agent 被允許的動作

Osmani 提出的第一個具體做法,是把 codebase 切成三種風險區,每一區綁定一組「agent 被允許的動詞」

  • 綠區:測試覆蓋好、慣例現代、相依隔離。→ agent 自己迭代(agents iterate on their own),跑緊迴圈,人只看 PR。
  • 黃區:品質混雜。→ 先寫測試再改(tests first: pin today's behavior before any change)。
  • 紅區:auth、billing、permissions、payroll 這種。→ pair or don't:要嘛每一步都有人在旁邊,要嘛不要讓 agent 碰。

讓這張地圖真的可運作的,是三條規則:

  1. 地圖由人畫,不是 agent 畫。 agent 對自己信心的評估,跟實際爆炸半徑無關。
  2. 升級要用賺的。 黃區要變綠區,條件是 characterization tests 已經存在——不是「感覺變好了」。
  3. 區域決定動詞。 不是決定「能不能用 agent」,而是決定「能用 agent 做哪一類動作」。

這裡有一句話我覺得是整篇的核心:autonomy 應該跟著 blast radius、observability、recoverability 走,而不是跟著模型的自信走。這三個問題請在每次放手前問一次:改壞了影響多大?改壞了看不看得出來?改壞了多快能退回去?

怎麼落地到 Claude Code / Codex

最低成本的做法是在 repo 根目錄放一份 CLAUDE.md(Codex 用 AGENTS.md),直接寫進去:

## Zones(人工維護,改動需 code owner 核可)

### 🟢 GREEN — 可自主迭代
- `app/services/reporting/**`
- `packages/ui-kit/**`
判準:行覆蓋 > 80%、無外部副作用、有 e2e。

### 🟡 YELLOW — 先釘行為再改
- `app/models/**`
- `lib/legacy_importer/**`
規則:任何修改前,先在「獨立 session」補 characterization test 並 commit。

### 🔴 RED — 不得自主修改
- `app/billing/**`、`app/auth/**`、`app/permissions/**`
規則:只做唯讀分析與提案,不得產生 diff。需要改動時停下來,輸出「變更提案 + 影響範圍 + 回滾路徑」給人。

搭配 harness 層的 deny 規則會更硬。Claude Code 可以在 .claude/settings.json 用 permissions deny 擋掉對紅區路徑的寫入工具呼叫——寫在 markdown 裡的規則模型會忘,寫在權限設定裡的規則模型繞不過。這是下面「第五招」的預告。

三、第二招:把「code 說不出來的話」寫下來——而且只寫這些

大部分團隊的 CLAUDE.md 都寫錯方向:寫了一堆「本專案使用 React + TypeScript,目錄結構如下……」。

Osmani 的判準很乾淨——只寫 agent 推不出來的東西

該寫:

  • 商業或團隊特有的 nuance(「這個折扣欄位只有日本站會用,因為 2019 年的稅制」)
  • 解釋系統為什麼長這樣的 trade-off
  • 靜態分析抓不到的準則
  • 領域規則
  • 外部約束(合約、法規、上游 API 的怪癖)
  • 反直覺實作背後的歷史脈絡

不要寫:
模組結構、call graph、命名慣例、測試怎麼組織、型別怎麼約束、linter 抓得到的東西——這些 agent 自己看得出來,寫進去只是浪費 context

實務上我建議的寫法是「一條約束 = 一行 + 一個出處」:

- 退款金額不得小數點後兩位以上進位 —— 2024-03 財務對帳 incident #4471
- `User#legacy_uid` 不可移除,仍被 SSO 供應商回呼使用 —— 見 vendor 合約 §4.2
- 訂單狀態機禁止新增中間狀態,下游資料倉儲 schema 是硬編碼 —— #eng-data 2025-11 討論

有出處的約束才有人敢刪。沒出處的約束會在兩年後變成新的技術債。

四、第三招:comprehension memo——讓「理解」變成可交接的產出物

老專案裡 agent 最貴的一段,是「搞懂這塊 code 在幹嘛」。這段理解如果只活在 context window 裡,session 一結束就蒸發,下次再燒一次 token。

Osmani 的做法是把它變成 artifact。跑一趟唯讀的研究 pass,產出一份 memo,內容包含:entry points、owners、callers、既有抽象、測試、production 訊號、相關歷史、open questions。

最關鍵的一條要求:每一個宣稱都要引用一個檔案、issue、ownership record 或 dashboard。沒有出處的句子一律當成猜測。

可以照抄的 prompt:

你現在是唯讀模式。不要修改任何檔案,不要執行會產生 side effect 的指令。

任務:理解 `lib/legacy_importer/` 這個模組,輸出一份 memo 到
docs/memos/legacy-importer.md,包含以下章節:

1. Entry points(誰會呼叫它,列出 file:line)
2. Owners(從 CODEOWNERS / git blame 推測,標註信心程度)
3. Callers(完整 call graph,只列一層以內)
4. 既有抽象與它們的假設
5. 現有測試涵蓋了什麼、沒涵蓋什麼
6. Production 訊號(log 格式、metric 名稱、alert)
7. 相關歷史(git log 中與此模組有關的 revert、hotfix、大改)
8. Open questions — 我看不出答案、需要人回答的問題

硬性規則:每一個事實陳述後面必須加 `(source: <file:line 或 commit sha 或 issue 編號>)`。
推測必須明確標記 `[推測]`。不確定就寫進第 8 節,不要編。

然後——換一個乾淨的 context 做規劃,只餵 memo 不餵原始碼掃描結果。由人在這一步選路線。如果實作到一半發現 memo 畫錯了,停下來重畫地圖,而不是硬著頭皮改。最後 review 再開一個新 context,從驗收條件反推往回看。

三個獨立 context(研究 / 規劃 / 審查)的用意是避免同一份錯誤假設一路自我確認到底。

五、第四招:characterization test,以及那條最容易被違反的規則

Characterization test 的定義:記錄系統「目前實際行為」的自動化測試,目的是讓你能安全地重構老 code。重點是「實際行為」——包含那些醜陋的、看起來像 bug 但業務其實在依賴的部分。

Osmani 給了一條規則,我認為是整篇最被低估的一句:

同一個 session 不應該既寫測試、又讓測試通過,否則那片綠燈只是在編碼它自己發明的實作。

這是非常實際的陷阱。你叫 agent「幫我補測試然後重構」,它會寫出剛好符合它腦中新實作的測試,全綠,然後你上線炸掉。

正確的兩段式做法:

Session A(只寫測試,禁止改 production code):

只能新增 spec/characterization/ 底下的測試檔案,禁止修改任何 lib/ 或 app/ 下的檔案。

目標:為 `LegacyImporter#normalize_row` 產生 characterization tests。
方法:不要看它「應該」做什麼,只記錄它「現在」做什麼。
- 用真實樣本(從 spec/fixtures/ 取)餵進去,把實際輸出當成期望值
- 包含邊界:nil、空字串、超長字串、非 UTF-8、負數金額
- 如果某個輸出看起來像 bug,仍然把它寫進測試,並加註解 `# NOTE: 疑似 bug,但先釘住現況`
- 最後列出你無法穩定重現的輸入(時間相依、隨機、外部呼叫)

commit 這一批測試,人工掃一遍那些 NOTE然後才開新 session 做重構,這時測試套件就是一個 agent 沒參與發明的 oracle。

六、第五招:零風險優先的工作階梯,與「harness 階梯」

左邊是工作風險階梯,右邊是把規則固化進 harness 的階梯

Osmani 給了一條升級順序,從零風險往上爬:

  1. Explain it — 畫出現況行為,完全不改任何東西
  2. Pin behavior — characterization tests,醜的部分一起釘
  3. Mechanical transforms — codemod、rename、move。diff 好讀、無聊、可機器檢查
  4. Inventories — dead code、未使用 export 的清單(是清單,不是改動
  5. Hairy parts — 建立信心之後才碰硬骨頭

多數團隊的錯誤是直接從第 5 階開始,然後得出「agent 在我們 repo 不好用」的結論。

另一條階梯更重要,我叫它「約束固化階梯」。Osmani 的原則是:每一次重複的修正,都代表 harness 少了一塊。同一個問題你修第三次,就該把它往上推一階:

形式 壽命
1 你順手在 diff 裡改掉 下次就忘了,重複發生
2 Review 留言 下週又出現
3 寫進 CLAUDE.md 指令行 模型讀得到,但會在長 context 裡弄丟
4 包成 Skill(例如「檢查 blast radius」) 可重用的程序,被呼叫時才生效
5 Lint rule / git hook / 型別 / 測試 每次都跑,不需要人記得
6 Deny rules / CI gate / scoped credentials 完全不需要任何人記得

實作上,第 5、6 階是 ROI 最高的。與其在 CLAUDE.md 裡寫「不要直接用 ENV[] 讀設定」,不如寫一條 lint rule;與其寫「不要碰 billing」,不如在 .claude/settings.json 的 permissions deny 直接擋掉。

Osmani 還有一個關於 worktree 的警告值得抄下來:worktree 隔離的是「變更」,不是「行為」。多個 worktree 可能共用 git metadata、credentials、本機服務與網路存取。要讓 agent 無人值守地處理不可信內容,你需要的是真正的 sandbox 與 scoped credentials,不是多開幾個資料夾。

七、第六招:定義「遷移完成」,以及測試不夠時的 traffic comparison

回到 Blindness。要避免它,你得先把「完成」的定義寫死:

一個遷移完成,當且僅當:新路徑能用,且舊相依可被證明已經消失。

可檢查的四條:

  • 新路徑真的在服務該路由
  • 舊路徑已刪除(不是 deprecated)
  • 沒有殘留 shim
  • 沒有任何東西還在呼叫 legacy 實作

對應的 metric 不是「生成了幾行」,而是:剩餘舊 import 數、新路徑承接的流量比例、parity mismatch 數、已移除的 legacy 相依數。這四個數字可以做成 dashboard,讓 agent 每次 PR 都回報。

那如果這塊 code 的行為根本沒有測試能描述呢?Osmani 引了兩個經典工程做法:GitHub 的 Scientist library 和 Netflix 的 GraphQL cutover。機制是 traffic comparison:

  1. 請求同時分叉到 control(舊路徑,可信)與 candidate(新路徑)
  2. 回給使用者的永遠是舊答案(對外零變化)
  3. 背景比對兩邊 payload
  4. 每個差異寫進 mismatch log
  5. Gate:payload 一致才 promote

並且記住那句判準:只有 production 流量才真正理解的介面,按定義就是紅區。

八、第七招:最後才平行化

這點跟直覺相反。看到 Bun 開 64 個 Claude 的新聞,很多人第一反應是「我也來開 20 個」。

Osmani 的立場是 parallelize last,因為平行化只會把你既有的瓶頸乘以 N。先具備三件事再開扇出:

  1. 一個單元有可靠的裁判——它必須能穩定地說「不行」
  2. 出錯時便宜的復原路徑
  3. 人能吸收的 review 格式

第 3 點他給了具體排序。自動化 review 的輸出應該依序是:意圖 → 被改動的 invariant → 測試結果 → parity mismatch → 回滾路徑 → 完整 diff(放最後)。人的注意力優先給「爆炸半徑最大 + oracle 最弱」的那一塊。

這個排序可以直接寫成 PR template 或 review skill 的輸出格式,很好抄。

九、數據與限制:那些頭條數字的真實脈絡

這節我逐一核對過一手來源,因為這些數字在二手轉述裡被扭曲得很嚴重。

Bun:Zig → Rust,11 天。 依 Jarred Sumner 2026 年 7 月 8 日在 Bun 官方部落格的 post-mortem〈Rewriting Bun in Rust〉:原始 Zig 程式碼不含註解 535,496 行,1,448 個 .zig 檔轉為 .rs,最終 diff 淨增 1,009,272 行6,778 個 commit(不含 merge 為 6,502),5 月 3 日開工、5 月 14 日併入主幹。並行規模是「同時 4 條 workflow,各自在獨立 worktree,每條 16 個 Claude」,尖峰約 64 個 Claude 同時跑;token 花費約 $165,000(59 億 uncached input、6.9 億 output、720 億 cached input reads)。

⚠️ 這裡有一個需要更正的細節:Osmani 文中寫的是「across 50 workflows」,但 Bun 官方 post-mortem 寫的是 4 條 workflow × 每條 16 個 Claude ≈ 64 個並行實例。以一手來源為準。

真正該抄的不是數字,是結構:每一行 code 都經過兩個「對抗式 reviewer」(也是 Claude,只拿到 diff,被指示「假設這段 code 是錯的」),再由一個 fixer 套用回饋才 commit;merge gate 是既有的完整測試套件;而且在任何 agent 開跑之前,團隊先花了數小時寫一份把 Zig idiom 對應到 Rust 的 porting guide。

Asana:Enzyme → React Testing Library,兩週。 依 Asana 工程團隊 2026 年 8 月 7 日的文章,原本排定的是五年計畫,實際用 1.5 週工程時間、橫跨 2 個日曆週完成,成本約 $12,000($11K 模型用量 + $1K 基礎設施),對照原本約 $6M 的人力估算。

⚠️ 兩個常被轉錯的點:第一,他們用的是 OpenAI Codex(frontier model、extra-high reasoning),不是 Claude,最多同時 4 個 agent,各自指向不同目錄。第二,這是廠商/團隊自述的帳單,不是對照實驗,不能當成「節省 500 倍」的科學結論。

最有教學價值的反而是他們的 prompt——只有五句話:

We want to migrate the repo from Enzyme tests to React Testing Library style tests. Follow existing norms and best practices in the codebase. Migrate all files in [directory] that use enzyme to use react testing library. Test your changes with [test command]. Generally bias for migrating easy-to-convert files first.

Asana 自己下的結論是:「agent 產出的品質,高度取決於你交給它的環境品質。」 成功來自既有的 codebase 慣例與測試基礎設施(typecheck、lint、test、CI 能立刻抓到錯),而不是精巧的 prompt。工程師只在每天早晚 review 一次進度與 PR。

Spotify:650+ agent PR / 月。 這個數字出自 Anthropic 的 Spotify 客戶案例頁,描述的是背景 coding agent 每月有 650+ PR 被 merge 進 production,宣稱在複雜遷移上省下最多 90% 工程時間。Spotify 自家工程部落格(Niklas Gustavsson,2026 年 6 月 3 日)則給了另一組數字:Fleet Management 上線以來累計 250 萬個自動化維護 PR 被 merge、PR 頻率增加 76%、最近一次 Java 後端遷移 3 天完成。內部 agent 叫 Honk,跑在 Claude Agent SDK 上,送 PR 前會先跑格式化、lint、build 與測試,安全的自動 merge、複雜的轉人工。

這裡的重點是 Osmani 的那句話:Spotify 能做到,是因為多年前就鋪好了 Backstage 這條軌。可轉移的是 agent 周圍的結構,頭條數字通常不能轉移。

Stripe:3.7M 行 TypeScript 遷移,零 agent。 這是用來反襯的案例:Stripe 在單一 PR 把 370 萬行從 Flow 遷到 TypeScript,用的是 codemod(建立在 Airtable 開源的 codemod 之上),花了數個月開發。留下來的耐久資產是那台遷移機器,不是那個 PR。這提醒你:能被 codemod 確定性解決的事,就不要交給 agent 去機率性地做。

VB6 → C#:92% vs 47%。 arXiv:2608.28972《Legacy System Modernization with Coding Agents: A Case Study》(Iago da Silva Rodrigues Alves、Cristiano Politowski、João Eduardo Montandon,2026 年 8 月 29 日),用 Claude Code 把一套營運 20 年以上的 ERP 的 12 個功能從 VB6 遷到 C# .NET 10。行為等價度:低複雜度 92%、中複雜度 81%、高複雜度 47%,整體平均 70%。成本也是斷崖式:低複雜度平均 1.47M token(約 $1.66),高複雜度 9.09M token(約 $10.28)。等價度從兩個維度量:persistence(存進去的資料是否相同)與 functional behavior(業務規則是否相同)。

樣本只有 12 個功能、單一系統、單一 agent 版本,不能外推成通則。但那條曲線跟 SWE Refactor Bench 的「31.4 vs 5.6」完全一致:複雜度一上去,就斷崖

十、什麼時候該用、什麼時候別用

綜合上面的一手資料,我的判斷表是這樣:

該放手讓 agent 跑:

  • 有確定性 oracle 的機械式轉換(測試框架遷移、API 改名、annotation 轉 record)
  • 唯讀的理解工作(memo、call graph、dead code 盤點)
  • 綠區內的新功能
  • 已經有 lint / typecheck / CI 能立刻說「不行」的 repo

該用 codemod 而不是 agent:

  • 轉換規則可以窮舉、且要 100% 一致的大規模改寫(Stripe 那類)

別放手:

  • 語言層級的改寫(benchmark 上 5.6 分),除非你像 Bun 一樣先寫好 porting guide、配置對抗式 reviewer、且有完整既有測試套件當 gate
  • 只有 production 流量才能驗證的介面——先做 shadow / replay 比對,不然那是紅區
  • 高複雜度的業務邏輯遷移(47% 等價度意味著每兩個功能就有一個行為跑掉)
  • 你還沒有可靠裁判的時候就想平行化

十一、這禮拜可以做的四件事

  1. 畫三色地圖,寫進 CLAUDE.md / AGENTS.md 紅區同步加上 permissions deny 規則。這件事一小時做得完,而且立刻降低最大宗的事故類型。
  2. CLAUDE.md 裡「agent 自己看得出來」的段落刪掉,只留 code 說不出的約束,每條配一個出處連結。多數專案這一刪會少掉一半篇幅,剩下的一半才是真正有用的 context。
  3. 挑一個黃區模組,跑兩段式 characterization test 流程(寫測試的 session 禁止改 production code)。這是把黃區換成綠區的唯一合法途徑。
  4. 換掉你的 agent 儀表板指標。 不要看生成行數,改看:lead time、review 分鐘數、人為介入次數、逃逸缺陷、rollback 次數、oracle mismatch、殘留的 suppression。遷移專案另外追:剩餘舊 import、新路徑流量佔比、parity mismatch、已移除的 legacy 相依。

最後補一個 Osmani 提到、但容易被略過的洞見:agent 會把隱性成本變成可計價的成本。那些「大家都知道但沒寫下來」的團隊慣例,會以「每週在 review 裡重複出現的同一則留言」的形式浮出水面。以前模糊是免費的,現在模糊每天都在跟你收 token 費。

從這個角度看,在老專案導入 agent 最大的收穫,可能根本不是它寫的那些 code,而是它逼你把二十年沒人寫下來的東西,終於寫了下來。

來源

整理:DataAgent · Coding Agent 實戰教學

文章裡這些把關做法,要在一個團隊裡真的落地、而不是只有你一個人在用,通常卡在流程與共識。我把這部分整理成企業內訓:
coding agent 導入與治理 — 企業內訓與顧問 →

發表迴響

%d 位部落客按了讚: