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. 把长期约束转成机器可验证的结果
实现过程中,每遇到一条“以后也必须成立”的要求,就判断它能否机器化:
- 能用测试表达的行为,写入测试。
- 能用 Schema 表达的结构,写入 Schema。
- 能用静态规则或权限表达的红线,加入门禁。
- 无法机器化但需要长期共享的判断,进入 Living Spec。
- 只解释本次过程的信息,保留在提交、PR 或归档中。
这样做的目的是让不同信息回到最适合它的载体,文档仍然保留。代码负责描述实现,测试和 Schema 负责守住可验证约束,长期文档负责保存无法从代码直接读出的业务语义。
4. 任务结束时做一次上下文清理
任务通过验收后,不要把整份任务材料永久塞进项目默认指令。任务结束后的四项处理分别面向实现与约束、历史与默认上下文:
- 确认代码、配置和运行状态已经反映当前实现。
- 确认长期约束已经进入测试、Schema、门禁或 Living Spec。
- 用 Git、PR 或归档保留必要的决策轨迹。
- 将其余任务材料移出默认上下文,只在相关问题出现时读取。
这一步可以直接减少后续 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 保存必须长期共享但无法机器化的定义,历史材料归档后按需读取。 这套组合追求事实明确与偏离可发现,并让上下文始终新鲜,文档数量只需满足这些目标。