探究Spec Coding与OpenSpec
灿若繁星先生 Lv6

用 AI 写代码已经快两年了。最早我也是"说一句写一句"的路子——把需求丢给 AI,它生成什么我就用什么,跑不通再追问,追问完再补。功能最后都能写出来,但回头一看代码,总觉得哪里不对劲:字段名对不上、接口路径改了三次、状态字段从枚举变成了布尔值……更崩溃的是,这种混乱会随着功能复杂度的上升指数级增长。

后来接触到了 Spec Coding 这个思路,再后来又发现了 OpenSpec 这个工具,才算找到了一种让 AI 编程真正靠谱的姿势。这篇文章把我这段时间的理解和实践做个整理。

Vibe Coding 的问题:你的意图在"漂移"

先说一个真实的场景。产品要给电商后台加一个"商品下架"接口,下架后对用户不可见,但历史订单里还能看到商品信息。Vibe Coding 的做法是这样的:

1
2
> 帮我写一个商品下架接口
AI: 好的,我来写一个 PUT /products/{id}/offline 接口...

写到一半发现要处理库存,追问一次。又发现历史订单要展示商品名称,再追问一次。三小时后功能跑通了,但你回头一看:

  • 接口路径从最初说的 /products/{id}/status 变成了 /products/{id}/offline
  • status 字段从枚举悄悄变成了布尔值(第 8 次对话时 AI 改的,你没注意)
  • 订单表多了 3 个冗余字段,2 个实际没用上
  • availablestatus 两个字段功能重叠,产生了逻辑矛盾

这种问题有个专门的叫法:Intent Drift(意图漂移)。常见有三种模式:

渐进妥协——你说"用枚举表示状态",AI 第 12 次对话时说"用 boolean 更简单",你觉得有道理就同意了。10 次小妥协累积下来,最终方案面目全非。

上下文遗忘——AI 在对话第 3 条说"不冗余存储商品名称",在第 28 条完全忘了,又建议你在订单表加冗余字段。两次建议互相矛盾,AI 自己不知道。

隐式假设——你说"历史订单里还能看到商品信息",AI 理解成"需要在订单表冗余存商品名",其实你的意思是"通过 JOIN 查产品表就够了"。这个假设从来没有被明确说出来,方向直接跑偏。

Spec Coding:先把契约写清楚

Spec Coding 的出发点很直白:AI 是执行者,规格是契约。

还是那个商品下架的需求,Spec Coding 的做法是先写一份 spec:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## 商品下架功能 Spec

### 目标
商品下架后对用户端不可见,但历史数据保持完整。

### 接口
PUT /api/admin/products/{id}/status
Body: { "status": "OFFLINE" }
Response: 204 No Content

### 状态机
ONLINE → OFFLINE(下架,用户端隐藏)
OFFLINE → ONLINE(重新上架)
DELETED(软删除,不可恢复,历史数据保留)

### 数据变更
- products.status 字段更新
- 不清零库存(库存独立管理)
- 不修改订单历史

### 边界条件
- 有未完成订单时不允许下架,返回 409
- 已是 OFFLINE 状态再次下架,返回 200(幂等)

把这份 spec 甩给 AI,让它按照规格实现。结果是:一次生成,代码和规格完全一致,边界条件都处理了。

两种方式的核心差异:

对比项 Vibe Coding Spec Coding
开始方式 直接描述需求开始写 先写 spec 再让 AI 实现
AI 的角色 共同设计者 执行者
决策记录 散落在对话历史里 集中在 spec 文件里
意图偏移 每轮对话都可能漂移 spec 锁定意图
可复现性 同一需求再跑一次结果可能不同 spec 不变,结果稳定

需要说明的是,Vibe Coding 不是不能用。探索方向、做原型验证、改动范围小可以随时推倒重来的场景,Vibe Coding 完全没问题。但当需求已经明确、涉及多个文件和模块、有数据库变更、需要长期维护时,就应该切换到 Spec Coding。

spec​.md 怎么写才有效

Spec Coding 的核心就是一份写给 AI 看的规格文档。直接上标准模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# [功能名称] Spec

## 背景与目标
<!-- 一两句话,说清楚为什么要做,解决什么问题 -->

## 范围
### In Scope(做什么)
- ...

### Out of Scope(不做什么)
- ...

## 接口定义
### [接口名称]
- 方法、路径、请求参数、响应格式、错误码

## 数据模型变更
### 新增表 / 修改表
- 表名、字段说明

## 业务规则
- 规则一:...
- 规则二:...

## 边界条件与异常处理
- 场景一:xxx 时,返回 xxx

## 验收标准(GIVEN-WHEN-THEN)
### 场景一:正常流程
- GIVEN: 前置条件
- WHEN: 触发动作
- THEN: 期望结果

## 技术约束
- 不使用 xxx
- 性能要求:xxx

## 不在本 Spec 范围内的设计决策
<!-- 哪些细节由 AI 自行决定,避免它纠结 -->

模板里每个部分都有讲究。背景与目标不是为了让 AI 理解业务,是让它知道优先级——遇到冲突时偏向哪个方向。Out of Scope 比 In Scope 更关键,因为 AI 有填充倾向,它会主动帮你把"看起来相关的"东西一起实现了。明确说不做什么,能拦住 80% 的意外改动。

验收标准是整份 spec 最值钱的部分。 写完之后可以直接让 AI 生成测试用例,也可以用来验证生成的代码是否正确。

拿用户注册功能做个对比。烂的 spec 长这样:

1
2
3
4
5
6
7
## 用户注册功能 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
2
3
4
5
6
7
8
# 第一轮:数据层
根据 spec 的数据模型部分,生成 UserAddress Entity 和 Repository

# 第二轮:业务层
根据 spec 的业务规则和边界条件,生成 UserAddressService

# 第三轮:接口层
根据 spec 的接口定义部分,生成 UserAddressController

第四步:用验收标准验证代码。 把 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
openspec/
├── config.yaml # 项目配置(技术栈、架构约束等)
├── specs/ # Source of Truth(当前的真实行为描述)
│ ├── catalog-management/
│ │ └── spec.md
│ ├── order-management/
│ │ └── spec.md
│ └── payment/
│ └── spec.md
└── changes/ # Proposed Changes(进行中的变更)
└── add-user-auth/
├── .openspec.yaml # 变更元数据
├── proposal.md # Why & What
├── design.md # How(技术方案)
├── specs/ # Delta 规范修改草案
└── tasks.md # Steps(实施步骤)

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 到主规范

典型的一次功能开发流程:

  1. /opsx:propose "实现用户收货地址管理"——AI 自动创建变更目录,生成 proposal、design、specs、tasks 四份文档
  2. 人工 review 并修正 spec
  3. openspec validate 验证格式
  4. /opsx:apply 让 AI 按任务清单逐步实现
  5. /opsx:archive 归档变更

spec​.md 的格式要求

OpenSpec 对 spec.md 有严格的格式要求,必须使用 Delta Header + Requirement + Scenario 格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## ADDED Requirements

### Requirement: 商品实体定义

系统 SHALL 定义商品实体,包含唯一标识、名称、价格和库存。

**Priority**: P0 (Critical)

**Rationale**: 商品是电商系统的核心实体,是所有交易的基础。

#### Scenario: 创建有效商品

Given 需要创建新商品
When 提供商品信息 { id, name, priceCents, stock }
Then 商品实体创建成功
And id 格式为 prod_xxxx
And priceCents >= 0
And stock >= 0

这个格式有两个好处:一是 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