OpenSpec 规范驱动开发实战:config.yaml 与 Claude Code 协作指南
1. 从一份 config.yaml 说起:OpenSpec 到底在解决什么问题
第一次接触 OpenSpec 是在一个多人协作的后端项目里,当时团队正被“需求文档和代码对不上”这件事反复折磨。产品经理在飞书文档里改了字段定义,后端同学没注意,接口返回结构跟前端约定不一致,联调当天才发现,返工到凌晨。后来有人丢了一个config.yaml到仓库根目录,说“以后接口规范都从这里生成,别再手写文档了”。那个文件就是 OpenSpec 的入口。
OpenSpec 是一套围绕SDD(Specification-Driven Development,规范驱动开发)理念构建的工程化工具链。它的核心思路很朴素:把接口规范、数据模型、字段约束这些“契约”用结构化的方式写在一个中心化的配置文件里,然后通过 CLI 自动生成文档、类型定义、Mock 数据甚至校验逻辑。你不再需要维护一份“给人看的文档”和一份“给机器用的代码”,两者从同一个源头派生。
这件事听起来像是“又一个 API 文档工具”,但实际用下来,它跟 Swagger/OpenAPI 那套的定位有微妙差别。OpenAPI 更偏向“描述已经存在的接口”,而 OpenSpec 更强调“先写规范,再写实现”,也就是 SDD 的核心——规范是源头,代码是产物。这个顺序上的差异,直接决定了团队协作方式的不同。
适合谁来参考这套东西?我的判断是三类人:一是中小团队里负责接口约定的后端或全栈,二是需要频繁跟前后端对齐字段的前端负责人,三是正在尝试把 AI 编码工具(比如 Claude Code)接入日常开发流程、想让 AI 产出更可控的工程师。如果你只是一个人写个小脚本,那确实用不上;但只要涉及两人以上的接口协作,OpenSpec 这套思路就值得花半小时了解一下。
下面我会从整体设计思路、核心配置细节、完整实操流程、常见问题排查四个层面,把我在实际项目里落地 OpenSpec 的过程拆开讲。中间会穿插config.yaml的写法、CLI 的用法,以及跟 Claude Code 配合时踩过的坑。
2. 整体设计与思路拆解:为什么是“规范先行”而不是“文档后补”
2.1 SDD 与传统开发流程的本质差异
传统开发流程里,规范文档往往是“事后补”的。接口写完了,顺手在 Apifox 或 YApi 上填一下字段,能跑就行。这种模式下,文档天然滞后于代码,而且没人有动力去维护它。等到新人接手或者前端联调,才发现文档里的字段跟实际返回对不上。
SDD 把这个顺序倒过来:先定义规范,再由规范驱动实现。OpenSpec 就是把这个理念工具化的载体。你在config.yaml里声明一个接口的路径、方法、请求体、响应体,CLI 会基于这份声明生成:
- 接口文档(Markdown 或 HTML)
- TypeScript 类型定义
- Mock 数据
- 请求参数校验规则
这意味着“文档”不再是独立产物,而是规范的副产品。规范改了,所有产物重新生成,不存在“忘了更新文档”这回事。
我选择这套方案的核心考量是单一数据源。以前一个字段的改动要同步到文档、类型定义、Mock、校验逻辑四个地方,现在只改config.yaml一处。改动成本从四次降到一次,出错概率也跟着降。
2.2 为什么用 YAML 而不是 JSON 或数据库
OpenSpec 选择config.yaml作为规范载体,这个决策值得说一下。JSON 也能表达同样的结构,但 YAML 有两个实际优势:一是支持注释,你可以在字段旁边写“这个字段是给风控用的,别删”,JSON 做不到;二是缩进表达层级,写起来比一堆花括号清爽,尤其是嵌套的请求体结构。
至于为什么不用数据库存规范,我的理解是:规范应该跟代码一起进版本控制。config.yaml放在仓库里,谁改了什么字段、什么时候改的,git log一清二楚。数据库存规范就失去了这个追溯能力,而且本地开发还得连数据库才能生成文档,太重了。
提示:
config.yaml建议放在仓库根目录或spec/目录下,跟业务代码同级,方便 CI 流程直接读取。
2.3 OpenSpec 与 Claude Code 的协作定位
热词里频繁出现 Claude Code,这不是偶然。OpenSpec 生成的规范文件,恰好是 Claude Code 这类 AI 编码工具最喜欢的输入格式——结构清晰、约束明确。你让 Claude Code “根据config.yaml里的/api/user/profile接口实现一个 handler”,它产出的代码大概率能直接跑,因为字段名、类型、必填项都在规范里写死了。
我实测下来,有 OpenSpec 规范约束的情况下,Claude Code 生成的接口代码一次通过率明显高于“口头描述需求”的方式。原因很简单:AI 不需要猜字段名是userName还是username,规范里写的是哪个就是哪个。
所以 OpenSpec 在这套工作流里的定位是“规范的守门人”,Claude Code 是“规范的执行者”。两者配合,SDD 的闭环才算真正跑通。
3. 核心细节解析与实操要点:config.yaml 怎么写才不踩坑
3.1 规范文件的基本结构
一份典型的config.yaml大致长这样:
version: "1.0" project: name: user-service baseUrl: /api/v1 models: User: type: object properties: id: type: integer required: true description: 用户唯一标识 username: type: string required: true minLength: 3 maxLength: 20 email: type: string format: email required: false endpoints: - path: /user/profile method: GET summary: 获取当前用户信息 response: ref: User这个结构分三层:project定义全局信息,models定义可复用的数据模型,endpoints定义具体接口。models和endpoints分离的好处是,同一个User模型可以被多个接口引用,改一处全生效。
3.2 字段约束的写法与校验逻辑
字段约束是 OpenSpec 最实用的部分。上面username的minLength: 3和maxLength: 20,CLI 生成校验逻辑时会直接转成对应的校验代码。我试过在 Node.js 项目里,生成的校验函数大致是这样:
function validateUsername(value) { if (typeof value !== 'string') return 'username must be a string'; if (value.length < 3) return 'username too short'; if (value.length > 20) return 'username too long'; return null; }这里有个坑要注意:required字段的校验和类型校验是分开的。如果字段是required: true但类型写错了,生成的校验逻辑可能先报类型错误再报缺失错误,顺序不对会让前端拿到奇怪的报错信息。我的做法是把required放在类型定义之前,让 CLI 优先处理必填校验。
注意:
format: email这类语义化格式,OpenSpec 默认只生成正则校验,不保证覆盖所有边界情况。生产环境建议在生成的校验逻辑上再包一层业务校验。
3.3 模型复用的边界在哪里
models复用很爽,但别滥用。我踩过一个坑:把User模型同时用在“注册接口的请求体”和“查询接口的响应体”上,结果注册时id是必填的(因为响应体里id必填),但注册时根本没有id。后来拆成UserCreate和UserResponse两个模型才解决。
判断标准很简单:请求体和响应体的字段约束几乎不可能完全一致。请求体通常有更多校验规则(比如密码强度),响应体通常有更多只读字段(比如创建时间)。所以我的经验是,请求和响应各建一个模型,公共字段用$ref引用基础模型,差异字段单独写。
3.4 版本管理:规范也要有版本号
config.yaml顶部的version字段容易被忽略,但它很重要。接口一旦对外发布,规范就不能随便改,否则调用方会炸。我的做法是:version跟 API 版本号对齐,比如/api/v1对应version: "1.0"。要改破坏性变更,就升version并新建一个config-v2.yaml,老版本继续维护。
这样做的代价是仓库里会有多个规范文件,但换来的是调用方的稳定性。实测下来,这个取舍是值得的。
4. 实操过程与核心环节实现:从零跑通 OpenSpec 工作流
4.1 环境准备与 CLI 安装
OpenSpec 的 CLI 安装方式取决于你的运行环境。在 Ubuntu 或 macOS 上,通常通过包管理器或直接下载二进制文件。我这边用的是 Ubuntu 22.04,安装步骤如下:
# 下载 CLI 二进制文件 curl -L -o openspec https://example.com/openspec-cli/latest/openspec-linux-amd64 # 赋予执行权限 chmod +x openspec # 移动到 PATH 目录 sudo mv openspec /usr/local/bin/ # 验证安装 openspec --versionWindows 环境下,下载对应的.exe文件后,把所在目录加入系统环境变量PATH即可。这里有个细节:Windows 的路径分隔符和 Linux 不同,如果config.yaml里写了相对路径引用其他文件,跨平台时要注意用正斜杠/,OpenSpec 内部会做转换。
提示:安装完成后先跑
openspec init,它会在当前目录生成一份示例config.yaml,照着改比从零写快得多。
4.2 编写第一份规范文件
假设我们要做一个用户服务,先定义模型:
version: "1.0" project: name: user-service baseUrl: /api/v1 models: UserBase: type: object properties: username: type: string required: true minLength: 3 maxLength: 20 email: type: string format: email required: true UserCreate: allOf: - ref: UserBase - type: object properties: password: type: string required: true minLength: 8 UserResponse: allOf: - ref: UserBase - type: object properties: id: type: integer required: true createdAt: type: string format: date-time这里用了allOf做模型组合,UserCreate和UserResponse都继承UserBase,各自扩展差异字段。这种写法比复制粘贴字段定义干净得多,改UserBase时两个子模型同步生效。
4.3 定义接口与生成产物
模型定义好之后,接口定义就很简单了:
endpoints: - path: /user/register method: POST summary: 用户注册 request: ref: UserCreate response: ref: UserResponse statusCode: 201 - path: /user/profile method: GET summary: 获取当前用户信息 response: ref: UserResponse然后跑生成命令:
openspec generate --config config.yaml --output ./generated生成的目录结构大致是:
generated/ ├── docs/ │ └── api.md # 接口文档 ├── types/ │ └── models.ts # TypeScript 类型定义 ├── mock/ │ └── handlers.js # Mock 数据 └── validators/ └── index.js # 校验逻辑我实测下来,types/models.ts里的类型定义可以直接被前端项目引用,省掉了手写interface的功夫。mock/handlers.js配合 Express 或 Koa 能直接起一个 Mock 服务,前端不用等后端写完就能联调。
4.4 接入 Claude Code 的实操配置
这一步是热词里问得最多的。Claude Code 本身是一个命令行 AI 编码工具,安装方式这里不展开(不同平台差异较大,官方文档有详细说明)。重点讲怎么让它跟 OpenSpec 配合。
核心思路是:把config.yaml作为上下文喂给 Claude Code,让它基于规范生成实现代码。我的做法是在项目根目录放一个CLAUDE.md,内容大致是:
# 项目规范 本项目使用 OpenSpec 管理接口规范,规范文件位于 `config.yaml`。 生成接口实现时,请严格遵循以下规则: 1. 字段名、类型、必填项以 `config.yaml` 为准 2. 校验逻辑参考 `generated/validators/` 下的生成结果 3. 响应结构必须匹配 `generated/types/models.ts` 中的类型定义然后在 Claude Code 里直接说:“根据config.yaml里的/user/register接口,用 Express 实现一个 handler。”它产出的代码会主动去读config.yaml和生成的类型文件,字段名不会写错。
我试过对比:没有CLAUDE.md约束时,Claude Code 生成的代码里username有时写成userName,有时写成user_name,得手动改。加上约束后,这个问题基本消失。
注意:Claude Code 读取文件有上下文长度限制,
config.yaml太大时建议拆分成多个文件,按模块引用,避免超出限制导致规范被截断。
4.5 把生成流程接入 CI
规范文件进了仓库,就要防止有人改了规范但忘了重新生成产物。我的做法是在 CI 里加一步校验:
# 重新生成产物 openspec generate --config config.yaml --output ./generated # 检查是否有未提交的变更 if [[ -n $(git status --porcelain ./generated) ]]; then echo "生成的产物与规范不一致,请重新运行 openspec generate 并提交" exit 1 fi这样任何人改了config.yaml但没重新生成,CI 会直接拦下来。实测下来,这个检查拦住过好几次“改了规范忘了生成”的情况,省了不少联调时的扯皮。
5. 常见问题与排查技巧实录
5.1 生成产物与预期不符怎么排查
最常见的问题是生成的类型定义里字段类型不对。比如config.yaml里写了type: integer,生成的 TypeScript 却是string。这种情况九成是 YAML 缩进错了,type被解析成了properties的同级字段而不是子字段。
排查方法:用openspec validate --config config.yaml先校验规范文件本身是否合法。这个命令会输出具体的行号和错误原因,比对着生成结果猜快得多。
5.2 模型引用报错的处理
$ref或ref引用不存在的模型时,CLI 通常会报model not found。但有时候模型明明定义了却还是报错,原因往往是定义顺序。部分版本的 OpenSpec 要求被引用的模型必须在使用它的模型之前定义。我的习惯是把基础模型放最前面,派生模型放后面,按依赖顺序排列。
5.3 跨平台路径问题
Windows 上生成的产物路径可能带反斜杠,导致在 Linux CI 上跑校验时找不到文件。解决办法是在config.yaml里所有路径都用正斜杠,CLI 内部会做平台适配。如果 CLI 版本较老不支持自动转换,可以在 CI 里加一步路径替换。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 生成类型字段类型错误 | YAML 缩进错误 | 跑openspec validate检查 |
| 模型引用报 not found | 定义顺序不对 | 基础模型放前面 |
| CI 校验失败 | 改了规范没重新生成 | 本地跑openspec generate后提交 |
| Claude Code 字段名写错 | 缺少规范约束文件 | 添加CLAUDE.md并引用 config.yaml |
| 生成产物路径找不到 | 跨平台路径分隔符 | 统一用正斜杠 |
5.5 几个我踩过的坑
第一个坑是规范文件写太细。一开始我把每个字段的description都写得很长,结果config.yaml膨胀到两千多行,Claude Code 读取时被截断,反而丢掉了关键约束。后来把详细说明挪到单独的docs/目录,config.yaml只保留结构化定义,问题解决。
第二个坑是模型复用过度。前面提过,请求体和响应体强行共用一个模型,导致必填项冲突。拆开之后清晰多了。
第三个坑是忘了版本号。有次改了一个已发布接口的字段类型,没升version,前端直接报错。后来养成习惯:任何影响调用方的改动,先升version再说。
6. 关于这套工作流的一些个人体会
OpenSpec 这套东西的价值不在于工具本身多强大,而在于它逼着团队把“规范”这件事前置。以前大家觉得写文档是负担,现在规范成了代码的一部分,写规范就是写代码,心理负担小了很多。
跟 Claude Code 配合是意外之喜。原本只是想让 AI 帮忙写点样板代码,结果发现有了规范约束之后,AI 产出的代码质量稳定了很多。这让我意识到,AI 编码工具的上限很大程度上取决于你给它的输入有多结构化。规范越清晰,AI 越靠谱。
如果让我给刚接触 OpenSpec 的人一个建议,那就是:先从一个小接口开始,别一上来就把整个项目的规范都写了。写一个接口,跑通生成流程,接入 CI,再接入 Claude Code,每一步都验证过之后再铺开。我见过有人一口气写了五百行config.yaml,结果生成报错,排查了半天发现是第三行的缩进问题,心态直接崩了。
另外,config.yaml不要追求一次写完美。规范是活的,随着业务迭代慢慢补。先让流程跑起来,再逐步完善细节,比憋大招靠谱得多。