SDD实战:如何写出让AI听得懂的规格文档
先问一个问题你有没有遇到过这种情况产品经理写了一份需求文档开发看完说我理解的不是这样开发写了一段代码AI生成出来的完全是另一个东西三个月后回头看自己写的文档完全不知道当初想表达什么问题出在哪里我们写的文档缺的不是文笔而是格式。今天要聊的SDDSpecification-Driven Development规格驱动开发本质上就是在解决一个问题怎么写文档才能让AI也让人准确理解我们想要什么第一章SDD到底是什么1.1 通俗理解SDD用一个生活例子来解释你点外卖的时候会发生什么你 → 填写订单规格→ 商家接单 → 制作 → 骑手配送 → 收到外卖如果把写代码比作做菜传统开发SDD开发你告诉厨师做道好吃的菜你给厨师一张详细的食谱厨师自由发挥厨师按食谱做结果可能好吃可能难吃结果基本可控SDD的核心就是把你想要什么写得足够详细详细到AI或任何人照着做不会做错。1.2 为什么现在SDD突然火了三个原因1. AI能读懂规格文档了以前写规格文档主要是给人看。人可以追问、可以脑补、可以根据上下文理解。但AI不一样——你写模糊了AI就做歪了。2. AI生成代码的速度太快了传统开发写代码 → 调试 → 改bug → 上线 SDD开发写规格 → AI生成代码 → 验证规格 → 上线 当AI生成代码只需要几分钟时写好规格就成了最大的瓶颈。3. 规格文档成了源代码在SDD的思维里规格文档才是核心代码只是规格的执行产物。就像菜谱比厨师的手艺更值钱麦当劳就是这么干的——标准化食谱保证每家店味道一致。1.3 SDD的三种承诺级别不是所有团队都需要一步到位。SDD分三个级别┌─────────────────────────────────────────────────────────────────┐ │ SDD承诺级别金字塔 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────┐ │ │ ╱ Spec-as ╲ │ │ ╱ -source ╲ ← 规格即源码 │ │ ╱ (规格是源头) ╲ 代码可随时重生成 │ │ ╱───────────────── ╲ │ │ ╱ Spec- ╲ │ │ ╱ anchored ╲ ← 规格是治理契约 │ │ ╱ (规格是主合同) ╲ 代码必须对齐规格 │ │ ╱───────────────────────╲ │ │ ╱ Spec-first ╲ │ │ ╱ (规格指导AI) ╲ ← 规格指导但非强制 │ │ ╱──────────────────────────────╲ │ │ │ └─────────────────────────────────────────────────────────────────┘级别1Spec-first规格指导AI适合刚引入SDD的团队。规格写出来但代码仍然是主要交付物。级别2Spec-anchored规格是治理契约适合大多数团队。规格是合同代码变更必须更新规格规格更新也必须同步代码。级别3Spec-as-source规格即源码适合AI原生团队。规格是源代码代码是构建产物可以随时用新AI重新生成。第二章为什么AI需要听得懂的规格2.1 AI生成代码的两个大坑坑1幻觉HallucinationAI会脑补信息来填补规格中的空白。举个例子你告诉AI用户可以删除浏览历史AI可能理解为删除一条记录删除全部删除30天前的删除需要确认吗删除后还能恢复吗删除是同步还是异步你没说清楚AI就自己猜了。坑2上下文丢失Context Lost同一个项目里不同人可能对同一个概念有不同理解。比如用户这个概念产品经理理解真实的人类用户前端理解登录账号后端理解userId对应的数据实体AI理解天知道是什么规格不统一AI就混乱。2.2 好规格 vs 坏规格对比坏规格让人和AI都困惑用户可以删除浏览历史 删除时要检查权限 删除要快好规格让人和AI都清晰## Requirement: 浏览历史批量删除 系统应提供按product_code列表批量删除用户浏览历史的能力。 ### 场景1正常批量删除 - 当客户端调用 DELETE /browse/history请求体为 {product_codes: [P001, P002]} - 那么系统删除当前用户所有匹配product_code的浏览记录 - 并且返回 {code: 200, data: {deleted: 2}} ### 场景2删除不存在的记录 - 当客户端删除的product_code在历史中不存在 - 那么系统仍返回成功deleted0 - 并且不会报错或抛出异常 ### 场景3无权限情况 - 当客户端未携带token请求头 - 那么系统返回 401 Unauthorized - 并且返回 {code: 401, message: token required}第三章SDD规格文档的四个层次3.1 为什么需要分层写规格文档就像盖房子┌─────────────────────────────────────────────────────────────┐ │ 盖房子类比 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 【第一层项目提案】 │ │ 我要盖什么房子 │ │ - 别墅还是公寓 │ │ - 为什么要盖自住投资 │ │ - 预计花多少钱 │ │ │ │ 【第二层设计图纸】 │ │ 我怎么盖 │ │ - 几层楼什么结构 │ │ - 地基怎么处理 │ │ - 为什么选砖混不用钢结构 │ │ │ │ 【第三层施工规范】 │ │ 每一步怎么做 │ │ - 砖要砌在哪个位置 │ │ - 水泥砂浆配比多少 │ │ - 验收标准是什么 │ │ │ │ 【第四层任务清单】 │ │ 先做什么后做什么 │ │ - 第一天打地基 │ │ - 第二天砌墙 │ │ - 第三天封顶 │ │ │ └─────────────────────────────────────────────────────────────┘OpenSpec就是这样一套分层规范它把规格文档分成四层Proposal → Design → Spec → Tasks。3.2 第一层Proposal提案—— 回答为什么做核心问题为什么要做这个功能做了之后有什么变化会影响到哪些地方提案的黄金结构## Why为什么做 用一段话描述 - 现在存在什么问题 - 这个问题有多严重 - 解决之后有什么价值 ## What Changes做了什么 列出3-5个关键变更点用bullet list一句话一个 ## Capabilities能力清单 ### New Capabilities新增能力 - 能力A做什么 - 能力B做什么 ### Modified Capabilities修改的能力 - 列出被影响到的已有能力或者写None ## Impact影响范围 - Affected code会改哪些代码可以用路径模式 - APIs会新增或修改哪些接口 - Data/Storage存储层有什么变化一个真实的提案示例## Why 理财超市目前缺少用户搜索历史、用户浏览历史和热门浏览能力 无法支持常见的历史回看与热点推荐场景。需要在现有架构下 补齐这批接口统一存储口径尽快支撑前端联调。 ## What Changes - 新增理财超市搜索/浏览相关接口历史查询、批量删除、热门浏览 - 新增Redis双层历史模型个人维度历史 全局浏览热度榜 - 模糊检索改用t_product_info而非t_product_search ## Capabilities ### New Capabilities - mall-user-search-history: 用户搜索历史查询与删除 - mall-user-browse-history: 用户浏览历史查询与删除 - mall-global-browse-ranking: 全局热门浏览排行榜 ### Modified Capabilities - None ## Impact - Affected code: - controller/新增搜索/浏览controller - manager/新增编排manager - repository/新增Redis访问抽象 - APIs: 新增5个REST接口 - Data/Storage: 新增3组Redis key3.3 第二层Design设计—— 回答怎么做核心问题现有系统是什么样子我们的目标和边界在哪里技术上怎么实现的有什么风险怎么应对设计的黄金结构## Context背景 描述现有的系统架构、约束条件、依赖关系 ## Goals / Non-Goals目标 非目标 明确做什么也要明确不做什么 很多人规格写不清楚就是缺少非目标这个部分 ## Decisions决策 这是设计文档最核心的部分 每个技术决策都要写清楚 - Decision做了什么决定 - Rationale为什么这么做 - Alternatives considered考虑过什么替代方案为什么没选 ## Risks / Trade-offs风险 权衡 - 可能会出什么问题 - 怎么缓解 ## Migration Plan迁移计划 分步骤的实施计划让读者知道先做什么后做什么 ## Open Questions开放问题 还没想清楚的问题或者需要和业务方确认的问题一个真实的设计决策示例## Decisions ### 1) Redis数据模型决策 - **Decision**采用3类key - mall:global:browse:rankZSETmemberproduct_codescore浏览次数 - mall:user:{userId}:browse:historyZSET存最近浏览 - mall:user:{userId}:search:historyZSET存搜索关键词 - **Rationale** - ZSET天然支持排序热门排行和去重同一product_code只保留一条 - 同一个key可以同时满足查top10热门和查我的历史 - 覆盖式写入用ZINCRBY一条命令就搞定 - **Alternatives considered** - 用LIST存储历史实现最简单但不支持去重和topN查询 - 用HASH存储结构灵活但批量删除需要遍历性能差3.4 第三层Spec规格—— 回答做什么核心问题系统的具体行为是什么在什么情况下做什么正常情况、边界情况、错误情况分别怎么处理规格的黄金结构## ADDED Requirements新增需求 ### Requirement: [需求名称] [The system SHALL ... 系统必须遵守的规则] #### Scenario: [场景名称] - **WHEN** [什么情况下触发] - **THEN** [系统应该怎么做] - **AND** [还有什么其他结果] - **BUT** [可能会有的转折] #### Scenario: [边界场景] ... #### Scenario: [错误场景] ...为什么要用Gherkin风格的WHEN-THEN因为它强迫你思考什么情况下 → 系统应该怎样。这个模式能覆盖掉很多模糊地带。一个真实的规格示例## ADDED Requirements ### Requirement: 用户浏览历史查询与删除 系统应提供用户浏览历史的查询和批量删除API。 #### 场景查询最近浏览记录 - 当客户端携带token请求头调用浏览历史查询接口 - 那么系统返回当前用户最近10条浏览记录 - 并且每条记录包含product_code和product_full_name - 并且记录按浏览时间倒序排列 #### 场景批量删除浏览历史 - 当客户端调用删除接口请求体为{product_codes: [P001, P002]} - 那么系统删除当前用户所有匹配product_code的浏览记录 - 并且返回被删除的记录数量 #### 场景重复浏览同一产品 - 当同一用户多次浏览同一product_code的产品详情页 - 那么浏览历史中只保留一条记录 - 并且该记录的时间戳更新为最新浏览时间 - 并且之前的历史数据被覆盖 ### Requirement: 浏览行为写入 用户每次浏览产品详情时系统应更新全局热榜和个人浏览历史。 #### 场景浏览产品详情页 - 当用户访问产品详情页 - 那么系统将product_code写入全局浏览热榜score1 - 并且系统将{product_code, product_full_name}写入用户浏览历史3.5 第四层Tasks任务—— 回答先做什么核心问题分解成哪些具体任务任务之间有什么依赖怎么才算完成任务清单的黄金结构## Phase 1: 基础架构 - [ ] 1.1 任务描述清晰、可执行 - [ ] 1.2 任务描述 ## Phase 2: 核心功能 - [ ] 2.1 任务描述 - [ ] 2.2 任务描述 ## Phase 3: 验证上线 - [ ] 3.1 任务描述好任务 vs 坏任务对比坏任务好任务实现用户浏览历史功能实现UserBrowseHistoryRepository接口写接口新增GET /browse/history返回最近10条记录测试编写UserBrowseHistoryRepository的单元测试覆盖覆盖式写入和批量删除场景第四章AI友好规格的写作技巧4.1 用SHALL/SHOULD/MAY表达约束力度这是RFC 2119定义的三个级别关键词含义解读SHALL强制要求必须实现不实现就是bugSHOULD推荐要求建议实现有合理理由可以不实现MAY可选要求实现也行不实现也行例子### Requirement: 数据校验 - 系统SHALL验证product_code格式为字母数字组合 - 系统SHOULD验证product_code长度不超过20字符 - 系统MAY验证product_code是否存在于产品库4.2 WHEN-THEN模式的核心要点WHEN后面跟的是触发条件或输入用户点击删除按钮客户端发送DELETE请求收到Kafka消息THEN后面跟的是必然结果系统返回成功数据库中记录被删除发送通知消息AND用于连接多个同类型条件返回成功 AND 返回被删除的数量BUT用于表达转折记录被删除 BUT 不会立即从缓存中清除4.3 数据边界要写具体数值❌ 模糊写法分页参数page和pageSize 返回产品列表✅ 清晰写法分页参数 - page页码从1开始page1返回第1-10条 - pageSize每页条数默认10最大100 返回字段 - total总记录数整数 - page当前页码 - pageSize每页条数 - items产品列表数组每个元素包含 - product_code产品代码字符串最大20字符 - product_full_name产品全称字符串 - browse_count浏览次数整数4.4 错误场景同样重要很多规格文档只写正常流程但AI最难处理的就是异常情况。要覆盖的错误场景### 场景token缺失 - 当客户端调用API时未携带token请求头 - 那么系统返回401 Unauthorized - 并且返回{code: 401, message: token required} ### 场景token格式错误 - 当客户端携带格式错误的token - 那么系统返回401 Unauthorized - 并且返回{code: 401, message: invalid token format} ### 场景空删除列表 - 当客户端调用删除接口时product_codes为空数组[] - 那么系统返回400 Bad Request - 并且返回{code: 400, message: product_codes cannot be empty} ### 场景page参数越界 - 当客户端请求的page大于总页数 - 那么系统返回空数组items[] - 并且返回正确的total和pageCount供前端判断4.5 用对比表替代长篇论述❌ 长篇大论的决策描述我们考虑了多种存储方案。第一种是使用Redis的LIST结构 这种结构实现最简单插入和读取都很快但是缺点是不支持去重 如果用户多次浏览同一产品会产生多条记录需要在业务层处理 去重逻辑增加复杂度。第二种方案是使用Redis的HASH结构 这种结构可以存储更多字段但批量删除需要先遍历再删除 性能较差。基于以上分析我们决定采用ZSET结构...✅ 清晰的对比表格### 存储方案对比 ┌─────────────┬────────────┬────────────┬────────────┬────────────┐ │ 方案 │ 去重支持 │ topN查询 │ 批量删除 │ 结论 │ ├─────────────┼────────────┼────────────┼────────────┼────────────┤ │ LIST │ ❌ │ ❌ │ ⚠️ │ 放弃 │ │ HASH │ ✅ │ ❌ │ ❌ │ 放弃 │ │ ZSET │ ✅ │ ✅ │ ✅ │ ✅采用 │ └─────────────┴────────────┴────────────┴────────────┴────────────┘ 结论采用ZSET去重和topN查询都是原生支持性能最优。第五章OpenSpec实战复盘5.1 OpenSpec是什么OpenSpec是 Wealth Insight 项目自定义的一套规格驱动开发工作流。它定义了openspec/ ├── changes/ │ └── change-name/ │ ├── .openspec.yaml ← 元数据谁创建的、什么时间、什么状态 │ ├── proposal.md ← 提案为什么做 │ ├── design.md ← 设计怎么做 │ ├── tasks.md ← 任务清单 │ └── specs/ │ └── capability/ │ └── spec.md ← 规格做什么5.2 从一个真实Change学习以add-mall-browse-search-history-apis为例Proposal提案回答三个问题Why为什么做解决没有搜索/浏览历史的问题What做了什么新增5个接口、Redis模型、模糊检索Impact影响什么哪些代码、哪些API、哪些存储Design设计的精髓是 Decisions每一项技术决策都要写清楚做了什么决定Decision为什么这么做Rationale还考虑了哪些方案Alternatives比如模糊检索的数据源决策Decision禁止使用t_product_search用t_product_infoRationale知识库明确字段可追溯且满足约束Alternatives用了t_product_search更快但与约束冲突Spec规格的精髓是 WHEN-THEN 场景每一个 Capability 拆解成多个 Scenario每个 Scenario 用 WHEN-THEN-AND 描述比如 Browse History 的覆盖语义Scenario: 重复浏览同一产品 - WHEN: 用户多次浏览同一产品详情 - THEN: 只保留一条记录 - AND: 时间戳更新为最新Tasks任务的精髓是依赖关系任务之间有清晰的先后顺序Common层 → Domain层 → Infrastructure层 → Controller层5.3 OpenSpec的独特设计1. Why/What/Capabilities 三段论提案不是流水账而是Why问题的价值判断为什么要做What变化的抽象描述做了什么Capabilities能力的边界定义能做什么2. Decision 的 Rationale 必须写每个技术选型都要回答为什么选这个还要回答为什么放弃其他的。这逼迫开发者在做决策前真正思考过各种方案的利弊。3. Open Questions 保留未知不是所有问题都要在写规格时解决。Open Questions 把还没想清楚的问题显式列出来防止以为想好了其实没想好。4. Non-Goals 防止范围蔓延明确不做什么和做什么同样重要。很多项目做砸了就是因为规格里没写 Non-Goals。第六章SDD自检清单每次写完规格文档对着这个清单检查一遍┌─────────────────────────────────────────────────────────────────┐ │ SDD规格文档自检清单 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ✅ Proposal 检查 │ │ ──────────────────────────────────────────── │ │ □ 清晰说明了为什么做Why │ │ □ 明确了影响范围代码、API、存储 │ │ □ 列出了所有新能力Capabilities │ │ □ Capabilities 的边界清晰不会让人误以为能做更多 │ │ │ │ ✅ Design 检查 │ │ ──────────────────────────────────────────── │ │ □ Goals 和 Non-Goals 都有明确 │ │ □ 每个 Decision 都有 Rationale为什么这么做 │ │ □ 列举了 Alternatives considered考虑过什么 │ │ □ 列出了 Risks 和 Mitigation风险预案 │ │ □ 有 Migration Plan分步骤实施计划 │ │ □ Open Questions 捕获了未解决的疑惑 │ │ │ │ ✅ Spec 检查 │ │ ──────────────────────────────────────────── │ │ □ 每个 Requirement 用 SHALL/SHOULD/MAY 表达约束力度 │ │ □ 每个场景有 WHEN-THEN 模式触发 → 结果 │ │ □ 覆盖了正常场景、边界场景、错误场景 │ │ □ 数据格式有具体示例不只是说JSON │ │ □ 字段命名与 API 文档一致 │ │ □ 响应码有明确说明200成功、400参数错误、401未授权等 │ │ │ │ ✅ Tasks 检查 │ │ ──────────────────────────────────────────── │ │ □ 任务有明确依赖关系先做什么后做什么清晰 │ │ □ 任务粒度可执行不是实现功能X这种模糊任务 │ │ □ 有验收标准完成任务意味着什么可验证 │ │ │ └─────────────────────────────────────────────────────────────────┘第七章常见问题Q1什么时候开始写规格A在需求明确之后实现代码之前。规格是需求到代码的桥梁。如果你还没想清楚要做什么就先别写规格。Q2规格要写多详细A写到AI照着做不会做错的程度。具体来说数据格式要具体字段名、类型、取值范围边界条件要覆盖空值、最大值、非法值错误处理要明确返回什么码、什么消息Q3规格写错了怎么办A改规格然后重新生成代码。这正是SDD的核心优势——代码是规格的产物规格可以迭代代码就可以迭代。Q4谁来写规格A产品经理写Proposal和Spec开发写Design和Tasks。但实际上分工不固定关键是谁对这块业务最理解谁就最适合写。Q5和传统PRD有什么区别A传统PRD是以让人理解为目标SDD规格是以让AI执行为目标。这意味着SDD规格更强调精确性不能有歧义SDD规格更强调完整性边界和异常都要覆盖SDD规格更强调可执行性每一条都可以转化为验收测试结语规格是给AI的需求文档┌─────────────────────────────────────────────────────────────────┐ │ │ │ 过去 │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 需求文档 │ ──▶ │ 代码 │ │ │ │ 给人看 │ │ 可能走样 │ │ │ └──────────────┘ └──────────────┘ │ │ │ │ 现在 │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 规格文档 │ ──▶ │ AI生成 │ ──▶ │ 代码 │ │ │ │ 给AI看 │ │ 精确执行 │ │ 符合规格 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ 核心心法 │ │ If you cant explain it precisely, you dont understand it │ │ 如果你没法精确地解释一件事说明你还没有真正理解它 │ │ │ └─────────────────────────────────────────────────────────────────┘SDD不是银弹它解决的是规格不清的问题。如果你的需求本身就没想清楚SDD也帮不了你——它只会让你更清楚地看到自己想得有多不清楚。但如果你已经想清楚了需求SDD能帮你把想清楚这件事固化成一份精确的文档让AI也能准确执行。所以 规格驱动开发