MapleCheng

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

0%

AI Sub-Agent 寫的 Code 怎麼驗收?Build Pass 不代表品質合格

昨天讓 AI Sub-Agent 寫一個前端模組,build 過了,TypeScript 沒報錯,頁面也跑得起來。

我看了一眼就想翻桌。

事情是這樣的

我們在做一個製造業客戶的系統,需要新增一個「製程管理」頁面。需求很明確:一個查詢列表、一個新增/編輯表單,標準的 CRUD 頁面。這種頁面我們專案裡已經有十幾個了,pattern 都是固定的。

所以我很放心地開了一個 Sub-Agent,給它 issue 描述和基本規格,讓它去寫。

二十分鐘後,Sub-Agent 回報:「完成了,build 通過,所有檔案已建立。」

好,讓我看看⋯⋯

第一眼就不對勁

打開檔案,第一個問題:假資料

它用了一個 mockData 陣列,裡面硬編碼了三筆假的製程資料,然後 Table 就直接吃這個 array。完全沒有呼叫 API。

好吧,也許它不知道 API 怎麼接?但我們專案用 orval 自動產生 API hooks,swagger 文件都在那裡。它根本沒去看。

第二個問題:手動 state 管理

1
2
3
4
// Sub-Agent 寫的
const [visible, setVisible] = useState(false);
const [editingRecord, setEditingRecord] = useState(null);
const [data, setData] = useState(mockData);

我們專案有自己的 FormDrawer 元件,內建了 open/close 狀態管理,有 FormTypes interface 定義表單的 open/submit/close 行為。這些東西全部寫在 @packages/antd/components/dialog 裡面,Sub-Agent 完全忽略,自己從頭搞了一套原生的 Modal。

第三個問題:硬編碼中文

1
2
3
4
// Sub-Agent 寫的
<Table.Column title="製程名稱" dataIndex="name" />
<Table.Column title="製程代碼" dataIndex="code" />
<Button type="primary">新增</Button>

我們全專案用 useTranslation 做 i18n,所有 label 都放在 locales/zhTW.json。它直接寫死中文,完全無視國際化。

更多問題陸續浮出

繼續往下看,問題越來越多:

  • **沒用 useTableSearch**:我們的 Table 都搭配這個 hook 做查詢參數管理,它自己寫了一個 handleSearch function
  • **沒用 useFeedback**:成功/失敗的通知用原生 message.success(),不是我們封裝的 hook
  • **沒用 useProgramRule**:權限控制完全沒做
  • 沒在 programRoutes.tsx 註冊:雖然這個檔案是自動產生的,但它連對應的 route config 都沒加

每一個問題單獨看都不嚴重。但加在一起,這個模組基本上是不能用的。它跟專案裡其他頁面的 pattern 完全不一致,如果就這樣 merge 進去,後續維護的人會很痛苦。

最扎心的是:Build 是過的

對,TypeScript 完全沒報錯。ESLint 也沒意見。pnpm build 順利產出。

因為它寫的東西「語法上」完全正確——用原生 React + Ant Design 寫一個 CRUD 頁面,這有什麼錯?

錯在它跟我們專案的規範不一致。而這些規範不是 TypeScript 能檢查的,也不是 ESLint rule 能涵蓋的。

根因:Sub-Agent 不會主動讀規範

這是最關鍵的教訓。

我們的專案規範寫在 .claude/skills/ 目錄裡,包括:

  • 頁面結構:主頁(Query+Table) → Table 用 FormDrawer ref → Form 實作 FormTypes
  • 元件使用:哪些情境用 FormDrawer、哪些用 FormModal
  • API 串接:一律用 orval 產生的 hooks,不手寫 API call
  • 國際化:所有 label 走 useTranslation
  • 通知:用 useFeedback hook

這些文件都在。但 Sub-Agent 不會自己去讀

它拿到的 prompt 是:「幫我建一個製程管理頁面,需要查詢列表和新增/編輯表單。」它就照自己的理解去寫了。它的理解來自訓練資料裡見過的無數個 React + Ant Design 範例,而不是來自我們專案的特定規範。

正確的做法

後來我自己重寫了這個模組。但更重要的是,我從這次經驗整理出了一套驗收流程:

派工時就要把規範塞進去

不能假設 Sub-Agent 會自己找規範。派工的 prompt 要明確寫:

  • 「先讀 .claude/skills/frontend-patterns.md
  • 「Table 要用 useTableSearch hook」
  • 「表單要用 FormDrawer 搭配 FormTypes interface」
  • 「API 用 orval 產生的 hooks,不要手寫」
  • 「所有 label 要用 useTranslation」

是的,很囉唆。但不寫就等著翻桌。

驗收 Checklist(不只看 build)

我現在驗收 Sub-Agent 的產出會逐項檢查:

  • 有沒有用到專案的共用元件?(而不是自己從頭寫)
  • 有沒有呼叫真正的 API?(而不是假資料)
  • 國際化有沒有做?
  • 權限控制有沒有做?
  • 跟現有頁面的 pattern 是否一致?
  • commit message 有沒有帶 issue 號?

這些全部用肉眼 review。沒有工具能自動檢查「你的 Modal 應該用 FormDrawer 而不是原生的」。

給 Sub-Agent 一個範例檔

與其寫一堆規範文字,不如直接指定:「參考 src/modules/xxx/pages/YyyPage.tsx 的寫法。」

讓它照抄一個已經符合規範的頁面,比讓它從零開始理解規範文件有效得多。

為什麼這個問題很重要

AI Coding Agent 現在很火。Claude Code、Cursor、Copilot⋯⋯每個人都在用。但我發現大部分人只看兩件事:「能不能跑」和「build 有沒有過」。

這在個人 side project 沒問題。但在團隊專案裡,code 的價值不只是「能跑」,還有「一致性」和「可維護性」。

一個用原生 React 寫的 CRUD 頁面,跟一個用團隊封裝元件寫的 CRUD 頁面,功能上可能一模一樣。但半年後有人要改需求的時候,前者會讓人想罵人。

            
            flowchart TD
    A[AI 產出 Code] --> B{Build Pass?}
    B -->|No| C[修到 Pass]
    C --> B
    B -->|Yes| D{符合專案規範?}
    D -->|No| E[重寫 / 大幅修改]
    D -->|Yes| F{跟現有 Pattern 一致?}
    F -->|No| E
    F -->|Yes| G[可以 Merge]
          

大部分人在 Build Pass 那一步就停了。但真正的品質門檻在後面兩步。

一些補充想法

這不是 AI 的錯

公平地說,如果我找一個新人工程師,給他同樣的需求描述但不告訴他專案規範,他八成也會寫出類似的東西。差別只在於新人會在 code review 被打回來之後學到教訓,而 AI 下次還是會犯同樣的錯(除非你在 prompt 裡再次強調)。

Context 的成本

把所有規範塞進 prompt 是有成本的。Token 不是免費的,而且 context window 有上限。你需要判斷哪些規範是「不說就一定會錯」的,優先放進去。

我的經驗是:共用元件的使用方式 > API 串接方式 > 國際化 > 權限。因為前兩個錯了基本上要重寫,後兩個至少還能補。

可能的改善方向

長期來看,我覺得有幾個方向可以降低這個問題的嚴重性:

  • 用 ESLint custom rule 強制使用特定元件(比如禁止直接用 antdModal,必須用 FormDrawer
  • .claude/ 規範裡加更多具體的 do/don’t 範例
  • 派工時自動注入專案規範(透過 skill 或 template)
  • 建立自動化的 pattern consistency check

但現階段,最實際的做法還是:認真做 code review,不要被 build pass 騙了。

結語

AI 寫 code 的能力已經很強了,但「寫出能跑的 code」和「寫出符合團隊規範的 code」之間,還有一段不小的距離。

這段距離不是靠 AI 進步就能消除的——每個團隊的規範都不一樣,這本來就是 context-dependent 的東西。能消除它的,只有更好的 prompt engineering 和更嚴格的 code review。

如果你也在團隊裡用 AI Coding Agent,記得:Build Pass 只是最低門檻。真正的驗收,從打開檔案的那一刻才開始。