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 Coding | SDD |
|---|---|---|
| 起点 | 一句话需求 | 一份完整的规格文档 |
| 迭代方式 | 对话式反复修改 | 文档版本化驱动 |
| 代码边界 | 模糊,容易蔓延 | 清晰,受规格约束 |
| 适用场景 | 原型、工具、一次性脚本 | 正式项目、多人协作、长期维护 |
| 可测试性 | 低,改起来全靠手感 | 高,验收标准明确 |
| 在 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", b"hello", "text/plain")}) # 再导入内容相同但文件名不同的文件 resp = client.post("/api/v1/documents/import", files={"file": ("b.txt", b"hello", "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?date=2025-06-09&limit=20 # 查询个人记录 GET /api/v1/reports/weekly?user_id=xxx&week=2025-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", json=payload) resp = client.post("/api/v1/entries", json=payload) 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 负责版本管理,文档变更历史一目了然。与代码同仓库,尤其适合文档与代码强关联的项目。
- OpenAPI(Swagger):如果项目涉及大量前后端接口对接,直接用 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 + httpx(FastAPI 的 TestClient 底层就是 httpx)+ 内存版 SQLite 跑测试,速度快,接近真实环境。
- 前端项目:Vitest + React Testing Library,配合 MSW 拦截 API 请求。前端 SDD 的规格文档通常是组件交互描述,我会让 AI 先根据规格生成组件骨架,再补测试。
- Java 项目:JUnit 5 + Testcontainers,数据库依赖比较多的场景用 Testcontainers 起真实容器,虽然慢一点但可信度高。
6. 落地 SDD 时的常见问题与避坑指南
市面上讲方法论的文章很多,但真正落地时你才会碰到各种糟心问题。这里我把自己踩过的坑整理成 Q&A 形式,希望对你有帮助。
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 流程,效率反而比之前“想到哪写到哪”高得多。