上篇文章《手写 0 行代码,打造媲美社区顶尖的流式 Markdown 渲染组件库》发出来之后,不少朋友问我:你说的 OpenSpec 到底是什么?真能做到"手写 0 行代码"吗?实际用起来有什么坑?
这篇就来聊聊我这几个月用 OpenSpec 做项目的真实体验——哪些地方确实香,哪些地方还得靠人。
先说说 Vibe Coding 这事儿
用过 Cursor、Copilot 或者 CodeBuddy 的朋友应该都有过这种体验:
你:帮我写一个用户登录功能
AI:好的,我来实现……(唰唰唰几百行代码出来了)
这种"说句话就出代码"的方式,圈子里叫它 Vibe Coding,翻译过来大概是"氛围编程"——你随便描述个需求,AI 凭感觉给你生成代码。说实话,第一次用的时候确实很爽,感觉编程门槛一下子就没了。
但用了几个月之后,我发现事情没那么简单。
比如你说"登录功能",AI 可能给你整个 JWT 方案,也可能用 Session,运气不好还给你加上微信登录、GitHub 登录——你根本没说要这些东西。三个月后回看代码,已经想不起来当时为什么这么实现的了。功能改了好几版,哪个版本对应哪个需求?天知道。团队协作就更乱了,每个人都在跟 AI 对话,代码风格五花八门。
后来我想明白了一件事:AI 的上下文窗口是有限的,但项目是要长期维护的。你跟 AI 说的那些需求,只存在于对话历史里,上下文一刷新,什么都没了。

OpenSpec 到底是啥
OpenSpec 说白了就是一套"先写清楚再让 AI 干活"的方法论。它有句口号我觉得挺到位的:
Specs are truth. Changes are proposals. Keep them in sync.
(规范是真相,变更是提案,保持同步。)
具体怎么操作呢?在让 AI 写代码之前,先用固定格式把"要做什么"和"怎么算做完"写清楚。这些东西存在项目目录里,AI 和人都能看到,相当于一份双方都认的"契约"。
几个核心概念:
| 概念 | 说人话 |
|---|---|
| Spec | 已经做完的功能长什么样,类似产品说明书 |
| Proposal | 准备做的事情,类似需求评审文档 |
| Scenario | 具体怎么验收,类似测试用例 |
| Delta | 对现有功能的改动,类似 Git Diff |
整个流程分三步:
创建提案 ──▶ 实现功能 ──▶ 归档沉淀
第一步写清楚要做什么(proposal.md、tasks.md),第二步让 AI 按清单干活,第三步做完后归档到 specs 目录,变成"当前真相"。

项目目录长这样:
openspec/
├── project.md # 项目约定
├── specs/ # 已实现的功能规范
│ ├── user-auth/
│ │ └── spec.md
│ └── payment/
│ └── spec.md
└── changes/ # 变更提案
├── add-two-factor-auth/
│ ├── proposal.md # 为什么做、做什么
│ ├── tasks.md # 实现步骤
│ └── specs/ # 对现有 spec 的改动
└── archive/ # 已完成的变更
跟 Vibe Coding 比,差别在哪
我不是说 Vibe Coding 不好,它有它适合的场景。两者的核心区别大概是这样:
| Vibe Coding | OpenSpec | |
|---|---|---|
| 需求怎么说 | 想到哪说哪 | 有固定格式 |
| 做完了怎么验收 | 全凭感觉 | 写好了 Scenario |
| 改了什么 | 改了就是改了 | 有 Proposal 记录 |
| 知识沉淀 | 没有,对话一刷新就没了 | 存在 specs 目录里 |
| 出了问题怎么追溯 | 翻 Git,但不知道改的是什么需求 | archive 里有完整记录 |

Vibe Coding 适合什么? 快速验证想法、写一次性脚本、个人小项目、简单 bug 修复。
OpenSpec 适合什么? 正经的新功能开发、会影响其他模块的改动、需要多人协作的项目、要长期维护的系统。
三个真实案例
光说概念太虚,我拿最近在做的一个 Markdown 渲染组件库举例。这个项目支持 React 和 Vue,过去一个月用 OpenSpec 迭代了十几个功能,挑三个有代表性的说说。
案例一:流式渲染(大活儿)
做 AI 对话应用的都知道,LLM 的响应是一个字一个字蹦出来的。最简单的做法是每来一个字就重新解析整段内容,但这样复杂度是 O(n²),内容一长就卡得不行。
这个功能的 Proposal 我是这么写的:
# Change: 支持流式渲染(高性能增量方案)
## Why
当前组件只能等内容全部传完才渲染。AI 对话场景下,
用户想看到内容实时出现,不想干等着。
性能痛点:每次追加都重新解析全部内容,复杂度 O(n²),长内容会卡。
## What Changes
- 新增增量解析器,采用分块缓存策略
- 只重新解析还没稳定的最后一个块,已稳定的直接复用
- React 和 Vue 都加上 StreamingMarkdown 组件
## Performance Goals
| 指标 | 目标 |
| ---------------- | ------- |
| 单次 append 延迟 | < 5ms |
| 渲染帧率 | ≥ 30fps |
| 缓存命中率 | > 90% |
Scenario 是用来验收的,比如:
#### Scenario: 稳定块缓存复用
- **GIVEN** 已经追加了标题和一个段落,都标记为稳定了
- **WHEN** 再追加新内容
- **THEN** 前面稳定的块不重新解析
- **AND** 只处理新追加的部分
Tasks 清单列得很细:创建块边界检测器、实现块缓存管理、实现增量解析器、React Hook、Vue Composable……AI 按这个清单一项项干就行。
做完之后,这个 spec 归档到 specs/markdown-streaming/spec.md。后面又在这个基础上加了输出速率控制、行内预测补全、表格渐进显示,都是通过 ADDED/MODIFIED 的方式往上叠加。
案例二:代码复制按钮(常规功能)
这个需求很简单:代码块右上角加个复制按钮,点一下把代码复制到剪贴板。
Proposal 里我特意划了边界:
## Scope
### In Scope
- 代码块右上角显示复制按钮
- 点击复制到剪贴板
- 成功/失败有视觉反馈
### Out of Scope
- 行内代码不加按钮
- 选中部分代码复制(不做)
为什么要写 Out of Scope?因为不写的话,AI 很可能"好心办坏事",自作主张帮你实现行内代码复制、选中区域复制这些你根本不需要的东西。到时候代码多了一堆,测试也多了一堆,还得花时间删。
还有个细节:流式渲染时代码块内容还在变,这时候复制按钮应该藏起来:
#### Scenario: 流式渲染时隐藏按钮
- **GIVEN** 代码块正在流式输入中
- **WHEN** 渲染代码块
- **THEN** 复制按钮隐藏
- **WHEN** 代码块稳定了
- **THEN** 复制按钮显示
这种边界情况不写下来的话很容易漏掉。
案例三:骨架屏动画丢失(修 Bug 顺便补 Spec)
有一天发现图片加载时的骨架屏动画不动了,流光效果消失了。
排查下来发现两个问题:
- CSS 动画写得有问题,缺了
linear缓动函数 - 更深层的原因是块的 key 生成策略有问题——key 里包含了内容的 hash,流式输入时内容一直在变,key 也跟着变,React 就不停地销毁重建组件,骨架屏状态每次都被重置
Proposal 写得很清楚:
## Problem
1. CSS 动画缺少 linear 缓动函数
2. 块的 key 基于 index + source hash 生成,
流式输入时 source 变化导致 key 变化,
组件频繁重建,skeleton 状态被重置
## Solution
1. 修复 CSS 动画
2. 块 key 只用 index,不包含 source hash
这个案例有意思的地方在于,它不只是修了个 bug,还暴露出原来的 spec 不完善。修完之后,我在 markdown-streaming spec 里补了一条:
#### Scenario: 稳定块 Key 不变
- **GIVEN** 某个块已经稳定了
- **WHEN** 后面追加新内容
- **THEN** 这个块的 key 保持不变
下次再遇到类似问题,AI 直接看 spec 就知道 key 不能乱变。
聊聊 OpenSpec 做不到的事
用了一段时间,我得说句实话:OpenSpec 不是银弹,很多事情它帮不了你。
方案设计还得人来
OpenSpec 擅长把已经想好的方案变成可执行的任务,但方案本身得你自己想。
❌ 你:帮我设计一个高性能流式解析器
AI:(输出一堆可能跑不通的东西)
✅ 你:我打算用分块缓存策略,append 延迟要控制在 5ms 以内,
缓存命中率 90% 以上,帮我整理成 proposal
AI:(基于你的输入整理成规范格式)
案例一里那些核心决策——分块缓存、块边界检测、性能指标——都是我自己定的。AI 的任务是把这些整理成文档,然后按任务清单写代码。
复杂 Bug 得人工定位,或引导 AI 帮你排查
案例三就是典型。Bug 表现是"动画不动了",但根因是"key 生成策略导致组件频繁重建"。
一开始我也没想到是 key 的问题。最初的猜测是 CSS 动画写得有问题,让 AI 改了几轮都没用。后来我换了个思路:与其让 AI 盲猜,不如让它帮我加日志,把运行时状态打出来看看。
🔍 你:动画还是不行,帮我在 StreamingImage 组件的 mount 和 unmount
生命周期里加上日志,打印组件 id 和时间戳,我想看看是不是组件被频繁重建了
AI:(加好日志)
📋 控制台输出:
[StreamingImage] mount id=img-1 at 1706432001234
[StreamingImage] unmount id=img-1 at 1706432001456
[StreamingImage] mount id=img-1 at 1706432001458
[StreamingImage] unmount id=img-1 at 1706432001567
...(每次追加内容都在重建)
💡 你:果然是组件在频繁重建。帮我再在 BlockCache.generateKey()
里打印每次生成的 key,我怀疑是 key 变了
AI:(加好日志)
📋 控制台输出:
[BlockCache] key=pending-0-a1b2c3d4
[BlockCache] key=pending-0-e5f6g7h8 // hash 变了!
[BlockCache] key=pending-0-i9j0k1l2 // 又变了!
✅ 你:找到了,key 里包含了 source hash,流式输入时内容变化导致 hash 变化。
把 key 生成逻辑改成只用 index,不要 hash
AI:(精准修复)
这种"让 AI 帮你加日志 → 看输出 → 缩小范围 → 再加日志"的排查方式,对时序相关的问题特别有用。流式渲染、动画、异步请求这类问题,光看代码很难发现,得看运行时的实际行为。
修完之后,tasks 里还专门加了一条"清理调试代码",把 StreamingImage.tsx 和 StreamingMarkdown.tsx 里的调试日志都删掉——这些日志只是排查用的,不能留在生产代码里。
AI 可能理解错 Spec
哪怕 spec 写得再清楚,AI 还是可能理解偏。举个例子:
#### Scenario: 稳定块缓存复用
- **THEN** 已稳定的块不重新解析
AI 可能这么实现:
// ❌ 虽然没重新解析,但遍历了所有块
function append(content: string) {
const allBlocks = [...stableBlocks, parseNewContent(content)];
return allBlocks; // 还是 O(n)
}
// ✅ 正确做法:连遍历都不要
function append(content: string) {
const newBlock = parseNewContent(content);
return { stableBlocks, newBlock }; // O(1)
}
所以 Code Review 不能省。

什么时候不用 OpenSpec 流程
- 简单 bug:按 spec 恢复预期行为即可
- 改个错别字、调个格式
- 升级不涉及 breaking change 的依赖
- 改配置文件
- 给现有功能补测试
这些直接改就行,不用写 proposal。
写在最后
用 OpenSpec 最大的感受是:让 AI 一开始就理解清楚需求,比事后修复理解偏差省事多了。
在这个项目里,我们用 OpenSpec 迭代了十几个功能,形成了 5 个核心 spec:markdown-streaming、code-copy、markdown-styles、mermaid-render、mermaid-streaming。每个 spec 都是可追溯、可演进的文档,不会随着对话消失。
如果你的项目也开始变得复杂,需要多人协作或长期维护,可以试试这套方法。不一定非要用它的 CLI 工具,核心是那个思路:先写清楚,再让 AI 干活。
附:常用命令
# 初始化
npx openspec init
# 看看现在有哪些 spec
openspec list --specs
# 看看有哪些进行中的 change
openspec list
# 验证 proposal 格式对不对
openspec validate <change-id> --strict --no-interactive
# 功能做完了,归档
openspec archive <change-id> --yes
附:在 CodeBuddy 中使用 OpenSpec
跟直接用 CLI 不同,OpenSpec 在 CodeBuddy 里有更深度的集成——通过 Command 机制 实现。
初始化:自动注册 Command
在项目根目录执行 npx openspec init 后,除了生成 openspec/ 目录结构,还会在 .codebuddy/ 下创建一组 command 文件。这些 command 会自动注册到 CodeBuddy 的对话界面中。
初始化完成后,在对话框输入 / 就能看到 OpenSpec 相关的命令:
/openspec:proposal # Scaffold a new OpenSpec proposal
/openspec:apply # Implement an approved OpenSpec proposal
/openspec:archive # Archive a deployed OpenSpec change
实际工作流
1. 创建变更提案
输入 /openspec:proposal,然后描述你要做的事情:
/openspec:proposal 给代码块加复制按钮,点击复制到剪贴板,
成功失败都要有视觉反馈,行内代码不加
AI 会按 OpenSpec 规范自动创建 openspec/changes/<change-id>/ 目录,生成 proposal.md 和 tasks.md。
2. 执行变更
提案确认没问题后,输入 /openspec:apply:
/openspec:apply add-copy-button
AI 会读取 tasks.md 里的任务清单,逐项实现,并实时更新任务状态(pending → in_progress → completed)。
3. 归档
功能做完测试通过后:
/openspec:archive add-copy-button
也可以不指定具体的 proposal,直接输入 /openspec:archive,AI 会自动从工作区检索待归档的 proposal 让你确认。
AI 会把 change 里的 spec 变更合并到 openspec/specs/ 对应的 spec 文件中,然后把整个 change 移动到 archive/ 目录。
跟 CLI 的区别
| 方式 | 特点 |
|---|---|
| CLI(命令行) | 适合脚本化、CI/CD 集成、批量操作 |
| CodeBuddy Command | 对话式交互,AI 帮你写 proposal,边聊边改,更适合日常开发 |
我现在基本不用 CLI 了,全程在 CodeBuddy 里完成。省去了手写 proposal 的麻烦,AI 帮你整理需求、生成规范文档、执行任务、归档——一条龙服务。
小技巧
-
@ 引用现有 Spec:开始新功能前,用
@openspec/specs/xxx/spec.md让 AI 看到上下文 - 复杂 Bug 排查:让 AI 帮你加日志而不是盲猜,参考文中案例三的做法
- Code Review:实现完一个任务后,可以让 AI 对照 spec 里的 Scenario 自查
本文案例来自 tc-markdown 项目,一个基于 unified 生态的多框架 Markdown 渲染组件库,支持 React 和 Vue。