用 AI 写代码已经快两年了。最早我也是"说一句写一句"的路子——把需求丢给 AI,它生成什么我就用什么,跑不通再追问,追问完再补。功能最后都能写出来,但回头一看代码,总觉得哪里不对劲:字段名对不上、接口路径改了三次、状态字段从枚举变成了布尔值……更崩溃的是,这种混乱会随着功能复杂度的上升指数级增长。
后来接触到了 Spec Coding 这个思路,再后来又发现了 OpenSpec 这个工具,才算找到了一种让 AI 编程真正靠谱的姿势。这篇文章把我这段时间的理解和实践做个整理。
Vibe Coding 的问题:你的意图在"漂移"
先说一个真实的场景。产品要给电商后台加一个"商品下架"接口,下架后对用户不可见,但历史订单里还能看到商品信息。Vibe Coding 的做法是这样的:
1 | > 帮我写一个商品下架接口 |
写到一半发现要处理库存,追问一次。又发现历史订单要展示商品名称,再追问一次。三小时后功能跑通了,但你回头一看:
- 接口路径从最初说的
/products/{id}/status变成了/products/{id}/offline status字段从枚举悄悄变成了布尔值(第 8 次对话时 AI 改的,你没注意)- 订单表多了 3 个冗余字段,2 个实际没用上
available和status两个字段功能重叠,产生了逻辑矛盾
这种问题有个专门的叫法:Intent Drift(意图漂移)。常见有三种模式:
渐进妥协——你说"用枚举表示状态",AI 第 12 次对话时说"用 boolean 更简单",你觉得有道理就同意了。10 次小妥协累积下来,最终方案面目全非。
上下文遗忘——AI 在对话第 3 条说"不冗余存储商品名称",在第 28 条完全忘了,又建议你在订单表加冗余字段。两次建议互相矛盾,AI 自己不知道。
隐式假设——你说"历史订单里还能看到商品信息",AI 理解成"需要在订单表冗余存商品名",其实你的意思是"通过 JOIN 查产品表就够了"。这个假设从来没有被明确说出来,方向直接跑偏。
Spec Coding:先把契约写清楚
Spec Coding 的出发点很直白:AI 是执行者,规格是契约。
还是那个商品下架的需求,Spec Coding 的做法是先写一份 spec:
1 | ## 商品下架功能 Spec |
把这份 spec 甩给 AI,让它按照规格实现。结果是:一次生成,代码和规格完全一致,边界条件都处理了。
两种方式的核心差异:
| 对比项 | Vibe Coding | Spec Coding |
|---|---|---|
| 开始方式 | 直接描述需求开始写 | 先写 spec 再让 AI 实现 |
| AI 的角色 | 共同设计者 | 执行者 |
| 决策记录 | 散落在对话历史里 | 集中在 spec 文件里 |
| 意图偏移 | 每轮对话都可能漂移 | spec 锁定意图 |
| 可复现性 | 同一需求再跑一次结果可能不同 | spec 不变,结果稳定 |
需要说明的是,Vibe Coding 不是不能用。探索方向、做原型验证、改动范围小可以随时推倒重来的场景,Vibe Coding 完全没问题。但当需求已经明确、涉及多个文件和模块、有数据库变更、需要长期维护时,就应该切换到 Spec Coding。
spec.md 怎么写才有效
Spec Coding 的核心就是一份写给 AI 看的规格文档。直接上标准模板:
1 | # [功能名称] Spec |
模板里每个部分都有讲究。背景与目标不是为了让 AI 理解业务,是让它知道优先级——遇到冲突时偏向哪个方向。Out of Scope 比 In Scope 更关键,因为 AI 有填充倾向,它会主动帮你把"看起来相关的"东西一起实现了。明确说不做什么,能拦住 80% 的意外改动。
验收标准是整份 spec 最值钱的部分。 写完之后可以直接让 AI 生成测试用例,也可以用来验证生成的代码是否正确。
拿用户注册功能做个对比。烂的 spec 长这样:
1 | ## 用户注册功能 Spec |
AI 面对这份 spec 只能自己猜。"收集用户信息"是哪些字段?"验证信息"什么规则?失败了怎么办?猜出来的结果和你预期的差 50% 不奇怪。
好的 spec 会把每个字段、每个校验规则、每个错误码、每个边界条件都写清楚,AI 的自由发挥空间就只剩实现细节了。
关于篇幅:单个 CRUD 接口 30-50 行就够,含业务规则的功能(支付、权限之类)80-150 行,跨模块的复杂功能 150-300 行。有人觉得写 spec 费时间——30 分钟写 spec 省掉的是后面 3 小时的返工。
从需求到代码的完整流程
以用户收货地址管理为例,过一遍完整流程。
第一步:用 AI 辅助生成初稿 spec。 把需求描述和模板格式发给 AI,让它生成初稿。注意补充技术栈信息和现有代码结构。
第二步:Review 并修正 spec。 AI 生成的 spec 通常有三个问题:Out of Scope 写得不够、边界条件只覆盖了 60-70%、技术约束缺失。这三个地方需要人工补全。写完后可以反过来让 AI 做一轮 review:“这份规格有没有遗漏的边界条件?”
第三步:分层生成代码。 不要一次性让 AI 把所有代码都写了,而是分层推进:先 Entity + Repository,再 Service,最后 Controller。每层单独 review,降低出错概率,也更容易排查问题。
1 | # 第一轮:数据层 |
第四步:用验收标准验证代码。 把 spec 里的 GIVEN-WHEN-THEN 场景转成测试用例,或者手动用 Postman 逐条验证。
整个流程的时间分配大致是:写 spec 20-30 分钟(最重要的一步),AI 生成代码 7-11 分钟,人工 review 10-15 分钟,验收 10-15 分钟。总计约 1 小时,传统方式通常要 3-5 小时。
Spec 的维护比写更难
Spec Coding 最难的不是怎么写 spec,而是怎么让 spec 保持最新。
代码每天都在变,如果 spec 不同步更新,它就会变成一份过期文档——不但没用,还会误导后来维护的人。最常见的三种漂移:修改代码时忘了改 spec;用 AI 做了临时修改没同步 spec;需求变更覆盖了原有 spec。
维护的核心原则是:Spec 文件和代码文件放在同一个 git 仓库里,一起提交,一起 review。
变更的工作流也应该是"先改 spec,再改代码"——先更新 spec.md,然后把更新后的 spec 给 AI 让它修改代码,最后 spec 和代码一起提交。这个顺序保证了 spec 始终是"意图的来源",代码是 spec 的实现。反过来做,spec 就变成了"代码的注释",失去了驱动意义。
当然也有例外。紧急 bug 修复、文字格式类修改、配置类调整,可以先改代码再补 spec。但不要积累超过 3 天——时间一长就记不清当时的决策了。
对于接口 spec 的变更,建议追加变更记录而不是直接修改,这样能追溯每次变更的原因,排查问题时也方便定位。
OpenSpec:让 Spec Coding 工程化的工具
上面讲的 spec.md 是手写 Markdown 的方式,简单直接,日常开发基本够用。但当项目变大、团队变多,spec 的管理和维护就需要更结构化的方案了。这就是 OpenSpec 要解决的问题。
OpenSpec 是一个规范驱动开发(Spec-Driven Development)框架,专为 AI 编程助手设计。 它的核心理念是四条:流动而非僵化、迭代而非瀑布、简单而非复杂、存量优先。
目录结构
OpenSpec 将项目状态分为两个核心区域:
1 | openspec/ |
openspec/specs/ 是系统当前的真实行为描述,所有已发布的特性都在这里。openspec/changes/ 是进行中的变更,每个变更是一个独立的文件夹。变更开发完成并归档后,其 Delta Spec 会合并入主 Spec,形成新的事实来源。
工作流
OpenSpec 提供了一套斜杠命令(Slash Commands),支持 Claude Code、Cursor、GitHub Copilot 等 20 多种 AI 工具:
| 命令 | 作用 |
|---|---|
/opsx:propose <描述> |
一步创建变更并生成所有规划文档 |
/opsx:explore |
探索模式,思考问题,不写代码 |
/opsx:apply |
按 tasks.md 实现任务 |
/opsx:archive |
归档变更,合并 Delta 到主规范 |
典型的一次功能开发流程:
/opsx:propose "实现用户收货地址管理"——AI 自动创建变更目录,生成 proposal、design、specs、tasks 四份文档- 人工 review 并修正 spec
openspec validate验证格式/opsx:apply让 AI 按任务清单逐步实现/opsx:archive归档变更
spec.md 的格式要求
OpenSpec 对 spec.md 有严格的格式要求,必须使用 Delta Header + Requirement + Scenario 格式:
1 | ## ADDED Requirements |
这个格式有两个好处:一是 Gherkin 格式的 Scenario 天然适合转化为测试用例,每个 Scenario 直接对应一个测试方法;二是结构化的 Requirement 可以被 openspec validate 命令校验,确保文档不只是文本,而是符合定义的结构化数据。
与 OpenAPI 的关系
OpenSpec 和 OpenAPI(Swagger)不是替代关系,而是互补。OpenAPI 定义接口的技术契约(路径、参数、响应格式),OpenSpec 定义的是业务行为约束(做什么、不做什么、边界条件、验收标准)。两者可以配合使用:先用 OpenSpec 定义需求和场景,再用 OpenAPI 定义接口细节。前端用 OpenAPI Mock Server 独立开发,后端用 OpenSpec 驱动 AI 生成代码。
一些实践心得
Spec Coding 不是银弹。 它的价值在于让 AI 的输出可预期、把决策明确记录下来、降低长期维护成本。探索期用 Vibe Coding,明确需求后切换到 Spec Coding——两种方式结合,才是最高效的 AI 编程工作流。
关于工具的选择。 不建议一上来就追求 OpenSpec 的"完整配置"。如果项目不大,一个 specs/ 文件夹 + 每个 feature 一个 markdown 文件 + @ 引用 spec 文件给 AI 就够了。等项目规模上来了再考虑引入 OpenSpec。
spec 写到多细才够? 一个检验标准:把 spec 甩给 AI,不补充任何口头解释,让它直接跑——如果还需要你解释,说明 spec 还有漏洞。
AI 可以帮你写 spec,但不能替代你思考。 AI 生成的 spec 初稿通常能用,但 Out of Scope、边界条件、技术约束这三个地方一定要人工把关。写完后也可以反过来让 AI 做一轮 review,它补漏洞的能力还不错。
先改 spec 再改代码。 这个顺序听起来费事,实际上能避免大量的返工。代码是 spec 的映射,spec 错了代码一定错。把时间花在 spec 上,是最划算的投资。
总结
- AI 编程的核心问题是 Intent Drift——意图在多轮对话中不知不觉地偏移
- Spec Coding 的核心假设:AI 是执行者,spec 是契约
- 一份好的 spec 要写清楚范围(尤其是 Out of Scope)、接口、业务规则、边界条件和验收标准
- 从 spec 到代码应该分层推进:Entity → Service → Controller
- Spec 的维护比编写更重要,核心原则是 spec 和代码一起提交、先改 spec 再改代码
- OpenSpec 是让 Spec Coding 工程化的工具,适合中大型项目,小项目用 markdown 文件就够了
- 两种方式结合使用:探索期 Vibe Coding,正式开发时 Spec Coding