我第一次接触到 OpenSpec,是在接手一个已经折腾了两周的搜索模块。需求文档洋洋洒洒写了一万多字,代码也堆了一千多行,可每次评审都对不上,测试更不知道该按什么标准写。后来我把核心行为整理成一份 spec,把所有验收点变成可运行的检查项,团队终于有了共同语言。这就是 SDD(Specification-Driven Development,规范驱动开发)的实用价值,而 OpenSpec 就是把这套理念落地的开源框架。
这篇文章是 OpenSpec 指南的第一期,不讲虚的,先把它的核心思路、安装初始化、spec 怎么写、怎么跑通一条完整流程,以及我踩过的坑一次说清楚。适合正在做后端服务、接口开发,或者开始大量用 AI 写代码但总被“答非所问”折磨的人。无论你是技术负责人,还是写业务代码的一线开发者,看懂这一篇之后,至少能把“需求”这个词变成项目里真正能执行的东西。
1. SDD到底在解决什么问题,OpenSpec凭什么成立
1.1 传统开发模式里最让人头疼的三个环节
我做了十几年开发,最耗精力的从来不是“写代码”本身,而是“到底要做什么”这件事。需求评审会上大家点头说“明白了”,回去一写发现理解完全不一样。产品经理说的是“用户要能快速登录”,开发理解成“给个手机号验证码”,测试理解成“用户名密码表单”,三个人最后验收的其实是三套东西。
这种混乱的成本一般不在当时爆发,而是等接口已经联调完、页面已经渲染完,测试用例跑出来一堆红色,才发现根子上的偏差。这时候改需求容易,改代码难,改已经达成的“口头共识”最难。
AI 时代这个问题又被放大了。市面上代码生成工具越来越强,可你把一句话丢给它,它给你生成的东西看着像样,实际行为却经常偏得离谱。原因很简单:自然语言描述在细节上是稀疏的,一千个人有一千种补全方式。没有可执行的标准,生成得越快,错得越远。
1.2 SDD的核心思路:把“说什么”和“怎么做”彻底拆开
SDD 的核心思路其实特别朴素:先写清楚“系统应该表现出什么行为”,再动手写实现。这里的“清楚”不是用一段散文描述,而是写成像验收标准、测试样例这种能被程序判定真假的东西。
打个比方,做菜的时候菜谱就是 spec。菜谱上写着“盐 3 克、大火煮 5 分钟”,这是可以量化的验收标准。你换成电磁炉还是煤气灶,切成片还是切成块,那是实现细节。只要最终咸度合适、软烂程度到位,就算合格。反过来,如果菜谱只写“加适量盐、煮一会儿”,十个厨师能做出十种味道,你没法判断谁做得对。
SDD 就是把“菜谱”和“做菜过程”分离,让菜谱本身成为约束、测试和验收的基准。这个思想并不新,TDD、BDD 都有类似影子,但 SDD 更强调 spec 是第一公民:代码是 spec 的一种实现,而 spec 是所有讨论的起点。
1.3 OpenSpec在SDD里的位置
OpenSpec 是一套开源的、围绕 spec 来组织开发流程的工具链。它不强迫你用某种编程语言,也不绑定某个测试框架,而是用 Markdown 文件承载规格,用命令行对规格做校验和验证。
我理解它的价值有三层。第一层是“沉淀”:把散落在文档、聊天记录、白板照片里的需求整理成结构化 spec 文件,放进 Git 里管理。第二层是“约束”:每个 spec 里有明确的验收标准和测试样例,实现完没实现,跑一下就知道。第三层是“协作”:无论是人还是 AI 助手,拿到的都是同一份 spec,谁都可以按它去实现,也都可以按它去验证。
我实际用下来的感受是,OpenSpec 特别适合那些需要频繁迭代、多人协作、或者把代码交给 AI 工具来写的场景。它像给需求装了一个可执行的“标尺”,不再靠默契和记忆推进。
1.4 为什么是OpenSpec而不是自己写一套
很多人听到这可能会想:我自己在仓库里建一个 docs/requirements.md 不就行了?我在早期也这么干过,但结果往往是文档写完之后再也没人看,因为它是静态的,跟代码毫无关联。
OpenSpec 跟普通文档最大的区别是它强调“可执行”。spec 里有标准化的字段结构,有验证命令,可以接入 CI,还可以导出给 AI 助手作为上下文。这些东西如果自己搭,需要设计格式、写校验脚本、做测试运行器、处理各种边界情况,至少得花掉一两周时间,还不一定比社区版本想得周全。
我用一张表说明它和常见方案的区别:
| 方案 | 是否能验证 | 是否进代码库 | 对 AI 友好度 | 维护成本 |
|---|---|---|---|---|
| 普通 Markdown 文档 | 否 | 一般会 | 低 | 高,容易过期 |
| Cucumber/Gherkin | 是,但偏端到端 | 会 | 中 | 中,语法较重 |
| 自建脚本 | 看情况 | 会 | 低 | 很高 |
| OpenSpec | 是 | 会 | 很高 | 低,结构固定 |
当然,OpenSpec 不是万能的。如果你的需求非常简单,一个接口就一个行为,那直接写测试就行,没必要上框架。但当需求一多、场景一交叉,规范和实现的边界变得模糊时,OpenSpec 这套结构才真正体现出价值。
2. OpenSpec快速上手:环境准备与初始化
2.1 安装与版本确认
先说环境要求。OpenSpec 的 CLI 是基于 Node.js 生态的,所以你需要先确保机器上有 Node.js 和 npm。版本我建议 Node 18 以上,太低会有兼容性问题。
检查完 Node 之后直接全局安装:
node -v npm -v npm install -g openspec openspec --version装完先看一眼帮助信息,不同小版本的命令命名可能会有差异,这很正常:
openspec --help我这里给出的命令是当前主流版本的用法,如果你手里的版本不一样,优先以openspec --help的输出为准。第一次跑不认识的命令前,先加--help看看,这是所有 CLI 工具的通用安全操作。
2.2 初始化一个规范的仓库结构
安装完毕之后,新建一个项目目录并初始化。我自己习惯把 OpenSpec 从项目第一天就放进去,而不是等代码写了一半再补:
mkdir my-service cd my-service openspec init初始化命令会在当前目录生成一个.openspec目录,里面存放所有规格相关的内容。这个目录应该提交到 Git 里,它跟源码一样重要,而不是临时工具。
如果openspec init后没有任何提示,就去检查一下当前目录是否已经是 Git 仓库,以及你的 npm 全局安装路径是否在系统环境变量里。这两个问题占了我遇到过的安装问题的八成。
2.3 目录结构与约定
初始化完成后,目录大概是这样的:
.openspec/ ├── config.yaml ├── specs/ │ └── .gitkeep ├── reports/ │ └── .gitkeep └── templates/ └── spec.template.md每个目录都有明确的职责。config.yaml是全局配置,控制 spec 的扫描路径、验证规则、报告输出位置;specs目录用于存放各类功能规格;reports是运行验证后的输出目录;templates是新建 spec 时用的模板文件。
我自己会额外在项目根目录写一条开发规范:任何新功能必须先有 spec,再动代码。刚开始团队会觉得“多此一举”,但坚持两个迭代之后,大家就能体会到好处了:开发前只需要看 spec,评审也只需要看 spec,代码 review 反而退居其次。
2.4 第一个spec:登录功能的最小版本
创建 spec 有两种方式。可以用命令生成模板,也可以手动在specs目录下建文件。我个人喜欢先命令生成,再手动修改,这样格式不容易错:
openspec spec new auth-login执行之后,会在specs目录下生成一个叫auth-login的子目录,里面会有 spec 主文件和关联的测试样例文件。你需要按照模板补全内容。
补全之后先跑一遍校验命令:
openspec validate这条命令会扫描所有 spec 文件,检查 YAML 格式、必填字段、验收标准是否符合规范。看见输出没有红色错误,说明你的 spec 已经是一个合法规格了。这里的“合法”只是格式合法,内容靠不靠谱,我会在下一节详细讲。
3. 规格文件怎么写才不白写:核心细节解析
3.1 Spec文件的核心组成:front matter、验收、测试样例
一个标准的 OpenSpec spec 文件,长这样:
--- id: auth-logout title: 用户退出登录 status: proposed priority: high tags: - auth - api --- ## 需求描述 退出登录需要让当前会话失效,并返回 200 状态码。 ## 验收标准 - 携带有效 token 调用 POST /api/logout 时返回 200。 - 退出成功后再用原 token 访问受保护资源必须返回 401。 - 未携带 token 调用 POST /api/logout 时返回 401。 ## 测试样例 - 用例:有效 token 退出成功 input: header: "Authorization: Bearer valid-token" expected: status_code: 200 - 用例:退出后原 token 失效 input: header: "Authorization: Bearer revoked-token" expected: status_code: 401文件最上方的---之间是 YAML front matter,用来记录这个 spec 的元信息。id是全局唯一的标识,title是人类可读的标题,status表示当前状态(proposed、in-review、implemented、deprecated 等),priority是优先级,tags用来分类。
正文部分,需求描述写背景和目的,不要在这里写实现细节;验收标准是必须满足的硬性条件;测试样例是把验收标准转成可执行判断的实例。
3.2 验收标准:从“差不多”到“可判定”
验收标准是整个 spec 的灵魂,也是最难写的部分。我见过太多人在这里犯同一个错误:写得像文学。
举个例子。一条验收标准写成“系统需要有良好的安全机制”,这句话你没法判断是否通过。什么叫良好?谁能拍板?但改成“用户密码连续错误 5 次后,锁定账号 15 分钟”,这就变得可判定。
我总结了三个原则,运维到现在都很受用:
第一,动词要具体。不要用“处理、支持、提供”,要用“返回、拒绝、跳转、锁定”。第二,数字要明确。超时时间、状态码、限制次数,能写数字就写数字。第三,每个验收标准只能有单一结论。不要在一条标准里写“A 返回 200,并且 B 返回 401”,拆成两条,跑起来才知道哪条挂了。
3.3 测试样例:让Spec可运行
测试样例是把验收标准变成机器能跑的东西。OpenSpec 的测试样例本质上是一种小型的契约测试:定义输入,给定期望输出,然后由运行器去验证真实实现是否匹配。
上面例子里的input和expected是最简单的形式。接口场景可以直接描述请求头、请求体和期望状态码;函数场景可以描述输入参数和期望返回值。OpenSpec 本身不过度关心你怎么写测试逻辑,它更关心你一定要把“输入-期望输出”这对关系写出来。
我自己的建议是:测试样例不要写得太大。一个用例只验证一条行为,失败时你能立刻定位到是哪个验收标准没过。宁可多写几个小而精的用例,也不要堆一个“全链路大用例”,后者失败时排查成本非常高。
3.4 与现有代码/接口的协同方式
spec 不是悬浮在项目之外的纸面文档,它要跟真实代码发生关系。在 OpenSpec 里,一个 spec 从proposed变成implemented,意味着对应的测试样例已经在真实实现上跑通了。
实际项目里,你可能会面对老接口、新需求并存的情况。这时候我建议不要指望一口吃成大胖子。一个接口如果有一堆历史包袱,先挑最核心的两三个行为写成 spec,跑通后固化下来,再逐步扩大覆盖范围。这个做法很像把一团乱麻先剪出一根清晰的线头,后续整理就会容易很多。
另外,spec 文件本身也应该纳入 Code Review。我们团队现在每次提交代码时,如果涉及行为变更,就要求同时提交或更新对应的 spec。评审代码之前先评审 spec,确认改的东西是大家共识内的事情。这个流程一旦跑顺,代码评审的争论会少很多。
4. 实操记录:从一张白纸到验证通过
4.1 定义一个具体场景:用户退出登录
为了让整个流程更直观,我带你完整走一遍我自己常用的小例子:给一个已有的用户服务增加“退出登录”接口。
这个场景足够小,但覆盖了“正常流程、异常流程、退出后的连带影响”三类典型验收点。需求先说清楚:
- 用户携带有效 token 调用退出接口,服务返回 200。
- 退出之后,旧 token 立即失效。
- 没带 token 直接调用退出接口,服务返回 401。
4.2 动手写spec
先在项目里执行:
openspec spec new auth-logout然后把模板内容替换成前面展示的那版 spec。写完后跑校验:
openspec validate如果输出没有任何警告,说明 spec 结构合格了。注意,这里说的是结构合格,不代表实现已经满足它。此时 spec 的status还是proposed,表示这是一个待落地需求。
我会习惯性地把status改成in-review,提交一次 MR,让同事先评审 spec 本身。这一步虽然看起来“耽误事”,但它的价值是从源头拦截不合理的需求——比如验收标准没法测试、需求范围过大等。
4.3 生成验证入口并准备测试
OpenSpec 的验证通常是这样运行的:
openspec run auth-logout它会把 spec 里定义的测试样例转换成一个个具体的检查,然后驱动对应的测试适配器去请求真实服务。整个项目初次接入时,这一步大概率会报“无法连接服务”之类的错误,这是正常的,因为你还没有实现。
我一般会用 OpenSpec 的“导出”能力,把 spec 转成一段结构化完整的需求描述,直接粘贴给 AI 编程助手。这个操作的妙处在于:你不再是让 AI “写一个退出接口”,而是让它“实现这份 spec 中定义的行为并跑通验证”。后者对 AI 来说约束强得多,产出质量也稳定得多。
4.4 实现关键代码
我这边用一个最简单的 FastAPI 示例来演示实现端。记住,实现本身用什么语言、什么框架完全不重要,OpenSpec 只关心行为是否满足 spec。
from fastapi import FastAPI, Header, HTTPException app = FastAPI() @app.post("/api/logout") async def logout(token: str = Header(None)): if token is None: raise HTTPException(status_code=401, detail="missing token") if not token.startswith("Bearer "): raise HTTPException(status_code=401, detail="invalid token") token_value = token.removeprefix("Bearer ") if not is_valid_token(token_value): raise HTTPException(status_code=401, detail="invalid token") revoke_token(token_value) return {"message": "logged out"}这里面的is_valid_token和revoke_token是伪代码,实际项目里对应你已有的鉴权模块。重点在于:实现必须覆盖 spec 列出的所有验收标准,尤其不能漏掉“未带 token 返回 401”这条异常分支。
4.5 跑通验证并记录结果
代码写完,启动本地服务,然后再次执行:
openspec run auth-logout我预期第一次跑的时候,大概率会有一条失败,比如“退出后原 token 失效”在真实代码里还没做。这时候别慌,这是 SDD 的正常节奏:
- 有失败,说明 spec 和实现之间的确有 gap。
- 修 gap,要么改实现,要么发现 spec 写得不合理就改 spec。
- 最终目标是让该 spec 的所有测试样例变成绿色。
全部通过之后,再把 spec 的status改成implemented。这样整个仓库里哪些需求落地了、哪些还在讨论中,一眼就能看出来。
5. 常见问题与排查技巧实录
5.1 Spec写了但验证不跑
最常遇到的情况是:spec 文件明明写好了,openspec run却提示“没有找到匹配的 spec”。我排查过很多次,原因基本是三种。
第一,spec 文件放错了目录。文件没有放在specs目录下,或者放在子目录但命名方式不符合配置里的扫描规则。第二,id不匹配。运行命令时写的 id 和文件 front matter 里的 id 不一致,系统匹配不上。第三,YAML 格式有问题。front matter 某个字段缺了引号,或者缩进错位,导致整个文件解析失败。
我的建议是任何时候先跑openspec validate。它是最快的体检工具,能暴露绝大多数低级错误。如果 validate 通过但 run 不跑,就把输出信息贴到终端里逐行看,OpenSpec 的错误信息一般都会指向具体的 spec 文件和字段。
5.2 测试与实现不匹配
还有一类问题,测试样例定义得很理想,但实现跑起来就是过不了。这时候要区分是“实现错误”还是“样例错误”。
我遇到过一次很典型的例子:我写测试样例时,约定退出成功后原 token 访问受保护资源返回 401。但实现里 token 是用 JWT,无状态设计的,服务端根本没有“销毁”这个动作,退出后 token 在有效期内依然能用。这就是需求与技术方案冲突了。
最后的解法不是硬改代码,而是回到产品层面确认:用户退出后,原 token 是必须立即失效,还是说等它自然过期也可以。如果产品方明确要立即失效,那就得引入 token 黑名单或者换有状态的会话凭证。这个问题必须在 spec 阶段想清楚,否则代码白写。
5.3 多人协作时经常冲突
OpenSpec 的 spec 也是文件,多人同时改同一个 spec 就会产生合并冲突。这本身不可怕,可怕的是冲突过程中把验收标准悄悄改了。
我们团队的约定是:spec 文件只由功能负责人修改,其他人在 MR 里提意见,不要直接改别人分支上的 spec。另外,spec 要尽量拆小,一个功能一个 spec,避免出现一个巨型 spec 被五六个人同时碰的情况。
给 CI 里加一道检查也很重要。每次有新代码合入主分支之前,强制执行openspec validate,格式不合格直接让流水线红掉。成本极低,收益是长期稳定的。
5.4 排查技巧速查表
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| validate 报 YAML 错误 | 字段缩进、引号、特殊字符导致 | 用 YAML 格式化工具检查,或重贴模板 |
| run 找不到 spec | id 不匹配或目录结构不对 | 核对 id,确认文件在 specs 下,跑 validate |
| 测试全部失败 | 服务未启动,或测试适配器地址配置错误 | 检查 config.yaml 里的 baseUrl、端口,先手动 curl 服务 |
| 单条用例失败 | 实现没覆盖该分支,或样例期望值不对 | 明确失败字段,对照验收标准逐条检查 |
| 合并冲突频繁 | 一个 spec 太大,多人同时修改 | 拆分 spec,指定文件负责人 |
5.5 分享一个排查时的独家技巧
debug spec 的时候,不要只看通过不通过,要会看失败信息里的“上下文”。OpenSpec 的失败输出通常会带着输入参数和实际响应。我每次都会把实际响应和 expected 逐字段对比,而不是急着改代码。
如果实际响应是一个 500,说明大概率是代码异常,跟 spec 逻辑无关;如果实际响应是 200,但返回体字段名跟你预期不一致,那可能是契约问题,要去对齐命名。定位方向不同,处理方式完全不同。这个区分能帮你省掉大量瞎猜的时间。
6. 落地心得与避坑经验
6.1 最容易被低估的三个坑
第一,贪大求全。我刚引入 OpenSpec 时,恨不得把所有历史功能一次性全部写成 spec,结果规范文件堆积如山,维护跟不上,最终全部沦为摆设。合理做法是只对新需求、正在重构的模块写 spec,存量代码用二八原则逐步补。
第二,把 spec 当成写完之后就锁死的合同。需求变了,spec 不更新,那测试就会开始误报,团队也会逐渐失去对 spec 的信任。SDD 里的 spec 应该是活文档,它需要跟着业务一起演进。每次变更都同步更新 spec,这应该作为开发流程的一部分,而不是额外负担。
第三,忽略了 spec 评审。如果你只在写完代码之后跑一遍验证,那本质上还是传统流程,只是多了一层测试。真正的收益在源头:让产品、开发、测试坐在一起先审 spec。这个动作能把很多需求问题挡在编码之前,省下的返工时间是立竿见影的。
6.2 团队落地的建议
如果你不是一个人瞎折腾,而是想把 OpenSpec 带进团队,我建议从一个小模块试水,别一上来全团队铺开。挑一个两周迭代内要交付的新接口,让两三个人先跑通全流程,把经验沉淀成团队内部文档。
然后把“spec 写好了没”加进需求评审的检查项。我不建议强制命令大家写,而是要在试用过程中让大家尝到甜头。比如,有人因为 spec 清晰少改了三遍代码,这个体验比任何制度都好用。
数据上可以观察几个指标:需求变更导致的返工次数、测试用例与需求的对齐度、AI 生成代码的一次通过率。这几个数据在引入 OpenSpec 前后对比会很明显。
6.3 下一步可以玩什么
第一期到这里,OpenSpec 的基础使用已经能覆盖日常开发了。接下来可以继续深入的方向至少有三个:一是把 OpenSpec 验证集成进 CI/CD 流水线,让每次提交都自动跑规格验证;二是研究它和多智能体协作,让 AI 编程助手直接消费 spec 并自主迭代实现;三是用它做需求追踪,把 spec 状态看板化和周报打通,管理层也能直观看到需求落地进度。
我自己现在的习惯是,接到新需求第一件事不是写代码,也不是画脑图,而是打开命令行敲一句openspec spec new。这个动作会让我的大脑从“马上动手”变成“先想清楚边界”,对需求的理解深度完全不一样。很多朋友问 SDD 到底有没有用,我的回答是:你先坚持三个迭代,不要中断,再回来看代码返工率,答案自己会浮出来。