AI Coding 用得越多,我越能感觉到两个问题。

第一个问题是:开发过程没有留下足够的中间记录。

Agent 可以很快生成大量代码,但方案不对时,常常只能整段推倒重来;某个架构决策是在对话里做的,过两天就不记得当时为什么这样选;一次任务被打断,换一个会话后,Agent 又要从头理解需求和已经做过的工作。真正丢失的不只是上下文,还有上下文中包含的判断。

第二个问题是:AI Coding Agent 没有跟着我和项目一起成长。

今天解决过的问题,下个月还可能重新踩坑;Agent 参与同一个项目十次,第十一次也不一定比第一次更熟悉。每次会话都很聪明,长期看却像一个不断失忆的新同事。

我开始学习 OpenSpec,就是希望先解决第一个问题,再看看它能为第二个问题提供多少基础。

OpenSpec 是什么

OpenSpec 不是一个新的 Coding Agent,也不是一种更神奇的 Prompt。它更像一套面向 AI Coding 的变更管理协议:在写代码之前,先把问题、要求、设计和任务写成文件;实现时按任务执行;完成后再把已经落地的行为合并到项目的长期规格中。

它的核心不是“多写几份文档”,而是把 AI Coding 从一次性对话,变成一个有明确状态的流程。

OpenSpec 中有两类关键信息:

  • openspec/changes/ 保存正在进行的变更,回答“这一次为什么改、改什么、怎么改”;
  • openspec/specs/ 保存系统当前已经实现的行为,回答“这个系统现在应该怎样工作”。

前者是变更日志,后者是随代码演进的活文档。当一次变更被 Archive,其中的需求会进入主 Spec,变更目录则被移入 openspec/changes/archive/。

安装和初始化

OpenSpec 需要 Node.js 20.19.0 或更高版本。可以使用 npm 安装:

1
2
npm install -g @fission-ai/openspec@latest
openspec --version

macOS 也可以直接使用 Homebrew:

1
brew install openspec

然后在项目根目录执行:

1
openspec init

init 会询问正在使用哪些 AI 工具,为 Claude Code、Codex、Cursor 等工具安装对应的 Skill 或命令,并在项目中生成:

1
2
3
4
5
openspec/
├── config.yaml       # 项目上下文和工作流规则
├── specs/            # 已经上线的系统规格
└── changes/          # 正在开发的变更
    └── archive/      # 已完成的变更记录

这些都是普通文本文件,应该和代码一起提交到 Git,而不是仅保存在某一次会话中。初始化或 OpenSpec 升级后,可以用 openspec update 刷新项目中的 Skills 和命令。

config.yaml 值得单独提一下。它的 context 会进入每个 OpenSpec 工作流,适合放置会影响所有变更的少量事实;rules 可以约束某类产物;operations 则能约束 Apply 和 Archive 如何执行。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
context: |
  Tech stack: TypeScript, Node.js, PostgreSQL
  所有 API 变更都必须兼容现有客户端
  所有 OpenSpec 产物使用中文

rules:
  tasks:
    - 每个 API 任务都要包含自动化测试

operations:
  apply:
    guidance:
      - 在标记任务完成前运行测试和 Lint

配置要尽量短。能从代码读出来的信息没必要重复塞进去,否则只会占用 Agent 的上下文。

一条完整的 OpenSpec 工作流

OpenSpec 官方的核心流程是 Explore → Propose → Review → Apply → Archive。如果把“发现问题后如何回退”也画进去,完整的开发过程更像下面这样:

flowchart LR idea["需求 / 问题"] --> explore["1. Explore<br/>读代码、澄清边界、比较方案"] explore --> propose["2. Propose<br/>生成 Proposal、Spec、Design、Tasks"] propose --> review{"3. Review<br/>人工确认问题、验收条件和任务"} review -- "方案不对" --> propose review -- "确认" --> apply["4. Apply<br/>按 tasks.md 逐项实现和验证"] apply -- "需求或设计发现问题" --> propose apply -- "中断 / 换会话" --> resume["读取变更文件<br/>从第一个未完成任务继续"] resume --> apply apply -- "代码、测试、验收全部通过" --> archive["5. Archive<br/>同步主 Spec,归档变更"] archive -. "下一个变更" .-> idea

这条流程最重要的是:**代码不是一开始就出现,而是在规格经过确认后才出现。**修改几段 Markdown 的成本,远低于让 Agent 重写几千行代码。

放进一个真实需求中怎么用

以“接入 RNNoise 降噪模型”为例。最初的需求往往只有一句话:

在现有实时音频处理链路中接入 RNNoise,降低环境噪声。

这句话还不能直接变成代码。按 OpenSpec 流程,可以这样做。

1. Explore:先把问题想清楚

在 AI 对话中说:

1
2
请用 OpenSpec Explore 模式调查接入 RNNoise 降噪模型的需求。
先读取现有音频采集、重采样、编码和线程调度代码,画出当前音频链路,列出需要确认的边界和备选方案,暂时不写代码。

Explore 阶段应该回答这些问题:

  • 当前输入音频的采样率、通道数、数据类型和每次回调帧长是什么?
  • RNNoise 放在采集、重采样、AEC、AGC 和编码之间的哪个位置?
  • 使用 RNNoise 原生 C 实现,还是将模型导出为 ONNX 并通过 ORT 推理?
  • 上游回调长度不固定时,如何组成 RNNoise 需要的 480 个采样点、10 ms 帧?
  • 单声道、立体声和多路并发音频如何处理,每条流的模型状态由谁持有?
  • 可接受的额外延迟、CPU 占用和内存上限是多少,初始化失败时是中断链路还是旁路原始音频?

如果这些结论没有想清楚,Agent 越快,只会越快地实现一个错误方案。

2. Propose:把讨论变成可审查的文件

方向确认后,继续说:

1
请就刚才确认的 RNNoise 接入方案创建 OpenSpec change,变更名称为 integrate-rnnoise-denoiser。

Agent 会创建类似这样的目录:

1
2
3
4
5
6
7
openspec/changes/integrate-rnnoise-denoiser/
├── proposal.md       # 为什么做,改动范围是什么
├── specs/
│   └── rnnoise-denoising/
│       └── spec.md  # 可验收的行为要求
├── design.md         # 关键技术决策,需要时才创建
└── tasks.md          # 实现清单和进度

spec.md 不应该只写“接入 RNNoise”,而应该写成可验收的系统行为。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
### Requirement: 实时 RNNoise 降噪

系统必须在实时音频链路中按 RNNoise 帧规格连续处理音频,并保持输出的时序和时长与输入一致。

#### Scenario: 处理任意长度的音频回调

- **WHEN** 上游输入 48 kHz 单声道 PCM,且每次回调的采样点数不固定
- **THEN** 系统必须先缓冲并组成 480 个采样点的完整帧
- **AND** 每个完整帧必须严格按时间顺序调用一次 RNNoise 推理
- **AND** 同一条音频流必须在相邻帧之间复用同一个模型状态
- **AND** 新建音频流或发生不连续跳转时必须重置状态

design.md 则应该记录“为什么”:为什么选原生 RNNoise 或 ONNX Runtime,为什么在重采样之后、编码之前执行降噪,如何管理每条流的隐状态,多声道是独立降噪还是先混音,曾经考虑过哪些方案又为什么放弃。这些内容比最后那几行推理调用更值得保留。

3. Review:把最贵的错误留在纸面上

Propose 完成后不要立即点头让 Agent 写代码。建议按下面的顺序审查:

  1. 看 proposal.md:解决的是不是正确问题,范围是不是过大;
  2. 重点看 specs/:每条要求能否验收,边界和异常是否完整;
  3. 有架构取舍时看 design.md:假设、备选方案和风险是否讲清楚;
  4. 最后看 tasks.md:任务是否覆盖所有 Spec,有没有夹带与需求无关的重构。

发现“现有采集输出其实是 44.1 kHz 立体声”或“多路音频不能共用同一个 RNNoise 状态”,可以直接编辑 Markdown,也可以让 Agent 更新变更文件。审查完成前,不产生业务代码。

4. Apply:换一个干净上下文开始实现

OpenSpec 官方建议在新的会话中执行 Apply,把上一阶段的讨论噪声留在规格外面。可以直接说:

1
2
请 Apply integrate-rnnoise-denoiser 这个 OpenSpec change。
按 tasks.md 逐项实现,每项完成后运行对应验证,只在通过后勾选任务。

Agent 会先读取这次变更的文件,再按 tasks.md 逐项实现。如果中途被打断,在新会话中再次要求 Apply 即可,新 Agent 会从第一个未勾选的任务继续。

如果实现过程发现原方案有问题,不要只在代码中偷偷改口径。先修正 Spec、Design 或 Tasks,再继续 Apply。否则文档与代码会从这里开始分叉。

5. Verify 和 Archive:用事实关闭变更

验收不能依赖 Agent 的一句“已完成”。至少要根据 Spec 执行:

  • 单元测试和集成测试;
  • 不同回调长度下的 480 采样点组帧、缓冲和输出时序测试;
  • 多路音频的状态隔离,以及停止、重启和不连续跳转时的状态重置;
  • 使用固定 WAV 样本与 RNNoise 参考实现做输出对比,同时检查 NaN、削波和样本数变化;
  • 在目标设备上测量额外延迟、实时线程 CPU 占用、内存分配和长时间运行稳定性;
  • 模型初始化失败、禁用降噪和不支持输入格式时,旁路策略符合 Spec。

OpenSpec 还提供可选的 Verify 工作流,可用来检查实现是否与变更产物一致。默认配置不一定安装它,没有 Verify 时,也可以让另一个新会话根据 Spec 做独立 Review。

所有任务、测试和人工验收全部通过后,再说:

1
请 Archive integrate-rnnoise-denoiser 这个 OpenSpec change。

Archive 会把这次变更的行为要求合并到 openspec/specs/,并把变更目录移入 openspec/changes/archive/。Git 仍然是独立的事:规格、代码和测试应该在同一条变更链上提交和审查。

OpenSpec 如何解决“没有中间记录”

回到开头的第一个问题,OpenSpec 的作用非常直接。

AI Coding 中的问题OpenSpec 的对应机制
Agent 方案不符合预期,代码需要重写在 Propose 和 Review 阶段先修正文本,延迟代码生成
不记得某个决策为什么做proposal.md 和 design.md 保存背景、备选方案与取舍
中途打断或上下文用完变更文件提供上下文,tasks.md 勾选项提供显式进度
不知道代码是否真的完成Spec 把“完成”改写为可验收的场景
对话结束后信息消失文件进入 Git,Archive 保留变更历史并更新主 Spec

其中我认为最重要的一点是:**进度不再是 Agent 脑中的隐藏状态。**只要仓库还在,任何 Agent 都能通过读取文件重建任务状态。会话可以丢,工作不应该一起丢。

OpenSpec 能否让 Agent 跟着项目成长

答案是:能解决一部分,但还不是真正的“成长”。

OpenSpec 确实会让后来的 Agent 比以前更容易理解项目:

  • specs/ 持续积累系统已经承诺的行为;
  • changes/archive/ 保留每一次变更的原因和过程;
  • config.yaml 把长期有效的项目约束注入每次工作流;
  • 决策与代码共同版本化,可以被审查、回滚和追溯。

这些解决的是“项目记忆”:Agent 进场后有一套更可靠的外部记忆可以读。

但历史记录不等于学习。OpenSpec 不会自动判断哪个过去的经验值得在新任务中复用,不会把一次线上故障自动提炼成新的审查规则,也不会记住使用者跨项目的偏好。如果没有整理,归档目录只会越来越大,而不一定越来越有用。

所以 OpenSpec 更像是一层必要的基础设施:它先把经验从会话中捞出来,但还需要更强的 Agent 把经验变成能够被下一次任务精准调用的知识。

我期待的下一代 AI Coding Agent

我理想中的 Agent 不是“永远不忘记任何事”。那只会制造更大的上下文和更多过期信息。真正的成长,应该包含三层记忆:

  1. 项目记忆:系统行为、架构边界、关键决策、当前任务和已知风险;
  2. 经验记忆:哪些方案曾经失败、为什么失败、后来如何修复、怎样提前发现;
  3. 个人记忆:使用者偏好的实现方式、可接受的风险、审查习惯和解释粒度。

这些记忆不能只管写入,还要有一个可审查的学习闭环:

flowchart LR work["需求开发 / Bug / 线上故障"] --> reflect["复盘:提取候选经验"] reflect --> approve{"人工确认<br/>是否正确、可复用"} approve -- "否" --> discard["丢弃或仅保留原始记录"] approve -- "是" --> memory["写入项目 / 经验 / 个人记忆"] memory --> retrieve["新任务只检索相关记忆"] retrieve --> plan["影响 Explore、Spec 和 Review"] plan --> measure["用测试、Review 和结果验证"] measure --> update["加强、修正、降权或过期"] update --> memory

这样的 Agent 才不是把历史聊天全部塞进 Prompt,而是能够做四件事:

  • 知道什么值得记住,什么只是当时的噪声;
  • 在正确的任务中取回正确的经验,而不是无差别加载所有过去;
  • 当代码、Spec 和现实冲突时,会质疑旧知识,而不是把它当成永久真理;
  • 让使用者知道 Agent 记住了什么,并可以删除、修正、回滚和限制范围。

我希望未来每次解决问题都不是一次性的 Token 消耗,而是给下一次开发降低一点成本。第一次需要我提醒 Agent 项目中的并发约束,第二次它应该主动检查,第三次这条经验应该已经变成 Spec、测试或工具中的自动化护栏。

使用 OpenSpec 的几个建议

一次 Change 只解决一个可验收目标

“重写账户系统”太大,“修改一个变量名”又不需要完整流程。更合适的粒度是:完成后能独立验收、必要时能独立回滚的一个行为变更。

Spec 写行为,Design 写取舍

Spec 描述外部可观察的行为和验收条件,不要把实现细节全部复制进去。Design 记录需要长期保留的技术决策,尤其是被放弃的方案和取舍原因。

不要让文档与代码分叉

实现中改了方案,先更新变更产物;合并代码时同时合并 Spec;验收未完成时不要急着 Archive。文档一旦失真,Agent 就会在错误的记忆上继续工作。

不是所有修改都需要仪式

拼写修正、明显的小 Bug 和无行为变化的重构,未必值得写完整 Proposal。OpenSpec 是用来降低歧义和变更风险的,不是把每一行代码都变成审批流程。

最后

AI Coding 放大了代码生成速度,也放大了错误方案的扩散速度。在这个阶段,仅仅追求“让 Agent 写得更快”还不够,我们需要的是一套让人和 Agent 都能知道正在做什么、为什么这样做、做到哪一步的工作流。

OpenSpec 的价值,就是把“聊过”变成“写下来”,把“Agent 说做完了”变成“按 Spec 验证过了”,把“换个会话就重来”变成“从下一个未完成任务继续”。

它还没有让 Agent 真正学会长期成长,但已经先为成长准备了可信的原料:规格、决策、任务、验证和变更历史。我期待下一步,AI Coding Agent 能在这些原料上形成可审查、可纠正、可遗忘的长期记忆。到那时,它才不只是一个每次都很聪明的临时工,而是一个真正会随着使用者和项目共同进化的协作者。

参考资料