目录

OpenCode & OpenSpec:SDD 实践

作为一名程序员,还是会被近几年 AI 编程领域的突飞猛进吓一跳,印象中,有这么几个明显的阶段

  • LLM for Tab 补全:代码主要是程序员编写,LLM 只是会预测下一段要放什么。典型代表比如 TablineCopilot
  • 氛围编程(Vibe Coding):程序员不再逐行写代码,而是用自然语言描述需求,让 LLM 帮你做跨文件修改。典型代表比如 CursorClaude
  • Spec-Driven Development (SDD):氛围编程有许多毛病,比如上下文漂移、软件架构不稳定等,这导致它很难在生产环境中落地。因此社区乃至工业界都开始探寻超越 Prompt 工程的方法去让 LLM 可以长时间稳定生成可用的代码,而不是一堆 AI 垃圾(AI Slop)。SDD 就是目前看来很有希望的一个方向
Warning

事实上,在编程领域以外也存在这样的失败模式,但我们这里只关注 AI 编程领域

首先,我们可以思考一下氛围编程存在什么问题?从使用流程上看,氛围编程是这样子的

flowchart LR
    prompting --> gen("code generation (or regenerate)") --> fix
    fix --> gen
    fix --> dot("...")
    dot --> final("Success or failure")

程序员会用自然语言描述它的修改需求(对应 prompting 阶段),这个描述通常是模糊的,然后 LLM 根据自然语言输入理解用户意图并生成修改计划(对应 code generation (or regenerate) 阶段)。在大多数情况下(起码从我个人的体验上来说),LLM 第一版生成的代码很难精准捕获我的意图,此时我们会向 LLM 提出修改建议(对应 fix 阶段),此时 LLM 会重新按照指令生成代码。如此循环往复,直到得到一个满意的结果,或者发现还不如自己写了算了!就我个人使用经验而言,我发现这个工作流程下会经常出现如下的现象🤕️

  • 为什么 LLM 生成的代码还在使用旧的 Python Type Hints(e.g., 使用 Dict 而不是直接用 dict)?
  • 为什么我需要重复向 LLM 解释一些关键约束?
  • 为什么之前有效的部分代码 LLM 不用,而是大幅度修改?

我最近看到一篇文章,它对氛围编程的失败模式做了很好的总结1

  • 上下文丢失(Context Loss):LLM 的注意力会优先分配给最新的用户输入,早期轮次确认的技术细节很可能在后续被忘记(即使他们还在上下文窗口里)
  • 假设漂移(Assumption Drift):对于用户没有明确指定的约束,不同的 LLM 会有自身的偏好(取决于训练数据),而这些隐式的偏好很可能不是你想要的
  • 模式违反(Pattern Violation):当假设漂移不断累计,最后你得到的结果已经完全偏移,它从各个方面都无法满足你的需求

更让人难过的是,这几个失败模式存在复合效应:上下文丢失导致用户需要重复声明约束,但用户的约束通常很模糊,那么假设漂移现象也在逐渐增强,最后导致完全的模式违反1

现在我们清楚了氛围编程的问题,那 SDD 是如何解决的?SDD 的全称叫做规约驱动开发(Spec-Driven Development),使用 SDD 的时候,写代码先从写规约(Spec)开始,然后生成详细的修改计划(Plan),将修改计划分解成多个任务(Task),最后才开始写代码(Code),如下所示

flowchart LR
    spec --> plan --> tasks --> code

纵观氛围编程的失败模式,我们不难发现氛围编程失败的原因在于我们没有清晰表达我们的需求,自然语言输入经常是模糊的。那么解决方案就是清晰表达我们的意图。幸运的是,在编程领域,我们已经有了相关的东西,比如软件架构、需求文档等,这些都遵循了一定的规范,可以在一定程度上消除自然语言的歧义

根据程序员跟 Spec/Code 的关系,可以将 SDD 分成 3 个阶段,下面的表格清晰展示了 3 者直接的区别2

维度 Spec-first Spec-anchored Spec-as-source
🔧 维护产物 仅代码 规约 + 代码 仅规约
📝 规约生命周期 实现后丢弃 随代码持续更新 永久维护
💰 维护成本 👑 零 ❌ 双重维护负担 ❌ 需强测试覆盖
🔄 确定性 — 不适用 ✅ 规约与代码手动对齐 ❌ 相同规约产生不同代码

一句话总结来说

  • Spec-first:先写 Spec 然后开始写代码
  • Spec-anchored:先写 Spec 然后写代码,Spec 需要不断维护,每次写代码之前先改 Spec
  • Spec-as-source:只需要写 Spec,我们不关系具体的代码是怎么实现的

这里的 Spec-as-source 在我看来很有意思,因为它让我想起来了源码和二进制的关系,在概念上来说有如下的对应关系

代码编译 SDD
“源码” 高级程序语言 规约(Spec)
“编译过程” 编译器 LLM
“制品” 二进制/字节码 高级程序语言

现在我想没有人会去看编译器产生的二进制和字节码,那 SDD 在未来会进化到这个形态吗?——我们不再关注 LLM 生成了什么代码,我们只关注 Spec 本身?来自 OpenAI 的 Sean Grove 传达了类似的观点3。这在 2026 的当下看起来还有点遥远,但说不定也不是那么远?核心挑战在我看来是如何确定 LLM 的非确定性问题,现在已经有相关的探索工作了,比如 Tessl 的 SDD 框架

Note

本篇文章我们聚焦于 Spec-anchored 阶段,即我们需要动态维护 Spec 和 Code 的场景

将 Spec-anchored 贯彻到实践过程中有几个要遵循的原则

  • Spec 放到代码仓里和代码统一进行版本管理:放在一起的好处是你的 AI 编程工具又可以检索到文档,又可以检索到相关代码
  • 先改 Spec 再改代码:任何对代码的改动都需要先修改 Spec,这样才能确保 Spec 是最新的,反应了代码的最新状态
  • Code review 需要额外检查代码实现是否对齐 Spec:总是要确保 Spec 和 Code 在语义上是一致的

我已经深度使用了 OpenCode 很久,之前就已经意识到了氛围编程的一些问题。因此我的主要使用方式是:先写相关文档,然后让 AI 编程工具根据我提供的文档去写代码,但是文档在之后经常被我丢弃了或者是没有更新。了解到 SDD 的思想之后,我发现这恰恰是 Spec-first 阶段,于是我开始思考换到更工程化的 Spec-anchored 实践。经过一番调研,我发现有如下的工具

  • Kiro:比较轻量级的 SDD 实现,只需要维护 3 个关键文档:requirements.md, design.md, tasks.md。但看起来和 Kiro 这个 IDE 强绑定了,因此没有纳入我的考虑
  • Tessl:前面提到了,这个还在概念性实验阶段,因此不纳入考虑
  • GitHub Spec kit:看起来是最正统的 SDD 选择,看完文档跑了个 demo 感觉有点厚重。它让我想起了软件工程里的瀑布模型,先做细致规划,各阶段之间做好质量管控(/clarify, /checklist, /analyze 命令),最后开始实现代码
  • OpenSpec:官方介绍里 README 里提到他们的一大卖点是轻量灵活,我同样看完了文档并跑了一个 demo,发现确实如此

下面是我总结了相关文档做的 GitHub Spec Kit 和 OpenSpec 的使用流程对比

  • 实线边框/线:表示必须遵循的步骤/路径
  • 虚线边框/线:表示可选的步骤和路径
  • 颜色:如果颜色一致表示做的事情差不多

从上图我们不难看出

  • OpenSpec 的默认模式(Core Mode)只需要跑 3 个命令:/propose -> /apply -> /archive,而 GitHub Spec Kit 最少需要跑 4 个命令:/specify -> /plan -> /tasks -> /implement
  • OpenSpec 的 /propose $\approx$ GitHub Spec kit 的 /specify -> /plan -> /tasks
  • OpenSpec 没有 GitHub Spec Kit 的 /constitution 阶段,这个阶段是用来指定一些硬性要求的(功能上跟 AGENTS.md 比较类似),但你可以通过修改 openspec/config.yaml 实现类似的效果
  • OpenSpec 在完成开发之后,有一个 /archive 的功能,可以将你当前实现特性的 Spec 合并到主 Spec 上(后面我会解释这是什么意思),但在 GitHub Spec Kit 里没有对应
  • OpenSpec 提供了 2 种模式:Core Mode 和 Expanded Mode,前者比较简单,后者则允许你做更多控制
Tip

我还是很推荐自行尝试下这些不同的 SDD 工具的,你可以用不同 SDD 工具尝试开发同一个特性,自行感受下。如果你懒得,那么我也找到了一个别人做好的对比,用于辅助你选型 SDD 工具 :)

另外没有在图中体现的一些细节,我认为也值得关注:

  • GitHub Spec Kit 要求你在一个新的 git 分支上实现你的特性,但是 OpenSpec 没有这个要求
  • GitHub Spec Kit 生成的文档远远比 OpenSpec 多,而且从格式上来说也会更详细
  • GitHub Spec Kit 的工作流有点强制的意思在里面,比较僵硬,比如我发现调用 /implement 的时候如果 checklist 里的文档还有未检查项,那么它会建议你先不要开始实现。但 OpenSpec 不是,它的很多阶段都是可选的

OpenSpec 吸引我的点主要是它的工作流和文档比较轻量,我可以用更少的命令生成更少的文档,文档的内容也更简单。另外一个原因是,既然我们处于 Spec-anchored 阶段,那么这意味着我们需要同时维护 Spec 和 Code,更长的 Spec 意味着作为程序员的我们需要看更多文档,也意味着你需要使用🔥️更多的 token我个人认为过于详细的 Spec 有损于 SDD 带来的收益

接下来我们看一下 OpenSpec 的 Delta Spec 是什么。简单来说,OpenSpec 认为 Spec 应该放在代码里并且可以随着你特性的增删改而进化

使用 OpenSpec 的时候,目录结构如下4,其中

  • openspec/specs 存储了全局的 Spec,它总是应该跟你的代码在语义上对齐。这里是按照模块进行划分的,比如 /openspec/specs/auth 对应认证模块
  • openspec/changes/[feature] 包含了你当前正在开发的特性的相关文件,它的目录里同样有个 specs/auth,这说明了你的当前开发的特性涉及对这个模块的修改
openspec/
├── specs/
│   └── auth/
│       └── spec.md ◄────────────────┐
└── changes/                         │
    └── add-2fa/                     │
        ├── proposal.md              │
        ├── design.md                │ merge
        ├── tasks.md                 │
        └── specs/                   │
            └── auth/                │
                └── spec.md ─────────┘

根据你特性的情况(增加/删除/修改中的一种),changes 目录下的 spec.md 文件可能有不同情形

## ADDED Requirements
### Requirement: ...
#### Scenario: ...

## MODIFIED Requirements
### Requirement: ...
#### Scenario: ...

## REMOVED Requirements
### Requirement: ...
#### Scenario: ...

在合并的时候(/sync 或者 /archive),OpenSpec 会让 LLM 帮你做符合语义的智能合并 :)

前面提到,我的主力编程工具是 OpenCode,OpenSpec 已经官方提供了集成,方式也很简单

# Install OpenSpec first
$ npm install -g @fission-ai/openspec@latest

# Navigate to your project
$ cd your-project
$ openspec init

根据提示选择 OpenCode 即可,你会看到你的项目多出了个 openspec 目录

openspec
├── changes
│   └── archive
├── config.yaml
└── specs

并且 .opencode 下多出来这些 slash command 和 Skills

.opencode
├── commands
│   ├── opsx-apply.md
│   ├── opsx-archive.md
│   ├── opsx-explore.md
│   └── opsx-propose.md
└── skills
    ├── openspec-apply-change
    │   └── SKILL.md
    ├── openspec-archive-change
    │   └── SKILL.md
    ├── openspec-explore
    │   └── SKILL.md
    └── openspec-propose
        └── SKILL.md
Warning

OpenSpec 是不用 AGENTS.md 的,所以如果你本来有的话需要将内容填充到 openspec/config.yaml 里,具体可参加官方指导

接下来你只要跟之前一样,打开 OpenCode,使用新增的 slash commands 就可以~举个例子,我要实现特性 A,那么最简单的工作流是

  • /opsx-propose "关于特性 A 的描述"
  • 检查对应的文档,如果有不对的地方可以手动修改
  • /new 清空上下文
  • /opsx-apply
  • /new 清空上下文
  • /opsx-archive

在使用 OpenSpec 之后,代码产出的质量在我看来是更高了,生产力得到了提升。更可贵的是我现在有了一份跟代码共同演化的规约文档,毕竟写文档维护文档也是一件枯燥无聊的事情 :(