AI编程时代如何用SDD文档驱动开发避免代码失控

发布时间:2026/9/8 19:47:48
AI编程时代如何用SDD文档驱动开发避免代码失控 1. 为什么我在 AI 编程时代重新捡起“写文档”最近大半年我几乎每天都在用各种 AI 编程工具写代码、改代码、查 bug。说实话效率提升确实明显但踩的坑也一点不少让 AI 生成一个功能模块它洋洋洒洒给你几百行代码跑起来没问题可一旦要加需求、换逻辑整个代码就像一团被猫玩过的毛线。改一处崩三处最后只能推倒重来。后来我复盘了一下问题不在 AI在我自己。我太依赖“对话式编程”了——想到什么就让 AI 写什么写着写着需求就飘了边界就模糊了代码自然就失控了。直到我认真去了解 SDD也就是 Spec-Driven Development文档驱动的开发方法论才算把 AI 这头猛兽拴上了缰绳。SDD 的核心思路就一句话**先写清楚“要做什么”和“怎么算做完”再让 AI 动手写代码。**一切以文档为准文档先行代码殿后。这个理念其实在传统软件工程里不算新鲜但在 AI 编程时代它的价值被彻底放大了。因为 AI 不知道你的业务上下文不懂你心里没说出口的隐藏需求它只会根据你的 prompt 给一个“看起来对”的答案。只有你把需求、约束、边界、验收标准都写清楚AI 才能给出真正可用的代码。这篇文章我不会跟你扯太多学院派理论就结合我这几个月的实战经验聊聊 SDD 到底是什么、怎么落地、能解决哪些具体问题。如果你是做应用开发、用 AI 辅助编码、或者正在带技术团队的这篇文章应该能帮你在“AI 写代码”这件事上少走很多弯路。2. SDD 的概念拆解它不是什么新东西只是被 AI 逼成了刚需2.1 SDD 的基本含义规格先于实现简单讲SDD 就是把“写代码”这件事拆成两步第一步用自然语言或结构化文档把软件要做什么、做到什么程度描述清楚第二步再基于这份规格说明去实现代码。在传统开发里这就是需求分析和概要设计但 SDD 更强调“规格文档作为唯一事实来源”代码只是规格的一种具体实现。在 AI 编程的语境下这个“规格文档”直接就变成了 AI 的输入。你给 AI 的 prompt 越规范、越完整AI 生成的代码就越贴合预期。反之如果你上来就说“帮我写个用户登录模块”AI 大概率会给你一个能跑但到处是坑的实现没有错误处理、没有并发控制、没有参数校验、硬编码了一堆业务常量。你用的时候才发现各种问题再回头去补来回沟通的成本比你自己写还高。2.2 从 Vibe Coding 到 SDD两种 AI 编程姿势的对比最近网上很流行一个词叫 Vibe Coding意思是跟着感觉编程——开着 AI 聊天窗口想到哪写到哪让 AI 不断生成、修改代码。这种方式在写 demo、做原型、跑通小工具的时候特别爽但它有明显的天花板代码库一旦超过几千行或者涉及多个模块协作这种“随缘编程法”就会原形毕露。我做了一个简单的对比SDD 和 Vibe Coding 的核心差异如下表维度Vibe CodingSDD起点一句话需求一份完整的规格文档迭代方式对话式反复修改文档版本化驱动代码边界模糊容易蔓延清晰受规格约束适用场景原型、工具、一次性脚本正式项目、多人协作、长期维护可测试性低改起来全靠手感高验收标准明确在 AI 时代的风险AI 幻觉被无限放大AI 幻觉被控制在范围内注意我不是说 Vibe Coding 一无是处它确实是 AI 时代很自然的入门方式。但你如果想正经做一个产品、一个可持续维护的系统SDD 几乎是唯一靠谱的选择。2.3 Thoughtworks 的三级分类框架SDD 也不是一刀切我最早看到 SDD 是被 Thoughtworks 的一位工程师专家 Birgitta Böckeler 的文章启发的。她提出了一个三级分类框架我实际操作下来觉得非常实用第一级需求规格Requirements Spec。用自然语言描述用户故事、业务规则和验收标准。这一级解决“做什么”的问题主要面向产品经理、业务分析师。第二级技术规格Technical Spec。面对开发人员描述系统架构、模块划分、接口定义、数据结构、异常处理策略。这一级解决“怎么做”的问题。第三级实例规格Executable Spec。用可执行的测试用例表达规格让“文档”可以直接被机器验证。这一级解决“怎么证明做完了”的问题。我在实战中把这三层直接映射到了 AI 编程的 input 设计上第一层给 AI 提供业务上下文第二层给 AI 规定技术边界第三层作为 AI 生成代码后的自动校验工具。三层缺一不可光有需求没有技术约束AI 会产出风格完全不同、无法跟现有代码融合的实现光有技术没有测试你无法验证 AI 写的代码到底对没对。3. 文档先行到底写什么从业务需求到执行规格的一步步拆解很多朋友看到“写文档”这三个字就开始头疼脑子里浮现的是几十页没人看的 Word。别急SDD 里的文档不是让你写那种“面子工程”而是写真正能驱动开发的“操作手册”。我把我在实践中用得最顺的一套文档结构分享出来你按这个框架填充基本就能覆盖大部分项目场景。3.1 业务需求层用户故事 验收条件这一层是给 AI 提供“为什么做”和“做什么”的上下文。我一般用下面的模板来写每条用户故事尽量控制在一个自然段内作为【某类用户】我希望【执行某操作】以便【达成某价值】。验收标准当【前置条件】时系统应该【行为 A】当【异常情况】时系统应该【行为 B】完成后【某个可观察的状态】应该变为【预期结果】。举个例子。我在做一个企业内部的知识库系统时有一条需求是这样写的作为知识库管理员我希望在导入文档时自动检测重复文件以便节省存储空间并避免内容混乱。验收标准当上传文件的内容哈希与库中已有文件完全一致时系统应该拒绝导入并提示“该文件已存在”当上传文件内容相同但文件名不同时系统应该询问用户是否覆盖文件导入成功后应该在文档列表中展示最新的版本号。这段描述我直接丢给 AI它生成的代码基本覆盖了主要逻辑分支。如果我只说“实现一个知识库支持文档上传”AI 大概率不会想到去处理重复文件这种细节。3.2 技术规格层架构约束 接口定义技术规格是约束 AI 不要“放飞自我”的紧箍咒。没有人喜欢返工而 AI 特别喜欢在不经意间引入你技术栈之外的依赖、或者改变你已有的编码风格。所以技术规格必须写清楚语言和框架版本。比如“使用 Python 3.11 FastAPI禁止引入 Django”。项目目录结构。比如“业务逻辑放在services/路由处理器放在api/数据模型放在models/”。接口定义。请求方法、路径、入参、出参、错误码。数据存储方案。用哪个数据库、表结构怎么设计、有没有缓存层。编码风格。命名规范、注释要求、异常处理模式。拿接口定义来说我习惯用 OpenAPI 风格去描述但不用写得太正式关键字段对齐就行。比如POST /api/v1/documents/import 入参 file: binary文件内容 override: boolean可选是否覆盖同名重复文件 出参 200: { document_id: xxx, version: 2, message: 导入成功 } 409: { error: duplicate_content, message: 该文件已存在 } 422: { error: invalid_file_type, message: 不支持的文件格式 }这段规格 AI 一看就懂生成代码时就会主动处理这些分支不会只写一个“快乐路径”了事。3.3 执行规格层可跑的测试就是最好的文档第三层也是我强烈推荐你重点投入的一层把验收条件转成自动化测试。这一层有双重作用一是给 AI 当“约束条件”二是给开发者当“安全网”。我在实际操作中会把每个用户故事的验收标准直接翻译成测试代码。比如上面的文档导入案例我可能先用 Pytest 写好三个测试用例def test_duplicate_content_rejected(client, db): # 先导入一个文件 client.post(/api/v1/documents/import, files{file: (a.txt, bhello, text/plain)}) # 再导入内容相同但文件名不同的文件 resp client.post(/api/v1/documents/import, files{file: (b.txt, bhello, text/plain)}) assert resp.status_code 409 assert resp.json()[error] duplicate_content这些测试写好后我把它们连同技术规格一起丢给 AI让它在不修改测试的前提下实现功能。这样 AI 生成的代码对不对不用靠人肉 review直接跑测试就知道。这个过程其实就是把第三级“可执行规格”用到了实战里。4. 完整实操一次用 SDD 驱动 AI 开发的全过程记录这一章我拿一个真实的小项目来走一遍完整流程你跟着做一遍基本就能掌握 SDD AI 的节奏了。项目背景是我给团队做的内部工具一个简单的工时记录 API支持团队成员登记每日工时、查看周报。需求不大但涉及联表查询、权限校验、聚合统计足够说明问题了。4.1 第一步先写业务需求不急着打开编辑器我打开一个空白 Markdown 文件开始写这个项目的业务需求。这里有个心得先别管技术方案就把业务理清楚。我通常会和需求方一起列关键流程然后写成用户故事。工时记录系统的故事拆解作为团队成员我希望每天登记我的工时以便后续统计工作量。作为团队成员我希望看到自己本周的工时总和以便确认是否满勤。作为团队经理我希望查看任意成员的一周工时明细以便评估分配是否合理。验收标准我挑两条写细一点故事 A登记工时当选择日期为工作日周一至周五时系统应该允许登记 1 到 12 小时的工时当日期为周末时系统应该拒绝登记并提示“周末无需登记工时”当已存在同一天同一项目的登记记录时系统应该返回 409 冲突错误团队成员只能登记自己的工时不能替他人登记。故事 B查看周报当查询本周的工时时系统应该返回周一至周日七天的数据无记录的天返回 0团队经理可以查询任意成员的周报普通成员只能查询自己的接口返回数据中应包含总工时时长和项目维度的小计。这一步大概花了我 30 分钟。看似“啥也没干”实际上项目的大框架已经在脑子里成形了后续所有环节都可以围绕这份文档展开。4.2 第二步确定技术约束写技术规格接下来定义技术方案。因为项目不大我选择了 FastAPI SQLite SQLAlchemy。技术规格文档里我明确写了项目结构time-tracker/ ├── app/ │ ├── main.py # 应用入口 │ ├── models.py # SQLAlchemy 模型 │ ├── schemas.py # Pydantic 请求/响应模型 │ ├── services/ # 业务逻辑 │ ├── repositories/ # 数据访问层 │ └── api/ # 路由层 ├── tests/ │ ├── conftest.py │ ├── test_entries.py │ └── test_reports.py接口方面定义了三个核心端点POST /api/v1/entries # 登记工时 GET /api/v1/entries?date2025-06-09limit20 # 查询个人记录 GET /api/v1/reports/weekly?user_idxxxweek2025-W24 # 周报并且约定了统一响应格式{ status: ok | error, data: {...} | null, message: 错误描述可选 }这些信息越具体AI 生成的代码就越“像你团队的人写的”。4.3 第三步写测试用例让 AI 有据可循接着我先把测试用例写好。这一步很多人不习惯因为传统开发里测试总是排在功能后面。但在 SDD 流程里我强烈建议先写测试甚至可以让 AI 帮你生成测试骨架——反正测试本身就是规格的一部分。# tests/test_entries.py from fastapi.testclient import TestClient from app.main import app client TestClient(app) def test_create_entry_success(): resp client.post(/api/v1/entries, json{ user_id: 1, date: 2025-06-09, hours: 8, project: platform }) assert resp.status_code 200 assert resp.json()[status] ok data resp.json()[data] assert data[user_id] 1 assert data[date] 2025-06-09 assert data[hours] 8 def test_create_entry_duplicate_returns_409(): payload { user_id: 1, date: 2025-06-10, hours: 7, project: platform } client.post(/api/v1/entries, jsonpayload) resp client.post(/api/v1/entries, jsonpayload) assert resp.status_code 409 def test_create_entry_weekend_rejected(): resp client.post(/api/v1/entries, json{ user_id: 1, date: 2025-06-14, # 周六 hours: 4, project: platform }) assert resp.status_code 422 assert 周末 in resp.json()[message]写测试时有一个坑要提醒不要只测“正常路径”。AI 特别擅长应付正常情况但业务真正翻车全在异常分支上。所以我每个关键用户故事都至少配一个异常测试。4.4 第四步把规格文档喂给 AI定向生成代码当业务需求、技术约束、测试用例三份文档齐了就可以把它们打包交给 AI 了。我一般会这样组织 prompt请根据以下规格实现一个工时记录 API。 【技术约束】 - Python 3.11 FastAPI SQLAlchemy 2.0 SQLite - 必须使用 app/services、app/repositories、app/api 三层结构 - 统一响应格式见技术规格文档 - 不要修改 tests/ 目录下的测试文件 【业务需求】 粘贴 3.1 的用户故事和验收标准 【接口定义】 粘贴 3.2 的接口清单 【测试用例】 粘贴 4.3 的测试代码 请生成完整的项目文件确保所有测试通过。这里有一个非常关键的技巧让 AI 先不要一次生成全部代码而是先让它给出实现方案你确认后再让它动手。我踩过多次坑AI 一道 mission 就直接生成十几个文件结果数据库模型设计不合理全部返工。现在我会先问它“你打算怎么建模user 和 entry 什么关系重复检测用什么策略”等方案通过了再让它写。4.5 第五步跑测试验证迭代AI 生成完代码后我直接在项目目录跑pip install -r requirements.txt pytest -v第一次跑通常不会全绿。遇到失败用例时我不会盲目让 AI 修而是把失败信息、预期结果、实际结果三段贴回去让它定位问题。比如有次测试失败是因为周末校验逻辑写在了 service 层但前端传参时日期格式不对导致判断失效。这种情况下如果不给上下文AI 就只是盲目地“把断言改成通过”把测试逻辑都给你改了。所以我的 prompt 一定是测试 test_create_entry_weekend_rejected 失败 期望状态码 422实际返回 200。 接口返回数据{status: ok, data: {...}} 我行周末校验的方式是判断 date.isoweekday()但似乎没有被执行。 请检查代码层级找到问题并修复不要修改测试。这样 AI 才会老老实实去修业务代码而不是偷偷改成测试能过的假实现。4.6 第六步补充边界场景做一轮强化评审主流程跑通、测试全绿之后整个项目其实只完成了 70%。剩余 30% 是 AI 很容易忽略的边界场景和安全性事项。我会加一轮强化评审专门检查超长输入、空值、异常类型。比如 hours 传了个负数、date 传了 abc、project 传了 10000 字。权限校验是否真的生效。比如普通用户能不能通过改 user_id 查别人的周报。性能问题。比如没有为 user_id date 建索引导致数据量大时查询变慢。数据一致性。比如重复检测在高并发下会不会失效。这些问题我平时写代码可能也会遗漏但现在我可以把“你自己 review 一下刚才的代码重点检查这几个方面”作为 prompt 发给 AI省去了人工逐行审阅的时间。但请注意AI 的 review 只能作为辅助参考关键逻辑的最终判断还得靠你自己。5. 围绕 SDD 的工具选型与配置建议工具选得好SDD 落地轻松一半。这里我给不同角色的建议5.1 文档管理我用的三件套Markdown Git首选。Markdown 维护成本低Git 负责版本管理文档变更历史一目了然。与代码同仓库尤其适合文档与代码强关联的项目。OpenAPISwagger如果项目涉及大量前后端接口对接直接用 OpenAPI 描述接口Swagger UI 自带可调试的文档页面开发提效非常明显。Cucumber / Gherkin行为驱动如果你的团队重视可执行规格可以把用户故事写成 Given-When-Then 格式工具直接生成测试模板。不过这个上手门槛稍高不是所有团队都适合。5.2 AI 编程工具怎么配合 SDD 使用我用的是 Claude 和 Github Copilot 这类工具配合 SDD 的姿势是在项目根目录放一个AGENTS.md或CLAUDE.md文件把项目结构、编码规范、常用命令写进去。这样 AI 每次读取上下文时都会先看到这份文档不用我反复提醒。对复杂模块我会先让 AI 根据我写的规格生成代码再让另一个 AI 模型做代码评审把两者结果对照权衡。不同模型的侧重点不太一样交叉验证能发现很多单模型忽略的问题。下面是我常用的一个AGENTS.md模板# 项目time-tracker ## 技术栈 - Python 3.11 FastAPI SQLAlchemy 2.0 SQLite ## 目录结构 - app/api: 请求入口只做参数校验和响应封装 - app/services: 业务逻辑 - app/repositories: 数据访问 - tests: 测试目录 ## 编码规范 - 禁止使用 requests统一用 httpx - 所有接口必须返回 {status, data, message} 格式 - 日期统一用 ISO 8601 字符串 - 异常处理统一用 app 自定义异常类禁止裸抛 ## 常用命令 - 启动服务: uvicorn app.main:app --reload - 跑测试: pytest -v有了这个文件AI 每次生成的代码风格都会相对统一我可省心不少。5.3 测试框架不同类型项目怎么配置Python 项目pytest httpxFastAPI 的 TestClient 底层就是 httpx 内存版 SQLite 跑测试速度快接近真实环境。前端项目Vitest React Testing Library配合 MSW 拦截 API 请求。前端 SDD 的规格文档通常是组件交互描述我会让 AI 先根据规格生成组件骨架再补测试。Java 项目JUnit 5 Testcontainers数据库依赖比较多的场景用 Testcontainers 起真实容器虽然慢一点但可信度高。6. 落地 SDD 时的常见问题与避坑指南市面上讲方法论的文章很多但真正落地时你才会碰到各种糟心问题。这里我把自己踩过的坑整理成 QA 形式希望对你有帮助。6.1 文档写到什么程度算“够”这是新手最容易纠结的问题。我的经验是写到“如果明天你休假另一个人只靠文档就能把这个功能做出来”的程度就够了。不需要面面俱到但关键分支、异常场景、约束条件必须写清。如果你是独立开发者没有“另一个人”那就假设“一个月后的你”是另一个人。因为一个月后你的记忆早模糊了文档就是你的外置大脑。6.2 文档和代码不一致怎么办这是 SDD 最大的敌人。代码改了三版文档还在最初的状态那我前面强调的“唯一事实来源”就崩了。我目前的解法有两个文档尽量和代码放在同一个仓库修改代码时必须一起修改文档把这个要求写进 PR 模板或者 CI 检查里。把验收标准揉进测试代码里文档描述与测试挂勾。测试是活的文档描述如果和测试矛盾以测试为准同时回头修正文档。6.3 AI 生成代码后擅自“加戏”怎么办AI 经常会自作主张加一些你没有要求的逻辑比如默认值、自动重试、花哨的日志格式。遇到这种情况不要直接骂 AI 笨这是你技术规格写得不够严。你需要在规格文档里明确“什么可以做”和“什么不能做”比如禁止事项 - 不要在 service 层直接使用 session统一从 repository 层访问 - 不要引入规格文档之外的第三方依赖 - 不要自动创建或修改数据库表结构6.4 AI 就是理解不了某个业务规则怎么办我曾经遇到过一个复杂的状态流转规则AI 反复生成都不对。后来我发现问题不是 AI 笨而是我把规则写得太抽象了。我改成用状态表 示例流程来描述订单状态pending → paid → shipped → completed - pending 状态下允许取消跳转 cancelled - paid 状态下允许退款跳转 refunded - shipped/completed 状态下不允许取消和退款 示例 1. 用户下单 → pending 2. 用户支付 → paid 3. 管理员发货 → shipped 4. 用户确认收货 → completedAI 一下就明白了代码一次通过。这说明写文档时示例比抽象描述有效 10 倍。6.5 测试跑不过但功能看起来是对的怎么办千万不要手工把测试改成通过。先把测试代码和实现逻辑逐行对照确认是测试写错了还是实现写错了。如果是实现错把错误信息 期望结果 实际结果贴回 AI 让它修如果是测试错那就大大方方改测试——毕竟测试也是文档的一部分文档错了当然要改。7. 再说几句大实话从我自己的实践来看SDD 最大的价值不是约束 AI而是约束我自己。我写文档的过程就是逼自己想清楚需求边界和业务逻辑的过程。很多时候在写文档阶段我就发现问题了根本不用等代码跑起来再返工。这一点放在 AI 时代尤其重要因为 AI 把写代码的成本和义务压到了极低如果自己也不动脑子那代码质量就会迅速塌方。如果你刚接触 SDD我建议你先别搞太大从一个小模块开始练手写一份 1 页纸的需求说明、几行关键接口定义、三个测试用例然后让 AI 实现看看整个过程顺不顺畅。跑通一次之后你就知道这套方法论好在哪了。我现在几乎所有的 AI 辅助开发任务都会强制执行 SDD 流程效率反而比之前“想到哪写到哪”高得多。