Spec-Driven Development:寫給 Agent 的需求文件,比寫給人的還嚴格
這是「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(太粗):
- 加搜尋功能
Good decomposition:
- 建立搜尋 index:在 build time 從所有 blog posts 產生 JSON search index
- 搜尋 UI component:建立 SearchBar + SearchResults component
- 搜尋邏輯:實作 fuzzy search,支援標題 + 內容 + tags
- 鍵盤導航:Cmd+K 開啟搜尋、方向鍵選擇結果、Enter 導航
- 整合測試:驗證 search index 產生、搜尋結果正確、keyboard navigation
每個 task 都有明確的輸入和輸出。Agent 可以一個一個做,每做完一個你 review 一次。如果第 3 步的搜尋邏輯出了問題,你只需要修那一步,不影響其他的。
AWS Kiro 的 Spec-First 理念
AWS 在 2025 年推出的 Kiro IDE 把 spec-driven 的理念直接建進了工具裡。在 Kiro 裡:
- requirements 檔描述需求與 acceptance criteria
- design 檔記錄技術方案
- tasks 檔把實作拆成可執行項目
- 你可以逐項或依工具當前支援的方式執行,再用專案自己的測試與 review 驗收
雖然我主要用 Claude Code,不用 Kiro,但它的核心理念值得學習:把 spec 當成第一公民,而不是附帶產物。細節以 Kiro Specs 官方文件為準,別把「有 tasks 檔」誤讀成「每一步都會自動產生完整測試」。
對照實驗:Bad Spec vs Good Spec
讓我用一個更技術的例子——「在 API 上加 rate limiting」。
Bad Spec
幫我在 API 上加 rate limiting。
Agent 的產出(摘要):
- 引入了
express-rate-limitnpm package - 在所有 API endpoints 上加了 global rate limiter(100 req/min)
- 加了一個 Redis-based sliding window counter
- 加了
X-RateLimit-Remaining和X-RateLimit-Resetheaders - 加了一個
/api/rate-limit-statusendpoint - 寫了 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」 |
| Simple | 2-3 句 + constraint | 「加 dark mode toggle,用現有的 CSS custom properties」 |
| Medium | 完整 spec(上面的 template) | 「加搜尋功能」 |
| Complex | Spec + decomposition | 「重構 auth system」 |
過度 spec 跟 spec 不足一樣是浪費。修一個 typo 不需要寫 Goal / Constraints / Verification。判斷的標準是:如果 agent 可能做出你不要的東西,就需要 constraint。如果任務只有一種合理的做法,一句話就夠。
Takeaway
-
Spec 品質會直接影響返工量——先花幾分鐘消除重大歧義,通常比做完再拆省;但投入時間要跟任務風險與複雜度走,沒有通用的 10 分鐘甜蜜點。
-
好的 spec 有三個要素:Goal(要什麼)、Constraints(不要什麼)、Verification(怎麼驗)。其中 Constraints 和 Out of Scope 是最被低估的——它們防止 agent 做出你不需要的功能。
-
Task decomposition 的甜蜜點是「一次能完整 review」的大小。用功能邊界、風險與驗收方式切,不要把 3–10 個檔案或 100–500 行當成硬規則。
上一篇:Context Engineering 深度解析 下一篇:Agent 產出品質保證