Spec Coding - 在 AI 编程中管理规格、约束与事实来源

AI 能在短时间内生成大量代码,研发的难点会从“怎样写得更快”转向“怎样让实现始终符合真实意图”。本文说明 Living Spec 和 Delta Spec,以及 Executable Spec 和 Ephemeral Spec 的取舍,并给出一套 Spec Coding 实践,适合快速迭代项目。

本文把 Spec Coding 理解为一种工作方式:在编码前把目标与边界、可观察行为以及验收条件显式化,让人和 AI 先对“要解决什么问题”达成一致,再由计划、实现和验证逐步收敛。它适用于不同工具,规格文档也可以按项目需要保留。

Spec Coding 解决的是实现正确性

只给 AI 一段自然语言需求,也能很快得到可以运行的代码,但这类代码仍可能实现错误。需求中没有说清楚的部分,模型只能根据当前上下文补全;上下文越陈旧、材料越矛盾,补全结果就越不可靠。

一份任务 Spec 是否完整,要看以下问题能否得到回答:

  • 为什么要做这次改动,预期结果是什么。
  • 哪些行为需要改变,哪些内容明确不在本次范围内。
  • 哪些兼容性、安全性和性能约束必须满足。
  • 通过什么测试、接口响应或页面结果判断任务完成。

Spec Coding 的直接价值是把隐含判断变成可审查的输入。多读一份文档本身不会产生这种价值。代码生成之前,人审查的是目标和边界;代码生成之后,测试和门禁检查实现有没有偏离。这样可以把人的注意力集中在少数需要判断的位置,避免平均分配给每一行生成代码。

GitHub 的 Spec Kit 对 Spec-Driven Development(SDD)给出了更强的定义:Spec 既提供辅助说明,也是实现的起点,并通过多阶段细化驱动后续计划、任务和代码。本文讨论的 Spec Coding 范围更宽,也包括一种轻量做法:Spec 只在单次任务中使用,最终由代码和可执行约束承接事实。

先确定 Spec 与代码的事实关系

Spec 是否需要长期维护,取决于团队把什么当作事实来源。三种关系没有被区分,会让关于 SDD 的分歧表现为流程之争。

关系 核心含义 适合的内容 主要风险
Spec → Code 先修改 Spec,再让代码跟随Spec 变化 产品行为与跨团队契约,以及合规要求 Spec 更新不及时会阻塞实现或产生双重事实
Code → Spec 当前实现先由代码确认,Spec 提炼并约束后续演进 既有项目改造与技术债治理,以及经验沉淀 只回写文字而不验证,仍会继续漂移
Spec = Source Spec 可以直接生成或校验实现 API Schema 与数据库迁移,以及配置模型和协议定义 表达能力受限,复杂业务判断难以完全声明化

这三种关系可以在同一项目中组合使用。同一个系统中,OpenAPI 文件可以是接口的生成来源,测试可以是行为约束,代码和生产配置描述当前实现,产品规则则由需求系统或长期 Spec 管理。每类信息只能有一个权威来源,其他位置保存副本、摘要或链接。

四种 Spec 生命周期

确定事实来源以后,还要决定 Spec 在任务结束后怎样处理。常见做法可以分成四类。

类型 处理方式 优点 代价
Living Spec 长期维护,随功能一起更新 当前行为集中、便于协作和审计 维护成本高,遗漏更新就会产生漂移
Delta Spec 只描述本次变化,完成后合并到主 Spec 并归档 评审范围小,适合既有项目迭代 需要可靠的合并、冲突处理和归档流程
Executable Spec 把要求转成测试与 Schema,以及静态检查或 CI 门禁 偏离时可以自动失败,约束更可信 只能覆盖可机器判断的部分,门禁本身也要维护
Ephemeral Spec 只服务当前任务,完成后退出默认上下文 上下文更少,过期材料不再持续干扰 AI 追溯时需要查历史,不能独自承担长期协作和审计

Living Spec:持续维护当前定义

Living Spec 把规格当作长期资产。功能发生变化时,团队同时修改 Spec、代码和验证材料,使下一位工程师或 Agent 能从 Spec 开始理解系统。

它适合长期产品定义和稳定领域规则,也适用于多人或跨团队协作,以及需要完整审计轨迹的项目。第一次写完整不会解决此后的同步更新问题。Living Spec 的难点是每一次变更都不能漏掉同步更新。没有负责人、评审门禁和更新习惯的 Living Spec,很容易从权威说明退化成过期文档。

Delta Spec:只评审本次差异

Delta Spec 先记录“新增和修改了什么,以及删除了什么”,不必每次都重写完整规格。变更完成后,再把差异合并进主 Spec,并将本次变更材料归档。

这种方式适合 Brownfield 项目,因为评审者只需关注变化部分。OpenSpec 的默认流程就是一个典型实现:规格被定义为行为契约,单次变更以 Delta 表达,归档时再合并进主规格。它降低了单次评审成本,但仍然要求主 Spec 被持续维护。

Executable Spec:把文字变成失败条件

可以自动判断的长期约束更适合转成可执行材料,不应只留在自然语言里:

  • 用户可观察行为转成单元测试、集成测试或端到端测试。
  • API 输入输出转成 OpenAPI、JSON Schema 或契约测试。
  • 数据约束转成数据库约束、迁移校验或数据质量检查。
  • 安全和工程红线转成静态检查、权限策略、Hook 或 CI 门禁。

例如,“未验证邮箱的用户不能接收营销通知”如果只写在 spec.md 中,代码变更后很难及时发现偏离;将它写成覆盖已验证和未验证用户的自动化测试,约束失效时构建会直接失败。

Executable Spec 也可能出错。错误的测试会稳定地验证错误行为,过度绑定内部实现的测试还会阻碍重构。因此,可执行约束应优先描述外部可观察结果,避免固定类名、函数名和内部调用顺序。

Ephemeral Spec:控制默认上下文

Ephemeral Spec 只为当前任务提供充分上下文。任务开始时,它应完整说明目标与边界、约束以及验收条件;任务完成后,它不再默认出现在后续每个会话中。

“退出默认上下文”仍然可以保留材料。Git 提交和 PR,以及任务记录或归档目录仍然可以保留它,需要追溯时再读取。它只减少每次都加载的材料,历史证据仍然保留。

这种方式适合变化快、任务边界清楚且重视上下文新鲜度的项目。长期产品定义和跨团队契约,以及合规审计或必须保留完整决策链的场景,仍然需要 Living Spec 和正式记录系统。

一套轻量的组合实践

快速变化的普通工程项目可以把 Ephemeral Spec 和 Executable Spec 组合起来:临时 Spec 负责让当前任务做对,可执行约束负责让未来修改不破坏关键规则,其余历史按需读取。

需求与讨论
    ↓
任务 Spec(当前会话默认加载)
    ↓
计划 → 实现 → 验证
             ├─ 长期行为 → 测试 / Schema / CI 门禁
             ├─ 长期说明 → Living Spec 或权威业务系统
             ├─ 当前状态 → 代码 / 配置 / 运行状态
             └─ 变更历史 → Git / PR / 归档

1. 用任务 Spec 固定目标和边界

任务开始时先建立一份足够短、但能够独立验收的 Spec。下面的模板用于固定目标和边界:

# 任务名称

## 背景
说明当前问题、受影响对象和为什么现在要处理。

## 目标
- 描述本次必须得到的结果。

## 非目标
- 列出本次明确不处理的内容。

## 可观察行为
- Given:前置状态。
- When:用户或系统执行的动作。
- Then:外部能够观察到的结果。

## 约束
- 兼容性、安全性、性能和权限边界。
- 不能修改的模块或公开接口。

## 验收条件
- [ ] 自动化测试通过。
- [ ] 构建和静态检查通过。
- [ ] 关键接口、页面或运行状态符合预期。

## 未决问题
- 仍需由负责人判断的问题;未确认前不进入高风险实现。

模板的重点是让模糊需求暴露出来,栏目按任务需要填写。小改动可以压缩成几行,高风险或跨系统改动则应补充数据流、回滚方案和责任人。

2. 在实现前声明事实来源

如果任务同时涉及需求系统和 Markdown,也涉及代码、配置及测试,应在 Spec 中写清优先级。例如:

信息类型 权威来源 何时读取
当前接口结构 OpenAPI 文件 设计与验证阶段
当前运行配置 部署配置和目标环境 排查与发布阶段
用户行为规则 产品规格系统 需求确认阶段
本次改动范围 任务 Spec 当前任务全程
历史决策 Git、PR 或归档 出现疑问时按需读取

如果两份材料冲突,Agent 不应该自行挑选看起来更完整的一份。流程需要规定:是回到权威系统确认,还是暂停实现并交给负责人判断。

3. 把长期约束转成机器可验证的结果

实现过程中,每遇到一条“以后也必须成立”的要求,就判断它能否机器化:

  1. 能用测试表达的行为,写入测试。
  2. 能用 Schema 表达的结构,写入 Schema。
  3. 能用静态规则或权限表达的红线,加入门禁。
  4. 无法机器化但需要长期共享的判断,进入 Living Spec。
  5. 只解释本次过程的信息,保留在提交、PR 或归档中。

这样做的目的是让不同信息回到最适合它的载体,文档仍然保留。代码负责描述实现,测试和 Schema 负责守住可验证约束,长期文档负责保存无法从代码直接读出的业务语义。

4. 任务结束时做一次上下文清理

任务通过验收后,不要把整份任务材料永久塞进项目默认指令。任务结束后的四项处理分别面向实现与约束、历史与默认上下文:

  1. 确认代码、配置和运行状态已经反映当前实现。
  2. 确认长期约束已经进入测试、Schema、门禁或 Living Spec。
  3. 用 Git、PR 或归档保留必要的决策轨迹。
  4. 将其余任务材料移出默认上下文,只在相关问题出现时读取。

这一步可以直接减少后续 Agent 的判断负担。完整但过期的材料比缺失材料更危险,因为它看起来可信,却会和当前代码一起制造事实冲突。

如何选择 Spec 工具

团队决定什么才是权威事实,工具只负责执行流程。选择之前,应先看项目需要哪种 Spec 生命周期。

  • Spec Kit:适合希望从规格开始,依次生成计划、任务和实现的完整 SDD 流程。官方核心流程是 Spec → Plan → Tasks → Implement。
  • OpenSpec:适合既有项目中的行为规格和增量变更。它强调轻量、可验证的行为契约,并在归档时把 Delta 合并进主 Spec。
  • Superpowers:更偏向编码 Agent 的工作纪律,通过可组合 Skills 覆盖需求澄清、计划、TDD、调试、审查和交付。
  • Matt Skill:团队只在某些环节反复遇到问题,可以只固化需求澄清、复杂排查、任务拆分或交接方法,不必一次引入完整流程。

流程越完整,稳定性和可审计性通常越强,但维护成本与上下文占用也会增加。个人项目、小范围改动和成熟团队各自采用适合自身的模板;能够覆盖当前风险的最轻流程,往往更容易长期执行。

如何验证 Spec Coding 是否有效

不能以“已经生成了 spec.md”判断流程有效。一次任务结束时,以下问题都要经过复核:

  • 只读任务 Spec,能否说清楚为什么改、改什么和不改什么?
  • 每一类长期信息是否只有一个权威来源?
  • Spec 与代码、测试或配置冲突时,是否有明确处理规则?
  • 验收条件能否通过命令、测试、接口响应或页面结果验证?
  • 关键长期约束是否已经进入测试、Schema、门禁或 Living Spec?
  • 本次任务材料是否在结束后退出默认上下文,同时仍可追溯?
  • 对高风险、跨团队或合规内容,是否保留了负责人审核和正式记录?

还可以持续观察三个结果指标:需求返工次数与 Agent 重复犯错是否减少,以及任务完成后多久能获得可信的验证结果。如果文档数量持续增加,但这三个指标没有改善,说明团队可能只是在增加 Spec,而没有改善事实管理。

总结

Spec Coding 的关键是在正确的时间把正确的信息交给人和 AI,Markdown 只是其中一种载体。任务开始时,Spec 要足够完整;实现过程中,长期约束要尽量变成可执行检查;任务结束后,当前实现交给代码和配置,历史交给版本控制,其余材料退出默认上下文。

对快速迭代项目,一个实用的默认组合是:Ephemeral Spec 承载当前任务,Executable Spec 保留长期约束,Living Spec 保存必须长期共享但无法机器化的定义,历史材料归档后按需读取。 这套组合追求事实明确与偏离可发现,并让上下文始终新鲜,文档数量只需满足这些目标。

参考