跳至主要內容
技術

Spec-Driven Development:寫給 Agent 的需求文件,比寫給人的還嚴格

Spec-Driven Development:寫給 Agent 的需求文件,比寫給人的還嚴格
Agentic Engineering 實戰手冊 第 5 / 14 篇 ,前往系列總覽

這是「Agentic Engineering 實戰手冊」系列的第五篇。上一篇:Context Engineering 深度解析

同一個功能,兩份 Spec,天壤之別的結果

同一個功能需求,我寫了兩份 spec 給 agent。一份花了 30 秒隨手打:「幫我加一個用戶通知功能。」另一份花了 10 分鐘認真寫。

30 秒那份,agent 寫了 400 行 code——email notification、push notification、in-app notification 全做了,還自己加了一個 notification preference 頁面。很「完整」,但我只需要一個簡單的 in-app toast。多花了 3 小時拆掉不需要的東西。

10 分鐘那份,agent 精準地做了一個 toast component + API endpoint + 測試。一次通過。

你花在寫 spec 的時間,省下的往往是 agent 走錯方向後的返工。

這不是嚴格的對照實驗,而是我過去一年工作紀錄裡反覆出現的模式:

Spec 投入時間Agent 產出品質人工修正時間總時間
<1 分鐘方向錯誤、過度設計2-4 小時~3 小時
5-10 分鐘基本正確、細節需調15-30 分鐘~30 分鐘
15-20 分鐘精準、一次通過<10 分鐘~25 分鐘
>30 分鐘不一定更好邊際效益遞減

在我這批中小型任務裡,10–15 分鐘常常夠用;複雜 migration 當然可能需要幾小時甚至幾天。重點不是追一個固定時間,而是寫到範圍、限制與驗收都沒有重大歧義,再停筆。

為什麼「口頭說一下」在 Agent 時代行不通

在傳統團隊裡,你可以跟同事說「幫我加一個通知功能」,然後他會:

  • 先看現有的 codebase 有沒有類似的東西
  • 不確定的地方來問你「你是要 email 還是 in-app?」
  • 看了 mockup 之後說「這個 toast 的位置應該在右上角嗎?」
  • 做的過程中發現問題會暫停,來找你討論

Agent 有時會追問,也可能先探索 codebase;但你不能把品質押在它「剛好問到關鍵問題」上。遇到模糊需求,有些 agent 會選一個合理解釋直接做,而且那個解釋未必是你要的。

真正的風險不是 agent 一定做多,而是它必須替你補完沒說出口的決策。

這不是 agent 的缺陷。這是 LLM 的本質——它被訓練成「有幫助的」(helpful),而「有幫助」在 training data 裡通常意味著「做完整一點」。你要反過來利用這個特性:不是教 agent 少做一點,而是在 spec 裡明確告訴它「做什麼」和「不做什麼」。

我之前分享過的失敗案例裡有一個故事——agent 寫了 800 行白寫的 code,根源就是 spec 問題。那次我學到:模糊的需求 + 認真的 agent = 一堆你不需要的功能。

Spec 三要素:Goal / Constraints / Verification

我最常用的 agent spec 骨架有三個部分;複雜任務可以再補背景、風險或 rollout 計畫,但這三項通常不能少。

1. Goal — 你到底要什麼

不是「怎麼做」,是「最後要什麼結果」。

壞的 Goal

用 React 的 useState 和 useEffect 寫一個 toast notification component,
用 CSS transition 做動畫...

這不是 Goal,這是 implementation plan。你把 agent 的手綁住了——它可能知道更好的做法,但你已經指定了每一步怎麼走。

好的 Goal

在頁面右上角顯示一個 toast notification。
3 秒後自動消失。支援 success / error / info 三種類型。
可以從任何 component 觸發。

告訴 agent 你要的結果,讓它先根據 codebase 提方案。它可能用 portal、store 或 custom event;你再依現有架構、團隊維護能力與風險決定,而不是預設 agent 一定比你清楚。

2. Constraints — 不要做什麼

這是 spec 裡最容易被忽略、但最重要的部分。

為什麼重要:Agent 的傾向是「做多」。不告訴它不要做什麼,它就會做所有它認為「有幫助」的事情。

Constraint 範例

## Constraints
- 只做 in-app toast,不做 email 或 push notification
- 不需要 persistence(頁面 reload 後消失就好)
- 不要新增任何 npm 依賴——用現有的 utility
- 不修改任何現有 component 的 interface
- 優先沿用現有抽象;若需要新增依賴或大幅擴張 scope,先停下來說明理由

每一條 constraint 都可能省你 1 小時的拆除工作。尤其是「不要新增 npm 依賴」這種——agent 最喜歡引入新的 library 來解決問題,但你可能不希望為了一個 toast 多一個 dependency。

3. Verification Criteria — 怎麼判斷做對了

這是你跟 agent 之間的「合約」。做到這些就算完成,沒做到就需要修正。

壞的 Verification

Toast 要能用。

好的 Verification

## Verification Criteria
1. 呼叫 showToast({ type: 'success', message: 'Saved!' }) 後,
   右上角出現綠色 toast,3 秒後消失
2. 呼叫 showToast({ type: 'error', message: 'Failed' }) 後,
   右上角出現紅色 toast,3 秒後消失
3. 連續呼叫 3 次,3 個 toast 垂直排列,各自計時消失
4. npm run build 通過,沒有 TypeScript 錯誤
5. 新增至少 3 個 unit test 覆蓋以上場景

Criteria 要具體到可以重複檢查。Agent 可以先跑測試自我驗證,但你仍要 review 使用者行為、設計取捨與測試本身有沒有寫偏。

如果你搭配 TDD,verification criteria 可以直接變成 test cases,讓自動化幫你驗收。

Task Decomposition:大任務怎麼拆

一個 feature 通常不應該是一個 agent task。它應該被拆成 3-5 個 agent-executable 的單元。

拆的粒度:sweet spot

太粗剛好太細
「做一個 blog 系統」「加一個 related posts component」「在第 42 行加一個 import」
Agent 自己做太多決策Agent 有明確範圍Overhead > 效益
產出難以 review產出 = 一個 reviewable PR你不如自己做

我的經驗法則:一個好的 agent task,大約等於你願意一次認真 review 的變更。檔案數與行數只是結果,不是硬限制;三行權限設定可能比五百行產生碼更危險。

拆法範例

Feature:在部落格加搜尋功能。

Bad decomposition(太粗):

  1. 加搜尋功能

Good decomposition

  1. 建立搜尋 index:在 build time 從所有 blog posts 產生 JSON search index
  2. 搜尋 UI component:建立 SearchBar + SearchResults component
  3. 搜尋邏輯:實作 fuzzy search,支援標題 + 內容 + tags
  4. 鍵盤導航:Cmd+K 開啟搜尋、方向鍵選擇結果、Enter 導航
  5. 整合測試:驗證 search index 產生、搜尋結果正確、keyboard navigation

每個 task 都有明確的輸入和輸出。Agent 可以一個一個做,每做完一個你 review 一次。如果第 3 步的搜尋邏輯出了問題,你只需要修那一步,不影響其他的。

AWS Kiro 的 Spec-First 理念

AWS 在 2025 年推出的 Kiro IDE 把 spec-driven 的理念直接建進了工具裡。在 Kiro 裡:

  1. requirements 檔描述需求與 acceptance criteria
  2. design 檔記錄技術方案
  3. tasks 檔把實作拆成可執行項目
  4. 你可以逐項或依工具當前支援的方式執行,再用專案自己的測試與 review 驗收

雖然我主要用 Claude Code,不用 Kiro,但它的核心理念值得學習:把 spec 當成第一公民,而不是附帶產物。細節以 Kiro Specs 官方文件為準,別把「有 tasks 檔」誤讀成「每一步都會自動產生完整測試」。

對照實驗:Bad Spec vs Good Spec

讓我用一個更技術的例子——「在 API 上加 rate limiting」。

Bad Spec

幫我在 API 上加 rate limiting。

Agent 的產出(摘要):

  • 引入了 express-rate-limit npm package
  • 在所有 API endpoints 上加了 global rate limiter(100 req/min)
  • 加了一個 Redis-based sliding window counter
  • 加了 X-RateLimit-RemainingX-RateLimit-Reset headers
  • 加了一個 /api/rate-limit-status endpoint
  • 寫了 migration script 建立 Redis key
  • 總共 ~350 行新 code

問題:我只有一個簡單的 Astro static site,沒有 Express、沒有 Redis、那些 API 是 Cloudflare Workers serverless functions。整個 output 基本上不能用。

Good Spec

## Task: API Rate Limiting for Cloudflare Workers

### Goal

在 /api/contact 和 /api/subscribe 兩個 endpoints 加上 rate limiting,
降低單一來源短時間大量請求造成的濫用。

### Context

- 這是 Astro 6 專案,部署在 Cloudflare Workers
- API endpoints 是 Cloudflare Workers serverless functions
- 沒有 Redis 或任何 external state store
- 目前流量很小(~100 DAU)

### Constraints

- 使用 Cloudflare Workers Rate Limiting binding(已建立:RATE_LIMITER)
- 不要引入任何 npm dependency
- 只對 POST requests 做 rate limiting
- 已登入時用 account ID 當 key;未登入時才用經雜湊的 IP,並註明共享 IP 的誤判風險
- 門檻由 binding 設定管理,不在 handler 裡另寫一份數字
- 超過限制回 429 Too Many Requests

### Files to modify

- src/pages/api/contact.ts
- src/pages/api/subscribe.ts
- 可以新增一個 src/lib/rate-limit.ts utility

### Verification

1. 對 rate-limit wrapper 寫 unit test:allowed / blocked 兩條路徑都覆蓋
2. 在 preview 環境做 burst 測試,確認超額請求會出現 429
3. 不同 account key 的計數互不影響
4. GET requests 不受 rate limit 影響
5. npm run build 通過

Agent 的產出(摘要):

  • 新增 src/lib/rate-limit.ts,包裝 Cloudflare Rate Limiting binding
  • 修改兩個 endpoint,import rate limiter 並加在 POST handler 前
  • 零 npm dependency
  • handler 只保留取 key、呼叫 binding 與回 429 的流程
  • preview burst test 與 build 都通過

這裡特別不用 KV 自製計數器,因為 Workers KV 是 eventually consistent,不適合要求原子 read-modify-write 的全域限流;Rate Limiting binding才是對應能力。不過它的計數也是每個 Cloudflare location 獨立、偏寬鬆,所以驗收不該承諾全球第六次請求必然精準被擋。若需求是強一致配額或計費,應另評估 Durable Objects 等方案。

把兩條路攤開來看,分岔點其實只有一個——就是那句「幫我在 API 上加 rate limiting」:

flowchart TD
    TASK["「幫我在 API 上加<br/>rate limiting。」"] -->|"Bad Spec"| BAD1["引入 express-rate-limit<br/>npm package"]
    BAD1 --> BAD2["Redis-based<br/>sliding window counter"]
    BAD2 --> BAD3["約 350 行新 code"]
    BAD3 --> BADEND["不能用:<br/>沒有 Express、沒有 Redis"]

    TASK -->|"Good Spec"| GOOD1["Constraints:<br/>不引入 npm dependency<br/>只對 POST 限流"]
    GOOD1 --> GOOD2["Cloudflare Workers<br/>Rate Limiting binding<br/>RATE_LIMITER"]
    GOOD2 --> GOOD3["新增 src/lib/rate-limit.ts<br/>零 npm dependency"]
    GOOD3 --> GOODEND["preview burst test<br/>與 build 都通過"]

兩條路的起點是同一個節點,這才是這張圖真正想讓你看到的事:agent 沒有變笨,也沒有變聰明,兩次收到的是一模一樣的句子。差別全部發生在分岔那一格——Bad Spec 沒寫 Constraints,agent 就照它最熟悉的預設組合動手,直接引入 express-rate-limit 和 Redis;Good Spec 在分岔的瞬間就塞進「不引入 npm dependency」這條限制,把 agent 最愛做的事先鎖死,後面才會走到 Cloudflare Workers Rate Limiting binding 這條真正能用的路。

我的 Spec Template(直接拿去用)

## Task: [一句話描述]

### Goal

[3-5 句描述最終結果,不描述實作方式]

### Context

[Agent 需要知道的背景:tech stack、部署環境、相關系統、目前狀態]

### Constraints

- [不要做什麼]
- [技術限制]
- [不碰哪些檔案]
- [行數 / 複雜度 / dependency 上限]

### Files to modify

- [具體的檔案路徑]
- [可以新增什麼檔案]

### Verification Criteria

1. [具體的測試條件 1]
2. [具體的測試條件 2]
3. [build / lint / type check 通過]

### Out of Scope

- [明確列出不屬於這個任務的東西]
- [避免 agent 自己 scope creep]

重點Out of Scope 是最被低估的區塊。它跟 Constraints 不同——Constraints 是「做的時候不要這樣做」,Out of Scope 是「這些事根本不要做」。

例如你在做搜尋功能,Out of Scope 可能包括:

  • 不做 search analytics(之後另外做)
  • 不做 search suggestions / autocomplete
  • 不做搜尋結果的 pagination

這些都是 agent 非常可能「順便」幫你做的東西。提前說不要,省事。

什麼時候不需要 Spec

不是每個任務都需要完整的 spec。回到 Post 1 提到的計畫粒度矩陣:

任務類型Spec 需求範例
Trivial一句話就好「修這個 typo」
Simple2-3 句 + constraint「加 dark mode toggle,用現有的 CSS custom properties」
Medium完整 spec(上面的 template)「加搜尋功能」
ComplexSpec + decomposition「重構 auth system」

過度 spec 跟 spec 不足一樣是浪費。修一個 typo 不需要寫 Goal / Constraints / Verification。判斷的標準是:如果 agent 可能做出你不要的東西,就需要 constraint。如果任務只有一種合理的做法,一句話就夠。

Takeaway

  1. Spec 品質會直接影響返工量——先花幾分鐘消除重大歧義,通常比做完再拆省;但投入時間要跟任務風險與複雜度走,沒有通用的 10 分鐘甜蜜點。

  2. 好的 spec 有三個要素:Goal(要什麼)、Constraints(不要什麼)、Verification(怎麼驗)。其中 Constraints 和 Out of Scope 是最被低估的——它們防止 agent 做出你不需要的功能。

  3. Task decomposition 的甜蜜點是「一次能完整 review」的大小。用功能邊界、風險與驗收方式切,不要把 3–10 個檔案或 100–500 行當成硬規則。


上一篇:Context Engineering 深度解析 下一篇:Agent 產出品質保證

留言討論

esc
輸入關鍵字搜尋文章...
查看收藏 →