MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

AI 寫的程式碼能 Build 不代表能用:一次 Code Review 的血淚教訓

最近我們用 AI Coding Agent 寫了一個完整的後端 + 前端功能模組。Agent 跑完之後,dotnet build 過了,pnpm build 也過了,單元測試也沒紅燈。

看起來一切完美,對吧?

Build Pass 的假象

事情是這樣的。我們有一個新功能需求:一個庫存轉換模組,包含後端 API(.NET Core)和前端介面(React)。我把需求拆成 11 個 unit,交給 AI Agent pipeline 去跑——後端 7 個、前端 4 個,依序執行。

大概跑了一個多小時,11 個 unit 有 10 個成功(1 個因為 session lock timeout 失敗,清掉重跑就好了)。最後我手動確認:

1
2
dotnet build  # ✅ Build succeeded
pnpm build # ✅ Build succeeded

好,建 PR,準備 review。

然後災難就開始了。

第一輪:命名全歪

Review 的第一件事就發現,AI 產生的 DTO 命名跟我們的 codebase 慣例完全對不上。

我們的慣例是這樣的:

  • API 層:Request 用 XxxReq、Response 用 XxxRes、查詢用 XxxSearchParams
  • BLL 層:輸入用 XxxInfo、輸出用 XxxResult、查詢用 XxxSearchInfo
  • 資料夾:Dtos/Params/(放 Req)、Dtos/ViewModel/(放 Res)

AI 寫出來的呢?XxxParamsXxxViewModelXxxResModel——每一個都「幾乎對」但「就是不對」。更慘的是,它在 API 層和 BLL 層之間的命名混在一起,改 A 的時候不小心動到 B。

這不是 AI 笨。這是 AI 沒有讀過我們內部的 convention document,它只能從 prompt 裡的描述去「推測」命名規則。推測的結果就是——看起來合理,但跟實際 codebase 不一致。

第二輪:業務邏輯的「合理但錯誤」

命名改完,開始看邏輯。AI 寫了一個 Save 方法,做了這些事:

  1. 判斷 info.Id == null 來決定新增或更新
  2. 新增和更新分成兩個 private method
  3. 用 transaction 包起來,失敗 rollback

每一項都「聽起來合理」。但我們的 codebase 慣例是:

  1. query.FirstOrDefault() 判斷資料存不存在(不看 Id)
  2. 單一 Save method,不拆分
  3. 草稿存檔不需要 transaction

這就是 AI 程式碼最危險的地方:它寫出來的東西在技術上完全正確,但不符合你的團隊慣例。如果你不熟悉自己的 codebase,你甚至不會發現有問題。

第三輪:Controller Return 的鬼打牆

這一輪讓我印象最深刻,因為我自己也搞混了。

AI 的 Controller 這樣寫:

1
2
3
4
5
6
[HttpPost]
public async Task<IActionResult> Post([FromBody] StockConvertCodeReq body)
{
var id = await _bll.Save(info);
return Ok(id); // ← 這行有問題
}

看起來沒問題?但我們的慣例是:

  • POST 新增 → StatusCode(201, id)
  • PUT 更新 → StatusCode(201)
  • DELETE → StatusCode(204)
  • GET → Ok(data)

我先改成 Created(),發現不對。改成 Ok(),還是不對。改成 StatusCode(201),差一點。最後才確認是 StatusCode(201, id) ——因為前端需要拿回 id 做導航。

這來回改了四次。四次。

第四輪:前端的過度工程

前端也沒好到哪去。AI 為了一個「掃碼 → 新增明細」的功能,寫了 500 多行的 Form component,包含:

  • materialLock state
  • qtyLock state
  • MaterialSelector modal
  • ensureMaterialInfo 驗證函式
  • useRef 做 material cache

實際上需要什麼?一個掃碼輸入框 + 一個 append 按鈕。掃完碼,填入 mini form,按 append 寫進主表單。大概 100 行。

AI 傾向於「防禦性過度設計」——它會想像各種邊界情況然後全部處理,結果是把一個簡單的功能變成一個複雜的系統。

第五輪:細節魔鬼

到了第五輪,剩下的都是小東西,但加起來很要命:

  • 編輯模式的按鈕文字用了 save 而不是 update
  • Form 嵌套問題(<Form> 裡面不能再放 <Form>
  • initialValues 應該改用 useEffect + setFieldsValue
  • 路由用 query string(/form?id=xxx)而不是 path params(/:id/edit

每一個都不難改,但 如果你信任 build pass 就直接 merge,這些問題全部會進 production

問題出在哪?

回頭看這整件事,核心問題很清楚:

AI 能理解「通用最佳實踐」,但不能理解「你的團隊慣例」。

dotnet build 檢查的是語法和型別。它不會檢查你的 DTO 叫 Req 還是 Params,不會檢查你的 Controller 回 Ok() 還是 StatusCode(201),不會檢查你的 Save method 是不是 follow 團隊的固定 pattern。

這些「慣例」存在於:

  • 團隊成員的腦中
  • 散落在各處的 convention document
  • Codebase 裡已有 module 的「既有寫法」

AI Agent 能接收 prompt,但 prompt 不可能把所有慣例都塞進去。結果就是——AI 寫出來的 code「技術正確、慣例錯誤」,而且錯得很隱蔽。

我們後來怎麼做

踩完這次坑之後,我們做了幾件事:

1. 把 convention document 原子化

原本我們有兩份大文件:backend.md(300+ 行)和 frontend.md(200+ 行)。問題是太長了,AI 不一定會把每條規則都記住。

我們拆成小檔案,每個檔案 < 200 行,開頭寫「什麼時候要讀這個檔案」:

  • bll-save.md — 寫 Save 方法時讀
  • controller.md — 寫 API endpoint 時讀
  • frontend-form.md — 寫表單時讀

這樣 AI Agent 在執行特定任務時,只需要載入相關的小檔案,而不是一次消化 500 行規範。

2. Prompt 裡加 CRITICAL WARNINGS

在每個 unit 的 prompt 裡,我們開始加上醒目的警告:

1
2
3
⚠️ CRITICAL: BLL Save 必須用單一 Save method
query.FirstOrDefault() 判斷新增/更新,
不要拆成 Insert/Update 兩個方法。

不是期待 AI 自己推論,而是直接告訴它「你最可能犯的錯」。

3. Build Pass ≠ Review Pass

這是最重要的心態調整。我們現在的 pipeline 是:

  1. AI Agent 跑完 → 確認 build pass
  2. 人工 review(至少看 DTO 命名、BLL pattern、Controller return)
  3. 修正 → 再 review
  4. 確認 merge

Build pass 只是第一關,不是最後一關。

4. 先 grep 再寫

這條規則現在寫死在我們的 agent prompt 裡:

寫任何 BLL 方法前,先 grep 同類方法看現有 pattern,一模一樣照抄。

不要讓 AI「發明」寫法。讓它先看你的 codebase 怎麼寫,然後照抄。

結語

AI Coding Agent 是真的很強。一個小時跑完 11 個 unit,涵蓋 DDL、DTO、BLL、Controller、前端頁面——如果全部手寫,至少要兩三天。

但「能跑」和「能用」之間,還是有一道很深的溝。

這道溝不是 AI 的問題,是「團隊知識」的問題。你的命名慣例、你的 Save pattern、你的 return convention——這些東西沒有寫在任何 RFC 或 best practice guide 裡面,它們只存在於你的團隊文化中。

AI 不會自動學會你的文化。你得教它。而且得教得很具體、很原子化、很不留餘地。

如果你也在用 AI 寫 production code,我的建議是:永遠不要相信 build pass。Review 每一行。不是因為 AI 寫得爛,而是因為「正確」有兩個層次——技術正確和慣例正確。AI 能做到前者,後者目前還是需要人。

至少目前是這樣。