AI 改完不敢上线?OpenSpec 规约驱动开发实战

23 阅读5分钟

AI 改完不敢上线?OpenSpec 规约驱动开发实战

最近读到一篇讲「Spec 规约编程 + 可视化 Skills / 代码地图」的文章,核心观点很戳人:面对几十万行的老工程,人和 AI 都缺一张全览图,所以 AI 改完的代码没人敢 Review、更不敢上线。文章给的答案是「用技能生成一张可交互的代码地图」。

我的补充是:地图解决的是「看得见」,还差一步「说得清、验得了」。看得见结构,不等于对「改成什么样算完成」达成共识。这一步要靠规约(Spec)来兜。本文就聊一个把这件事做得相当轻量的开源工具 —— OpenSpec。

一、问题不在 AI 写得差,在需求没落地

先把三类典型现场摆出来:

  1. 需求只活在聊天记录里。 你跟 AI 说了三句话,它改了八个文件。三天后要复盘「当初为什么要这么设计」,只能翻对话记录,而对话记录早被压缩、清上下文、换会话冲掉了。
  2. AI 的输出无法验收。 没有事先约定的验收条件,Review 就退化成「看起来没报错」。改完之后你要读一遍自己没参与写的实现,比自己写还累 —— 尤其在存量老工程里。
  3. 系统没有单一事实源。 新人问「这个列表默认展示几列、什么情况下会记住用户的选择」,答案散落在代码、Wiki、某次群聊和某个已离职同事的记忆里。

这三点加起来,就是所谓的 spec drift(规约漂移):代码在往前跑,对系统的共识却停在原地。

Spec-Driven Development(SDD,规约驱动开发) 的思路很朴素:把「系统应该表现出什么行为」写成结构化的、可评审的、进 Git 的 Markdown,让人类和 AI 在写第一行代码之前先对齐。Spec 是契约,代码只是契约的一种实现。

这里有个容易被忽略的分工,正好能跟那篇「代码地图」文章对上:

层回答的问题典型载体
Skills / 工作流这件事按什么步骤做技能包、slash command
代码地图 / 全景图代码在哪、链路怎么走、影响面多大可交互 HTML、流程图、时序图
Spec 规约改成什么样算完成、如何验收openspec/specs/、changes/

三者是互补关系,不是替代关系。地图和技能让 AI「会干活」,规约让人「能拍板」。

二、OpenSpec 是什么

OpenSpec 是 Fission-AI 开源的轻量级规约框架(MIT 协议),给 AI 编码助手加一层「规格层」。它在 README 里写的设计哲学,基本决定了它跟同类工具的气质差异:

→ fluid not rigid            流动,而非僵化
→ iterative not waterfall    迭代,而非瀑布
→ easy not complex           简单,而非复杂
→ built for brownfield       为存量工程而生,不只服务新项目
→ scalable from personal projects to enterprises   个人到团队都能用

几个关键特征:

  • 工具无关:支持 30+ 种 AI 编程工具(Claude Code、Cursor、CodeBuddy、Codex、Copilot、Trae、Qoder、Cline 等),本质就是往项目里写 skills / commands / AGENTS.md,不绑定任何 IDE 或模型。
  • 纯 Markdown:没有私有语法、没有数据库、没有服务端,产物全在 Git 里,天然可 diff、可 Code Review。
  • 为 brownfield 设计:双目录模型 —— specs/ 描述系统当前行为(唯一事实源),changes/ 存放进行中的变更提案。老项目可以一点点把行为沉淀进 specs,不需要一次性补齐文档。
  • 无阶段门禁:任何产物随时可以回头改,不要求「先冻结需求再开工」。

三、五分钟跑起来

3.1 安装与初始化

官方提供多种安装方式(以 README 为准):

# macOS / Linux:独立二进制安装
curl -fsSL https://openspec.dev/cli/install.sh | sh

# Windows PowerShell
irm https://openspec.dev/cli/install.ps1 | iex

# 或走包管理器(npm 方式要求 Node.js 22.12.0+)
npm install -g @fission-ai/openspec@latest
brew install openspec

进到项目里初始化,选择你正在用的 AI 工具:

cd your-project
openspec init                          # 交互式挑选工具
openspec init --tools claude,cursor    # 非交互,直接指定
openspec init --tools none             # 只要 openspec/ 结构,不写工具文件

初始化之后,项目里多了这些东西:

your-project/
├── openspec/
│   ├── config.yaml      # 项目级配置:schema、context、rules
│   ├── specs/           # 唯一事实源:系统当前行为,按 capability 组织
│   │   └── <capability>/spec.md
│   └── changes/         # 进行中的变更,一个需求一个文件夹
│       └── <change-name>/
│           ├── .openspec.yaml
│           ├── proposal.md
│           ├── specs/    # delta spec(增量规约)
│           ├── design.md
│           └── tasks.md
├── .claude/skills/      # 或 .cursor/、.codebuddy/ 等,按所选工具生成
└── AGENTS.md            # 交给 AI 的接入口

提示:装完记得重启 IDE / 会话,slash command 是启动时加载的。命令写法因工具而异:Claude Code、CodeBuddy 是 /opsx:apply,Cursor、Copilot 是 /opsx-apply,Amazon Q 是 @opsx-apply,Codex 走 skill 名 $openspec-apply-change。openspec init 会打印你所选工具对应的正确写法,别照抄别人的。

默认 profile 是 core,包含 explore / propose / apply / archive。想要更细的工作流(/opsx:new、continue、ff、verify、bulk-archive、onboard),执行:

openspec config profile    # 交互式选择
openspec update            # 让新工作流生效

3.2 五步闭环

OpenSpec 的每次变更都走同一个环:

  1 Explore  →  2 Propose  →  3 Review  →  4 Apply  →  5 Archive
   想清楚        AI 出计划     人改计划     AI 按任务实现   规约吸收变更
      ↑                                                        │
      └──────────────────── 下一个 change ←────────────────────┘

① Explore —— 先想,别急着写

/opsx:explore  帮我梳理一下「表格列偏好」在这个后台系统里应该怎么设计

这是一个「只思考不落地」的模式:AI 读代码、给选项、质疑你的假设,不写任何代码,也不写产物文件。几十万行的老工程尤其需要这一步 —— 先让 AI 把影响面摊开,比直接让它动手安全得多。想清楚了,一句「把刚才讨论的提案写出来」就能带着上下文进入 Propose。

② Propose —— 产出可评审的计划

/opsx:propose add-column-preferences

AI 会创建 openspec/changes/add-column-preferences/ 及四个产物,但到此为止不写业务代码。这就是 OpenSpec 最重要的一道闸:代码出现之前,计划必须先被人看过。

③ Review —— 改的是文字,不是代码

按官方推荐的顺序读:

  1. proposal.md:问题对不对?范围是不是太大?
  2. specs/:最高价值的一份。这些需求,你能不能接受它作为「完成」的定义?
  3. tasks.md:任务是否覆盖了所有 spec,又有没有夹带私货?

要改,直接手改 Markdown,或者告诉 AI「spec 少了未登录场景,补上」。此时修改成本是几分钟,等代码铺开再改就是几小时。

④ Apply —— 按清单施工

/opsx:apply add-column-preferences

官方有两个很实用的建议:开一个新会话(干净的上下文窗口实现质量更高),以及进度只看 tasks.md 的复选框。中断了、上下文爆了,新开会话再 apply 一次,AI 会从第一个未勾选的任务继续,没有隐藏状态。计划中途发现不对?回去改产物,再继续 apply,不用推倒重来。

⑤ Archive —— 让规约追上代码

/opsx:archive add-column-preferences

归档做两件事:把 delta spec 合并进 openspec/specs/,把整个 change 文件夹移到 openspec/changes/archive/ 留作审计历史。合并语义是:

ADDED    → 追加到主 spec
MODIFIED → 替换主 spec 中的同名 Requirement(整块替换)
REMOVED  → 从主 spec 删除
RENAMED  → 只改名字,FROM:/TO:

至此,openspec/specs/ 重新成为系统当前行为的唯一事实源,下一个 change 从它出发。这是整个闭环的价值所在:规约不会随时间腐烂。

四、四件套产物,各自管什么

默认 schema 叫 spec-driven,四个产物的依赖顺序是:

             ┌─ specs ──┐
proposal ────┤          ├── tasks ── apply
             └─ design ─┘
产物回答关键约束
proposal.md为什么做、改什么、影响面只讲 why / what,实现细节留给 design
specs/<capability>/spec.md什么算「完成」行为契约,不写类名、框架、步骤
design.md技术上怎么建技术决策、取舍、风险;条件不触发时可省略
tasks.md分几步做完带 - [ ] 复选框,进度即状态

proposal.md 的模板:

# Proposal

## Why
<!-- 1-2 句:解决什么问题,为什么现在做 -->

## What Changes
<!-- 具体列出新增/修改/移除,破坏性变更标 **BREAKING** -->

## Capabilities

### New Capabilities
- `<capability-path>`: <这个能力覆盖什么>

### Modified Capabilities
- `<existing-capability-path>`: <哪条需求要变>

## Impact
<!-- 受影响的代码、API、依赖、系统 -->

这里最容易踩的是 Capabilities 命名:要按「持久的系统行为」命名(如 user-auth、column-preferences),而不是按这次的开发任务命名(如 add-login-button)。官方建议先跑 openspec list --specs 盘点已有能力,复用精确路径,别造近义词重复能力。纯重构、文档、工具链改动没有行为变化,就在 .openspec.yaml 里设 skip_specs: true,别为了过校验硬编一条需求。

delta spec:整个框架的灵魂

规约不是每次全量重写,而是描述相对当前 spec 的增量。规范格式:

# Spec Delta

## Purpose
<!-- 仅新建 capability 需要,50+ 字符说明这个能力是干什么的 -->

## ADDED Requirements

### Requirement: 保存列偏好
系统 SHALL 在用户调整列显示后持久化该选择,并在下次进入同一列表时恢复。

#### Scenario: 用户勾选新列后重新进入列表
- **WHEN** 用户在「订单列表」勾选「下单时间」列并离开页面
- **THEN** 再次进入「订单列表」时该列保持可见

#### Scenario: 未登录用户
- **WHEN** 未登录用户调整列显示
- **THEN** 选择仅在当前会话生效,且不发起任何写接口请求

几条硬规则,写错会静默失败或归档丢信息:

  • Scenario 必须用 恰好 4 个 #(#### Scenario:),写成 ### 不会被正确识别;
  • 每条 Requirement 至少一个 Scenario,每个 Scenario 至少要有 WHEN/GIVEN 与 THEN;
  • 用 SHALL / MUST 表达规范性要求,别写「应该」「可以」;
  • 支持 ## ADDED / MODIFIED / REMOVED / RENAMED Requirements 四种操作;
  • MODIFIED 必须贴完整的旧 Requirement 块(含全部 Scenario)再改,只贴片段会在归档时丢掉原有细节;新增关注点而旧行为没变,用 ADDED 而不是 MODIFIED;
  • REMOVED 要写清 Reason 和 Migration;
  • delta 文件必须叫 specs/<capability-path>/spec.md,写成 specs/xxx.md 校验和归档都不认。

design.md 与 tasks.md 的模板很克制:

# Design

## Context
## Goals / Non-Goals
**Goals:**
**Non-Goals:**
## Decisions
## Risks / Trade-offs
# Tasks

## 1. <阶段名>
- [ ] 1.1 <具体任务>
- [ ] 1.2 <具体任务>

## 2. <阶段名>
- [ ] 2.1 <具体任务>

一个完整小例子

需求:后台订单列表支持用户自定义列,并记住选择。

openspec/changes/add-column-preferences/
├── .openspec.yaml
├── proposal.md        Why: 用户每次进列表都要重勾列
│                      Capabilities: New = column-preferences
│                                    Modified = list-view(默认列需求变更)
├── specs/
│   ├── column-preferences/spec.md   ADDED: 保存列偏好、未登录降级
│   └── list-view/spec.md            MODIFIED: 默认列展示规则
│                                    REMOVED: 旧的「固定 8 列」要求 + Reason/Migration
├── design.md          localStorage 优先 + 用户配置接口兜底;评估 SSR 首屏闪动
└── tasks.md           1. 数据层 / 2. UI 层 / 3. 测试(含未登录、跨页面恢复)

openspec/specs/list-view/spec.md 里原本那条「列表固定展示 8 列」的需求,在这次 change 里被 MODIFIED 成「按用户已保存偏好展示,无偏好时回退默认 8 列」。归档后主 spec 自动更新,下一个人接手时看到的是新规则,而不是一段历史对话。

五、CLI 速查

Agent 在工作流里会自己调这些,人掌握常用的几个就够:

命令用途
openspec init [--tools <ids>]初始化项目与工具集成
openspec update [--force] [--dry-run]刷新 AI 指令文件;升级 CLI 后必跑
openspec list / openspec list --specs列进行中的 change / 列已有能力
openspec show <id> --type spec [--json]查看 change 或 spec 全文
openspec status --change <name> --json查看四件套完成度
openspec instructions <artifact> --change <name> --json取某个产物的生成指令(Agent 用得多)
openspec validate [name] --all --strict校验结构与格式,--strict 把告警也当错误
openspec archive <name> --yes合并 delta 并归档
openspec view终端/页面上看 specs 与 changes 总览
openspec config profile / config list切换工作流集合、看全局配置
openspec schemas / openspec schema fork spec-driven my-workflow查看 / 派生自定义 schema
openspec store / doctor / context / workset多仓库规约仓库(beta)相关
openspec version / install / completion版本、独立二进制安装、补全

程序化调用一律加 --json。另外 openspec change / openspec spec 是旧版名词式命令,已标记废弃,新脚本别再用。

六、团队落地:从「个人习惯」到「工程约束」

单人用时,OpenSpec 主要解决「和 AI 对齐」。到了团队,价值会转移到协作与传承,几个可以直接抄的做法:

  1. PR 里同时提交代码和 change 文件夹。 Reviewer 先读 proposal.md 和 specs/,再读 diff。评审对象从「这 800 行代码对不对」变成「需求定义对不对 + 实现是否覆盖」,认知负担小一个量级。
  2. CI 跑 openspec validate --strict。 Scenario 层级写错、需求缺场景、描述过长这类问题在合并前就拦住。
  3. 用 openspec/config.yaml 固化团队约定。 这是最轻的一层定制,把技术栈、架构约束、测试要求注入每次工作流,比如「tasks 必须包含测试任务」「proposal 必须写回滚方案」:
schema: spec-driven
context: |
  技术栈:TypeScript + Vue 3 + Vite
  测试:Vitest + Playwright
rules:
  tasks:
    - 每个 change 的 tasks 必须包含对应的单测/E2E 任务
  proposal:
    - 涉及接口变更时必须写明兼容与回滚方案
  1. 约定不够用时再 fork schema。 config.yaml 只能在标准流程上叠加,改不了产物结构和文件名。想去掉 design、加自定义章节,用 openspec schema fork spec-driven my-workflow 派生自己的 schema 与模板。
  2. 跨仓库需求用 Stores(beta)。 一个功能横跨 API、Web、组件库三个仓库时,把 openspec/ 放进独立的规约仓库,各代码仓库只读引用,避免「平台团队在 Wiki 写一遍,三个仓库各自理解一遍」。
  3. 新会话 + 高推理模型。 官方明确建议在干净的上下文窗口里做实现,规划与实现都优先用推理能力强的模型。上下文卫生对这个框架的效果影响很大。
  4. 归档后再决定要不要留存档。 openspec/changes/archive/ 保留完整的 proposal / design / tasks / delta。如果以 Git 作为规约历史,可以先提交归档再删掉其中的 specs/ 子目录,减少检索噪音;openspec validate --archived 依然可用。

七、和同类方案怎么选

方案特点代价
无规约(纯 prompt)零成本、起步最快需求漂移、结果不可预测、无法 Review
GitHub Spec Kit流程完备、阶段清晰阶段门禁偏重、Markdown 产物多、依赖 Python 环境
KiroIDE 内体验完整绑定自家 IDE 与模型
OpenSpec轻量、流动、工具/模型无关、面向存量工程靠自觉与流程约束,不强制「先写规约再写代码」

这是 OpenSpec 官方 README 给出的对比口径,我基本认同:它不试图做最严谨的那一个,而是做最容易在真实老项目里坚持下去的那一个。规约框架最大的失败模式从来不是不够强,而是团队用两周就嫌麻烦弃用了。

顺带说回开头那篇「代码地图」文章。两者完全可以串成一条流水线:

代码地图 / Skills  →  摸清工程结构与影响面
        ↓
proposal.md        →  把影响面写进 Impact,划定能力边界
        ↓
delta spec         →  定义「改成什么样算完成」与验收场景
        ↓
tasks.md + apply   →  按清单施工,代码地图复核改动范围是否越界
        ↓
archive            →  行为共识回写主 spec,成为下一轮的起点

地图让 AI 知道「动哪里」,规约让人确定「凭什么算对」。前者是可视化,后者是契约,缺哪一半都撑不起「敢上线」。

八、踩坑清单

  • #### Scenario 写成 ###:validate 会静默不认,归档后主 spec 缺场景。
  • 把 spec 写成实现方案:出现类名、框架名、步骤编号就该挪到 design.md / tasks.md。判断标准是:实现可以换掉,但外部可见行为不变的内容,不配写进 spec。
  • capability 按任务命名:add-login-button 用两次就废了,应该是 user-auth 这种能长期拥有需求的行为边界。
  • 零 delta 的 change 被 validate 拒绝:纯重构/文档改动设 skip_specs: true,不要硬凑需求。
  • MODIFIED 只贴片段:归档是整块替换,会丢历史场景。复制完整 Requirement 块再改。
  • 忘了 openspec update:升级 CLI 后,工具目录里的 skills / commands 还是旧版,表现是「新命令敲了没反应」。
  • 跨工具照抄命令:/opsx:apply、/opsx-apply、@opsx-apply、$openspec-apply-change 是同一工作流在不同工具里的不同拼法。
  • apply 时上下文没清:把 Explore 阶段一大堆探索对话带进实现,质量明显下降。开新会话。
  • archive 太晚或太早:任务没勾完就归档,规约会先于代码变成「事实」;change 长期不归档,specs/ 就不再是事实源。代码与 change 文件夹一起提交,合并后尽快归档。

九、小结

AI 编码把「写代码」的成本压得很低,于是瓶颈转移到了**「对齐」和「验收」**。OpenSpec 没有造新语言、没上平台,只是用几个 Markdown 文件加一套五步闭环,把「需求 → 计划 → 实现 → 归档」重新变成一条可评审、可追溯、可 diff 的工程链路。

对存量几十万行的项目,它的好处尤其实在:不用推倒重来,可以从下一个需求开始,一次沉淀一个 capability。跑上三五个 change 之后,openspec/specs/ 就会变成这个项目里最值得新人先读的文档 —— 而且它永远和代码同步。


参考资料

本文基于 2026 年 11 月的官方文档整理,命令与产物格式请以项目 README / docs 的最新版本为准。