Spec Coding

一、Spec Coding和Vibe Coding的区别

对比维度 Spec Coding (规范驱动编程) Vibe Coding (直觉驱动编程)
核心思想 先写规范,再写代码。用结构化的"蓝图"约束AI的生成过程。 用自然语言描述意图,让AI直接生成代码,侧重快速实现想法。
工作流程 线性流程:需求 → 设计 → 任务拆解 → 代码实现 螺旋式迭代:原型生成 → 快速测试 → 优化迭代
核心优势 质量与可控性高 。代码准确、可维护性强,一次通过率可比Vibe Coding高30%-50% 开发速度快 。能快速响应需求变更,在敏捷开发中可将响应速度提升60%
主要劣势 前期投入大。需要投入时间编写和维护规范文档。 可靠性与维护性差 。容易产生逻辑漏洞,项目风险可能增加25%,且代码难以长期维护。
适用场景 企业级复杂项目,如金融、医疗等对准确性、合规性要求高的系统。 快速原型验证、个人项目,或需求频繁变动的探索性任务。
技术依赖 需要Spec文件验证器等多重组件。 主要依赖大语言模型(LLM)本身
提出者/来源 AWS 在其AI IDE产品Kiro中正式提出。 由前OpenAI联合创始人 Andrej Karpathy 于2025年提出。

1.1 Vibe Coding 方式

打开 Cursor,开始对话:

帮我写一个商品下架接口

AI: 好的,我来写一个 PUT /products/{id}/offline 接口...

写了一半发现要处理库存:

下架的时候库存清零吗?

AI: 根据一般业务逻辑,下架时库存应该保留,但标记为不可售...(于是 AI 加了 available = false 字段)

又发现历史订单要展示商品名称:

历史订单里商品名称要怎么处理?

AI: 建议在订单表里冗余存储商品名称...(于是 AI 又改了订单表结构)

三小时后,功能跑通了。但你回头一看:

  • 接口路径是 /products/{id}/offline,不是最开始说的 /products/{id}/status

  • status 字段从 enum 变成了 boolean(第 8 次对话时 AI 悄悄改的)

  • 订单表多了 3 个冗余字段,但有 2 个实际没用上

  • available 和 status 两个字段功能重叠,产生了逻辑矛盾

你需要花额外的时间去理解、修复这些"意外产出"。

1.2 Spec Coding 方式

动手之前,先写一份 spec:

复制代码
## 商品下架功能 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:

复制代码
@spec.md 按照这份规格实现接口,包括 Service、Controller、异常处理

结果:一次生成,代码和规格完全一致,边界条件都处理了。

1.3 Intent Drift 的三种模式

上面的案例里,Vibe Coding 出了问题,本质上都是 Intent Drift(意图漂移)。常见的三种模式:

模式一:渐进妥协

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

模式二:上下文遗忘

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

模式三:隐式假设

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

1.4对比

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

Vibe Coding Spec Coding
开始方式 直接描述需求开始写 先写 spec 再让 AI 实现
AI 的角色 共同设计者 执行者
决策记录 散落在对话历史里 集中在 spec 文件里
意图偏移 每轮对话都可能漂移 spec 锁定意图,AI 只能在范围内执行
可复现性 同一个需求再跑一次,结果可能不同 spec 不变,结果稳定
适合场景 探索、原型、不确定方向时 明确需求、正式功能、需要可维护的代码

不是说 Vibe Coding 不好------探索新功能、验证思路时还是直接聊。Spec Coding 是在需求明确之后,进入"正式实现"阶段时的工作方式。

二、spec.md 结构设计------AI 看得懂的需求文档

Spec Coding 的核心是一份写给 AI 看的规格文档。这节直接给大家一个标准模板,讲清楚每个部分的作用,然后用好 Spec 和烂 Spec 的对比,让你看清楚差距在哪。

2.1标准 spec.md 模板

复制代码
# [功能名称] Spec

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

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

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

## 接口定义
### [接口名称]
- 方法:GET / POST / PUT / DELETE
- 路径:/api/xxx
- 请求参数:...
- 响应格式:...
- 错误码:...

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

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

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

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

### 场景二:异常流程
- GIVEN: ...
- WHEN: ...
- THEN: ...

## 技术约束
- 不使用 xxx(如:不引入新的外部依赖)
- 性能要求:xxx
- 安全要求:xxx

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

每个部分的作用:

背景与目标

告诉 AI 为什么做,不是让它理解业务,是让它知道优先级。如果有冲突,偏向哪个方向。

范围(Out of Scope 最重要)

我的经验:Out of Scope 比 In Scope 更关键。AI 有填充倾向------它会主动帮你把"看起来相关的"东西一起实现了。明确说不做什么,能拦住 80% 的意外改动。

接口定义

越精确越好。路径、方法、参数名、响应格式写到字段级别。模糊的接口定义是意图漂移的主要入口。

业务规则

AI 不了解你的业务上下文,它会用"常见业务逻辑"来填充空白。规则没写进 spec,AI 就自己猜,猜错了你还要回来改。

验收标准(GIVEN-WHEN-THEN)

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

2.2好 Spec vs 烂 Spec

2.2.1烂 Spec(典型的 AI 会乱搞)

复制代码
## 用户注册功能 Spec

实现用户注册,包括:
- 收集用户信息
- 验证信息
- 保存到数据库
- 发送欢迎邮件

这份 spec 的问题:

  • "收集用户信息"------哪些字段?必填吗?

  • "验证信息"------什么验证规则?手机号格式?密码强度?

  • "保存到数据库"------哪张表?有没有唯一约束?

  • "发送欢迎邮件"------同步还是异步?失败了怎么办?

AI 面对这份 spec 只能自己猜。猜出来的结果和你预期的可能差 50%。

2.2.2好 Spec(AI 能精确执行)

复制代码
## 用户注册功能 Spec

## 背景与目标
新用户通过手机号注册账号,注册成功后可以登录系统。

## Out of Scope
- 第三方登录(微信、支付宝)不在本次范围
- 邀请码注册机制不在本次范围
- 短信验证码暂不接入(后续迭代)

## 接口定义
### POST /api/auth/register
请求体:
{
  "phone": "13800138000",    // 必填,11位手机号
  "password": "Abc12345!",   // 必填,8-20位,含大小写字母和数字
  "nickname": "张三"          // 选填,1-20个字符,默认为手机号后4位
}

响应(201 Created):
{
  "userId": "uuid",
  "phone": "138****8000",   // 脱敏显示
  "nickname": "张三",
  "createdAt": "2025-01-15T10:30:00Z"
}

错误码:
- 400:参数校验失败(附带具体字段错误信息)
- 409:手机号已注册

## 数据模型
users 表新增字段:无(使用现有表结构)
现有 users 表:id(uuid), phone(unique), password_hash, nickname, created_at, status

## 业务规则
1. 密码存储:bcrypt 加密,不存明文
2. 手机号唯一:重复注册返回 409,不透露已注册信息
3. nickname 为空时:自动设置为手机号后4位,格式 "用户xxxx"

## 边界条件
- phone 格式非法:400,message: "手机号格式不正确"
- password 不符合强度要求:400,message: "密码需包含大小写字母和数字,8-20位"
- phone 已注册:409,message: "该手机号已注册"
- nickname 超过20字符:400,message: "昵称不能超过20个字符"

## 验收标准
### 场景一:正常注册
GIVEN 手机号 13800138000 未注册
WHEN POST /api/auth/register { phone, password, nickname }
THEN 返回 201,userId 不为空,phone 脱敏,users 表新增一条记录

### 场景二:手机号重复
GIVEN 手机号 13800138000 已注册
WHEN POST /api/auth/register { 同一手机号 }
THEN 返回 409,不创建新记录

### 场景三:密码不符合要求
GIVEN 任意手机号
WHEN POST /api/auth/register { password: "123456" }
THEN 返回 400,message 说明密码强度要求

## 技术约束
- 不引入新的依赖
- 密码强度校验用正则,不用第三方库
- 不发送短信(暂不需要验证码)

两份 spec 的差距,不只是"详细程度",而是 AI 的自由发挥空间:第一份给了 AI 几十个决策点,第二份只剩实现细节。

三、从 PRD 到 Spec:系统转化流程与实战案例

手头有 PRD(产品需求文档),怎么把它变成上一节说的那种 spec?

这节给出一套系统的转化流程,带一个完整的电商商品管理功能作为实战案例。

3.1 PRD 里哪些信息有用

PRD 是写给人看的,spec 是写给 AI 看的。同样是需求文档,关注点完全不同:

PRD 的内容 对 AI 的价值 处理方式
业务背景、用户故事 低------AI 不需要理解业务上下文 压缩成一两句目标说明
功能列表 中------确认范围 直接转成 In Scope / Out of Scope
交互原型图 中------提取字段和流程 翻译成接口参数和业务规则
异常流程 高------直接转成边界条件 逐条保留
验收标准 高------直接转成 GIVEN-WHEN-THEN 直接保留并格式化
上线时间、责任人 无用 丢掉
UI 设计稿细节 无用(后端 spec) 丢掉

核心转化原则:把"产品想要什么"转成"AI 需要实现什么"。

3.2转化流程

复制代码
PRD
  ↓
① 提取功能范围(In/Out Scope)
  ↓
② 提取接口列表(路径、方法、参数)
  ↓
③ 提取业务规则(每条规则一句话)
  ↓
④ 提取边界条件(异常流程逐条列出)
  ↓
⑤ 补充技术约束(PRD 里没有但开发知道的)
  ↓
⑥ 写验收标准(GIVEN-WHEN-THEN)
  ↓
Spec

3.3实战:商品管理功能

3.3.1原始 PRD(节选)

商品管理功能

背景:运营人员需要在后台管理平台上管理商品信息。

功能需求:

  1. 商品列表:支持按名称、分类、状态筛选,分页展示

  2. 新增商品:填写商品基本信息后上架

  3. 编辑商品:修改已有商品信息

  4. 商品状态管理:上架、下架、删除

字段要求:

  • 商品名称(必填,最长50字)

  • 分类(必填,从预设分类选择)

  • 价格(必填,大于0)

  • 库存(必填,整数)

  • 描述(选填)

  • 主图(选填,URL格式)

业务规则:

  • 上架商品不能直接删除,需先下架

  • 下架商品可以重新上架

  • 删除后不可恢复(软删除,不物理删除)

验收标准:

  • 运营可以在5秒内搜索到指定商品

  • 价格修改后用户端立即生效

3.3.2转化过程

步骤一:确认范围

PRD 说"商品管理",但没说:

  • 商品图片上传怎么处理(是传 URL 还是上传文件)?→ 先问产品,这里假设传 URL

  • 分类是固定的还是动态的?→ 先问产品,这里假设是预设分类枚举

  • 有没有批量操作?→ PRD 没提,Out of Scope

步骤二:用 AI 辅助补全 spec

把 PRD 原文发给 AI,用以下 Prompt:

你是一个有经验的 Java 后端工程师。

以下是产品需求文档,请帮我转化为后端 API 的 spec 文档。

要求:

  1. 提取所有接口(方法+路径+参数+响应格式+错误码)

  2. 提取业务规则,每条一句话

  3. 提取边界条件和异常处理

  4. 写出 GIVEN-WHEN-THEN 验收场景(至少覆盖正常流程和主要异常)

  5. 标注 Out of Scope(没有明确说明的功能)

技术栈:Spring Boot 3.x + JPA + MySQL

现有代码结构:标准 Controller-Service-Repository 三层

PRD 内容如下:

粘贴 PRD

3.3.3转化后的 Spec

复制代码
# 商品管理后台接口 Spec

## 背景与目标
提供商品的增删改查接口,供运营后台使用。

## In Scope
- 商品 CRUD
- 商品状态管理(上架/下架/软删除)
- 商品列表查询(分页+筛选)

## Out of Scope
- 商品图片上传(本次只支持传图片 URL,不做文件上传)
- 商品分类管理(分类枚举在代码里写死,不做 CRUD)
- 批量操作
- 商品导入/导出

---

## 接口定义

### 1. 商品列表
GET /api/admin/products

Query 参数:
- name: String(选填,模糊匹配商品名称)
- category: String(选填,精确匹配分类枚举)
- status: String(选填,ONLINE/OFFLINE/DELETED,默认不含 DELETED)
- page: int(默认 0)
- size: int(默认 20,最大 100)

响应 200:
{
  "content": [
    {
      "id": 1,
      "name": "iPhone 16",
      "category": "ELECTRONICS",
      "price": 5999.00,
      "stock": 100,
      "status": "ONLINE",
      "createdAt": "2025-01-15T10:00:00Z"
    }
  ],
  "totalElements": 150,
  "totalPages": 8,
  "page": 0,
  "size": 20
}

### 2. 新增商品
POST /api/admin/products

请求体:
{
  "name": "iPhone 16",       // 必填,1-50字符
  "category": "ELECTRONICS", // 必填,枚举:ELECTRONICS/CLOTHING/FOOD/OTHER
  "price": 5999.00,          // 必填,> 0,最多2位小数
  "stock": 100,              // 必填,>= 0 的整数
  "description": "...",      // 选填,最长500字符
  "imageUrl": "https://..."  // 选填,URL格式
}

响应 201:返回创建的商品完整信息(含 id、createdAt)

错误:
- 400:参数校验失败
- 409:同名同分类商品已存在

### 3. 修改商品
PUT /api/admin/products/{id}

请求体:同新增(所有字段选填,只更新传入的字段)
响应 200:返回更新后的完整商品信息

错误:
- 400:参数校验失败
- 404:商品不存在
- 409:修改名称后与现有商品重复

### 4. 修改商品状态
PUT /api/admin/products/{id}/status

请求体:{ "status": "ONLINE" / "OFFLINE" / "DELETED" }
响应 200:{ "id": 1, "status": "OFFLINE" }

错误:
- 404:商品不存在
- 409:ONLINE 状态的商品不允许直接 DELETED
- 400:状态值非法

---

## 数据模型

products 表:
- id: BIGINT AUTO_INCREMENT PK
- name: VARCHAR(50) NOT NULL
- category: VARCHAR(20) NOT NULL(枚举字符串)
- price: DECIMAL(10,2) NOT NULL
- stock: INT NOT NULL DEFAULT 0
- description: VARCHAR(500)
- image_url: VARCHAR(500)
- status: VARCHAR(10) NOT NULL DEFAULT 'ONLINE'(ONLINE/OFFLINE/DELETED)
- created_at: DATETIME NOT NULL
- updated_at: DATETIME NOT NULL
- UNIQUE KEY uk_name_category(name, category)

---

## 业务规则
1. ONLINE 状态商品不能直接设为 DELETED,必须先 OFFLINE
2. OFFLINE 状态可以改为 ONLINE 或 DELETED
3. DELETED 状态不可变更(终态)
4. 列表查询默认不返回 DELETED 的商品,除非 status=DELETED 明确传入
5. 库存可以为 0(允许缺货状态)

---

## 边界条件
- price = 0:400,price 必须大于 0
- stock < 0:400,stock 不能为负数
- name 超过 50 字:400
- imageUrl 非 URL 格式:400
- ONLINE 商品尝试删除:409,"请先下架商品再删除"

---

## 验收标准

### 场景一:正常新增并上架
GIVEN 数据库中不存在同名同分类商品
WHEN POST /api/admin/products { name, category, price, stock }
THEN 返回 201,商品 status=ONLINE,数据库新增记录

### 场景二:重复商品
GIVEN 已存在 name="iPhone 16", category="ELECTRONICS" 的商品
WHEN POST /api/admin/products { 相同 name 和 category }
THEN 返回 409

### 场景三:ONLINE 商品直接删除
GIVEN 商品 id=1,status=ONLINE
WHEN PUT /api/admin/products/1/status { "status": "DELETED" }
THEN 返回 409,数据库状态不变

### 场景四:下架后删除(正常流程)
GIVEN 商品 id=1,status=ONLINE
WHEN PUT .../status { "status": "OFFLINE" },再 PUT .../status { "status": "DELETED" }
THEN 两次均返回 200,最终状态为 DELETED

### 场景五:列表不返回已删除
GIVEN 存在 status=DELETED 的商品
WHEN GET /api/admin/products(不传 status 参数)
THEN 返回结果中不包含 DELETED 商品

---

## 技术约束
- 不引入新的依赖
- 分页用 Spring Data JPA Pageable,不手写分页 SQL
- 模糊查询用 JPA Specification,不用 native query

四、让 AI 做 Spec Review

写完 spec 之后,在动手实现之前,先让 AI 做一轮检查:

复制代码
请 review 以下 spec,指出:
1. 遗漏的边界条件
2. 业务规则是否有冲突
3. 接口设计是否有明显问题
4. 有没有隐含的假设没有明确写出来

[粘贴 spec 内容]

我的经验:AI 的 review 通常能发现 2-5 个你没想到的边界情况,值得跑一遍。

五、OpenSpec 工具链------Spec Coding 的标准化框架

5.1 OpenAPI------接口 Spec 的事实标准

OpenAPI(原 Swagger)是目前最成熟的接口规格标准,用 YAML 或 JSON 描述 REST API。

一份 OpenAPI spec 长这样:

复制代码
openapi: 3.0.3
info:
  title: 收货地址 API
  version: 1.0.0

paths:
  /api/user/addresses:
    post:
      summary: 新增地址
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAddressRequest'
      responses:
        '201':
          description: 创建成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Address'
        '400':
          description: 参数校验失败
        '422':
          description: 地址数量已达上限

components:
  schemas:
    CreateAddressRequest:
      type: object
      required: [name, phone, province, city, district, detail]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 20
          description: 收件人姓名
        phone:
          type: string
          pattern: '^\d{11}$'
          description: 手机号
        province:
          type: string
          maxLength: 20
        city:
          type: string
          maxLength: 20
        district:
          type: string
          maxLength: 20
        detail:
          type: string
          maxLength: 100
        isDefault:
          type: boolean
          default: false

OpenAPI 的优势:

  • 有配套的 Swagger UI,可以直接在浏览器里查看和测试接口

  • 有代码生成工具(OpenAPI Generator),可以自动生成 Controller 骨架、客户端 SDK

  • 前后端可以先对齐接口 spec,再同步开发(前端用 mock server,后端实现真实逻辑)

局限:

  • 格式严格,写起来比 Markdown 啰嗦得多

  • 只能描述接口,描述不了业务规则、状态机、数据模型

  • 对 AI 生成代码的指导效果和 Markdown Spec 差不多,但写作成本更高

我的建议:

  • 团队项目、需要前后端对齐接口的场景:用 OpenAPI

  • 个人项目或快速迭代:用 Markdown Spec,更快

5.2 Database Schema 工具

数据模型的标准化工具主要有:

DBML(Database Markup Language)

dbml.dbdiagram.io 开发的简洁数据库描述语言:

复制代码
Table users {
  id bigint [pk, increment]
  phone varchar(11) [not null, unique]
  nickname varchar(20) [not null]
  created_at datetime [not null]
}

Table user_addresses {
  id bigint [pk, increment]
  user_id bigint [not null, ref: > users.id]
  name varchar(20) [not null]
  phone varchar(11) [not null]
  province varchar(20) [not null]
  city varchar(20) [not null]
  district varchar(20) [not null]
  detail varchar(100) [not null]
  is_default tinyint(1) [not null, default: 0]
  created_at datetime [not null]
  updated_at datetime [not null]

  indexes {
    user_id
    (user_id, is_default)
  }
}

DBML 可以在 dbdiagram.io 里在线渲染成 ER 图,也可以导出建表 SQL。

Liquibase / Flyway

用于管理数据库版本迁移,不是"写 Spec"的工具,而是"执行 Spec"的工具。典型用法:

  1. 写好数据模型 Spec(Markdown 或 DBML)

  2. 让 AI 生成 Flyway 迁移脚本

  3. 把迁移脚本提交到仓库,Flyway 自动执行

5.3把工具链串起来

我目前用的工作流:

复制代码
需求/PRD
    ↓
① Markdown spec.md(手写,AI 辅助)
    ↓
② AI Review spec(检查漏洞)
    ↓
③ 分层生成代码(Entity → Service → Controller)
    ↓
④ Flyway 迁移脚本(AI 从数据模型 spec 生成)
    ↓
⑤ 提交代码 + spec.md 一起进仓库

大型团队的工作流(可选升级版):

复制代码
需求/PRD
    ↓
① Markdown spec.md(手写,AI 辅助)
    ↓
② 前后端对齐接口:导出为 OpenAPI YAML
    ↓
③ 前端用 OpenAPI Mock Server 独立开发
   后端用 Markdown Spec 驱动 AI 生成代码
    ↓
④ DBML → ER 图(给产品/架构 review)
   Flyway 脚本(给 DBA review)
    ↓
⑤ 联调 + 验收

5.4 AI 辅助工具链

除了 Spec 格式标准,还有一些 AI 原生工具专门为 Spec Coding 场景设计:

Cursor 的 @ 引用

在 Cursor 里直接 @spec.md 引用规格文件,AI 会把文件内容注入上下文。这是目前最简单高效的"把 Spec 喂给 AI"的方式。

复制代码
@spec/user-address.md 按照 spec 实现 Service 层,
参考 @src/service/UserService.java 的代码风格

Claude Projects

可以把 spec 文件上传到 Claude Project,作为持久上下文。适合长时间在同一个项目上工作的场景------不用每次都粘贴 spec。

GitHub Copilot Workspace

GitHub 推出的 AI 工作流工具,支持从 issue 描述到代码的一键生成。目前还在 beta 阶段。

5.5现阶段的实用建议

我不建议一上来就追求工具链的"完整性"。工具是为效率服务的,过度配置反而降低效率。

从这里开始就够了:

  1. 项目根目录建 specs/ 文件夹

  2. 每个功能一个 specs/功能名.md

  3. 用 Cursor 的 @ 引用 spec 文件

  4. Spec 和代码一起提交 git

等项目规模变大了,再考虑引入 OpenAPI、DBML 这类工具。

复制代码
my-project/
├── specs/
│   ├── user-address.md
│   ├── order.md
│   └── payment.md
├── src/
│   └── ...
└── db/
    └── migrations/
        ├── V1__create_users.sql
        └── V2__create_orders.sql

从手写 Markdown Spec 起步,到按需引入 OpenAPI、DBML、Flyway,再到利用 AI 工具高效协作------Spec Coding 的工具链始终围绕一个核心:用最合适的成本,把需求精准地翻译给 AI 和团队。