
OpenSpec 这个词我在不少项目里见过它的影子有人拿它当 API 规范有人拿它当文档规范还有人干脆把它当成一个装 Markdown 文件的文件夹写完之后再也没人看。说实话大部分团队都没把它的价值用出来。这篇文章我想从一个实际开发者的角度把 OpenSpec 这套东西掰开揉碎讲清楚——它到底解决什么问题、怎么从零搭起来、怎么在团队里真正落地以及我踩过的那些坑。如果你正在做前后端分离、微服务改造或者每天为“接口又变了”“文档又过期了”“联调又吵架了”这些事头疼那这篇文章就是写给你的。1. OpenSpec 到底解决了什么问题1.1 传统开发的痛点接口联调为什么这么痛苦先说说一个特别常见的场景。产品经理拍板要做一个“用户中心”前端团队、后端团队、测试团队各就各位。后端哥们儿先埋头写代码写完了给前端一份 Swagger 地址说“接口好了你看一眼”。前端一看啧字段名对不上、返回结构变了、缺分页参数……于是两个人开始拉群对线最后把产品经理也拉进来一顿拉扯之后接口改了文档也改了一版。可两周之后后端又重构了文档没人管了前端拿着旧文档调新接口又是一轮新的吵架。这不是某个团队的个例而是几乎所有传统开发流程的通病。我把这类问题归纳成三个第一接口变更只靠口头传达。后端改了字段名可能只在群里说一句“users 接口的 name 改成 nickname 了”没有书面记录消息一刷就没了。第二文档永远是滞后的。就算团队用了 Swagger、Apifox 这类工具文档生成的时机和代码实现是绑死的代码没写完文档怎么都“虚”。第三前后端无法真正并行。传统模式里后端写接口的速度直接决定了前端能不能开工。前端等接口的时候要么干坐着要么自己造一块儿假数据等真正联调时又得全部推翻重来。这些问题的根源在于团队把“接口约定”这件事当成了开发的结果而不是开发的起点。大家默认“先写代码后补文档”契约永远是滞后的。可实际上前后端之间真正需要对齐的不是代码是“接口长什么样”这个约定——而约定完全可以提前定死。1.2 规范驱动开发把“约定”变成开发的起点针对上面的问题业界其实已经有了一套成熟的方法论叫规范驱动开发Spec-Driven Development也叫契约先行Contract-First。名字看着高深核心理念就一句话先写清楚接口是什么样再让前后端各干各的。打个比方。你要盖一栋楼传统做法是瓦工先砌墙砌完墙再叫水电工来开槽布线结果墙砌错了水电工骂娘。而规范驱动开发相当于先把施工图画出来瓦工和水电工都按同一张图纸干活谁也不用等谁谁也别赖谁。代码开发里的“施工图”就是接口规范Spec。规范驱动开发的基本流程是这样的第一步团队坐在一起把业务拆成若干个接口定义好每个接口的方法、路径、请求参数、响应结构第二步把定义结果写进一套规范文件中放入版本控制第三步通过工具链自动生成前后端代码骨架、Mock 数据、API 文档第四步前后端基于同一份规范并行开发最后联调时只要两边都遵循规范冲突自然就少很多。这套思路对比传统“代码先行”Code-First的优势很明显接口定义从“事后整理”变成了“事前约束”文档从“手工维护”变成了“自动生成”前后端从“串行等待”变成了“并行开发”。很多团队之所以还在天天吵架不是人不给力是流程天生就有缺陷。1.3 OpenSpec 的定位一套轻量、开放的规范工作流现在可以正式聊聊 OpenSpec 了。严格来说OpenSpec 不是某个公司搞出来的“重型平台”而是一套以Markdown Git为核心载体、围绕规范驱动开发理念设计的开放规范工具集。你不需要买任何商业授权也不需要搭一套复杂的在线系统只需要在项目里用 Markdown 写好接口规范再用它的命令行工具做校验、生成代码、启动 Mock整套工作流就跑起来了。为什么我用 Markdown 而不直接用传统 Swagger 那种 YAML因为 Markdown 的可读性和 Diff 友好度更高。一段接口说明用 Markdown 写出来任何人都能一眼看懂改了哪一行在 Git Diff 里也清清楚楚。而 OpenSpec 在 Markdown 的基础上用 YAML Front-Matter 的方式嵌入结构化数据既保留了人读的友好性又让机器能解析这就很聪明。OpenSpec 的典型使用人群包括后端开发、前端开发、测试工程师、架构师甚至懂点技术思维的产品经理。它的适用场景主要集中在三类一是前后端分离项目尤其是接口数量多、变更频繁的中大型项目二是微服务架构多个服务之间需要统一接口契约避免各写各的三是多团队协作的场景比如 A 团队提供基础用户服务B 团队、C 团队都要调用那 A 团队的接口规范就是所有人的“公共契约”更需要用规范驱动的方式来管理。2. 开始之前核心概念与工作流2.1 三个核心概念规范文件、契约、变更提案在用 OpenSpec 之前先把三个核心概念搞清楚。第一个是规范文件Spec File也就是用 Markdown YAML 写成的接口定义文件描述一个接口或一组接口的完整形态第二个是契约Contract它不是一个具体文件而是所有规范文件共同构成的“一致性承诺”——前端说“我按这个接口调”后端说“我按这个接口给”两边认的是同一套契约第三个是变更提案Change Proposal这是 OpenSpec 工作流里特别重要的机制任何对既有规范的修改都不能直接改文件而是要先写一份变更提案经过评审之后再把变更合并进规范文件。这三个概念的逻辑关系是这样的契约是目标规范文件是载体变更提案是保护机制。如果团队里谁都可以直接改规范文件那契约就名存实亡了。就好比施工队每个人都能随意改图纸这楼盖成什么样就全看运气了。所以 OpenSpec 强制要求改动必须先写提案评审通过才能合并合并之后工具链会同步更新衍生出来的代码和文档。2.2 目录结构与规范文件怎么组织我推荐大家按照下面这套目录结构来组织 OpenSpec 项目这也是社区里经过验证后比较顺手的布局spec/ api/ users/ spec.md orders/ spec.md payments/ spec.md schemas/ common.yaml user.yaml components/ error.yaml changes/ 2025-0001-add-user-avatar.md 2025-0002-orders-pagination.mdapi/目录按业务模块划分每个模块一个子目录里面放接口规范schemas/放全局复用的数据模型比如 User、Order 这种跨接口的对象components/放通用的响应结构比如统一错误格式changes/放所有变更提案用“年份-序号-简短描述”来命名既保证排序又方便追溯。这套结构的设计思路很容易理解按领域划分子目录让每个团队能快速找到自己关心的那部分规范把通用模型抽到上层避免重复定义变更提案单独存放保证历史可追溯。如果你项目还小可以先用扁平结构但一旦接口超过 20 个建议还是乖乖按模块分层否则找文件就是灾难。2.3 版本管理与变更流程OpenSpec 项目本身就是用 Git 管理的所以版本管理天然跟 Git 标签结合。我的习惯是规范库单独建一个仓库用语义化版本号SemVer打标签。比如v1.0.0、v1.1.0、v2.0.0。接口只要发生了破坏性变更必须升大版本号增删非必填字段或者新增接口升小版本号。变更流程是整套工作流里最值得学的部分我称之为“提案-评审-合并-发布”四步走创建提案开发者在changes/目录下创建一个 Markdown 文件描述变更原因、变更内容、对上下游的影响范围。评审讨论把提案发起 Pull Request相关团队在评论区讨论确认变更是否合理、是否有更好的方案。这个时候还没有改任何正式规范文件。合并更新评审通过后把提案合并进主分支同时更新对应的spec.md文件。注意提案文件要保留不能删除它是变更历史的“现场记录”。发布版本合并完成后打一个新的 Git Tag通知所有依赖方更新到新版本。这么一套流程走下来最大的好处是任何接口变更都有据可查。半年后有人问你“为什么 /users 接口的 name 改成 nickname 了”你翻一下变更提案就能找到当时的讨论记录和决策人再也不用面对“我哪记得”的灵魂拷问。3. 从零搭建一个 OpenSpec 项目3.1 环境准备安装 OpenSpec 和前置依赖开始动手之前先把环境准备好。我以常用的 Node.js 环境为例前提是你本地已经装好了 Node.js 16 及以上版本和 npm。安装 OpenSpec 的命令很简单npm install -g openspec如果你想避免全局安装污染环境也可以用npx openspec临时执行不过团队项目我建议还是在package.json里把openspec放到devDependencies这样每个人拉下代码后执行一次npm install就能锁定版本不会因为个人全局版本不一致引发校验结果差异。安装完成后验证一下openspec --version如果你用的是其他语言生态比如 Python那对应的可能是pip install openspec命令行接口名字基本类似流程大差不差。装不上或者版本不对大概率是 Node 版本太低升级 Node 或者用 nvm 管理版本就行。3.2 初始化项目骨架在目标目录里执行openspec init my-spec-project cd my-spec-project执行之后OpenSpec 会帮你生成一套标准的目录骨架包括api/、schemas/、components/、changes/这几个目录以及一个spec.config.yaml配置文件。这个spec.config.yaml是整套规范工作流的“总开关”核心配置如下project: name: my-spec-project version: 1.0.0 spec: dir: spec default_schema_version: draft-07 generator: typescript: out_dir: generated/typescript openapi: out_dir: generated/openapi mock: port: 4010 base_url: /api简单解释一下spec.dir指定规范文件的目录generator配置要生成哪些产物比如 TypeScript 类型和 OpenAPI 文档mock配置 Mock Server 的端口和基础路径。配置文件的语法因版本而异但核心就这几个维度理解思路就行。初始化完成后你可以跑一下openspec validate如果输出 “All specs are valid”说明骨架没问题可以开始写第一个接口了。3.3 手写第一个 API 规范POST /users 完整示例先做一个最简单的“创建用户”接口。在spec/api/users/spec.md文件里写下以下内容--- method: POST path: /users summary: 创建用户 tags: - users parameters: - name: body in: body required: true schema: $ref: ../../schemas/schemas.yaml#/CreateUserRequest responses: 201: description: 创建成功 schema: $ref: ../../schemas/schemas.yaml#/User 400: description: 参数错误 schema: $ref: ../../components/schemas.yaml#/Error --- ## 接口说明 创建新用户。调用成功后返回完整用户对象。 ## 业务规则 - 用户名唯一重复时报 409。 - 邮箱格式必须合法。 - 创建成功后默认状态为 active。你可能注意到了这个文件分两部分上面是 YAML Front-Matter用结构化的方式定义请求方法、路径、参数、响应下面是 Markdown 正文用自然语言补充业务规则。YAML 部分给机器读Markdown 部分给人读这就是 OpenSpec 的核心设计。接着在schemas/schemas.yaml里定义数据模型CreateUserRequest: type: object required: - username - email properties: username: type: string minLength: 3 email: type: string format: email password: type: string minLength: 6 User: type: object properties: id: type: string username: type: string email: type: string status: type: string enum: [active, disabled] created_at: type: string format: date-time写完这两个文件后执行openspec validate --strict如果返回All specs are valid说明格式正确、引用关系也没问题。--strict参数会开启更严格的校验比如检查字段命名是否符合规范、必填字段是否缺失、枚举值是否合理建议平时就用严格模式。3.4 生成 TypeScript 类型、OpenAPI 文档和 Mock Server规范写好了接下来就是享福的时间——让工具帮你干活。执行openspec generate默认情况下它会按照spec.config.yaml里的generator配置生成 TypeScript 类型和 OpenAPI 文档。我实际跑完generated/typescript目录下会出现这个文件// generated/typescript/users.ts export interface CreateUserRequest { username: string; email: string; password?: string; } export interface User { id: string; username: string; email: string; status: active | disabled; created_at: string; }注意到没有OpenSpec 生成的类型它的status字段不再是普通的字符串而是变成了字面量联合类型active | disabled。这个细节特别关键——前端拿到这个类型以后写switch分支时天然就知道有哪些枚举值想拼错都难。再启动 Mock Serveropenspec mock启动后访问http://localhost:4010/api/users就能看到一个根据规范自动生成的实例响应。更贴心的是Mock Server 会根据请求参数动态生成数据——比如你传了email参数返回值里的 email 会和请求参数保持一致模拟真实业务逻辑而不是只返回一把静态假数据。这一下前端再也不用等后端了Mock Server 就是“最听话的后端”。4. 团队协作中的实战要点4.1 规范评审怎么开一次高效的规范评审会写规范这个动作本身门槛不高难点在团队协作里怎么把规范评审变得高效、不流于形式。我的经验是不要再拉一群人坐在会议室里“过接口”那效率太低了。我现在的做法是把规范评审变成一次代码审查Code Review走 GitLab 或 GitHub 的 Pull Request 流程。规范文件的 Pull Request 评审清单大概是这些接口路径是否符合 RESTful 风格动词是否用得准确请求参数是否缺少required声明必填与选填是否合理响应码是否覆盖了常见错误场景至少要有 400 和 500数据模型是否复用了已有schemas还是重新造了一个差不多的结构枚举值是否与业务文档一致是否有破坏性变更如果有变更提案里有没有写清迁移方案评审通过之后规范才算是“定了”接下来才允许前后端各自开工。这个流程第一次跑会觉得麻烦但坚持三五个迭代之后团队会明显感觉到“返工变少了”“联调吵架变少了”因为大部分问题在规范阶段就已经暴露并解决了。4.2 在 CI/CD 里加一道自动校验的“守门员”规范在本地 validate 通过不代表进了主干分支之后还能继续保持健康。两个人同时改了同一个规范文件或者有人直接绕过流程改了spec/目录这些都可能让契约“悄悄烂掉”。所以一定要在 CI/CD 流水线里加一道自动校验。以 GitHub Actions 为例加一个简单的 workflowname: spec-validation on: [pull_request] jobs: validate-specs: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm ci - run: openspec validate --strict - run: openspec generate设置这样一道 CI 门槛后只要规范文件不合法Pull Request 就会亮红灯不能合并。这等于给契约加了一个“守门员”任何破坏契约的动作都会被拦在合并之外。实话说加了它之后团队里手滑改错格式、忘了更新引用的问题立刻减少七八成。4.3 破坏性变更的“红线”与处理策略接口规范最怕的不是改怕的是“悄无声息地改”。在 OpenSpec 工作流里破坏性变更是一条红线怎么强调都不过分。什么是破坏性变更简单说就是你的改动会让已调用的老代码报错。比如删掉一个必填字段、把可选字段改成必填、修改枚举值、改变响应状态码这些都是破坏性变更。正确的做法是所有破坏性变更都要走完整的变更提案流程并且用新版本号发布给依赖方留出升级时间。比如你计划把/users接口的响应里name字段改名为nickname那就别直接在老的spec.md上改而是走一个提案说明“为了统一命名风格将name改为nickname影响范围是所有调用方兼容期为 2 个迭代”评审通过后再在v2.0.0版本里发布。另外还有一个温和的过渡技巧破坏性变更可以分两步走。第一步新增一个nickname字段同时保留name字段标记deprecated第二步等所有调用方都用上nickname之后再在下一个大版本移除name。这样既改了规范又不会伤到老调用方团队配合起来舒服得多。5. 常见问题与排查技巧实录5.1 常见问题速查表我在实际使用 OpenSpec 的过程中遇到过一些典型问题整理成一张速查表方便你排查。问题常见原因解决方式validate 报错 “Failed to resolve schema reference”$ref路径写错或引用的 schema 文件不存检查相对路径最好用 IDE 的跳转功能验证引用生成的类型缺少某个枚举值规范里的enum定义不全或有拼写错误打开 schema 文件检查 enum 列表Mock Server 返回的字段和规范不一致规范文件更新后Mock 服务没有重新加载重启openspec mock确认加载的是最新规范openapi 文档没有生成spec.config.yaml里generator.openapi配置被删掉或注释检查生成器配置确认enabled: truePR 被 CI 卡住但本地 validate 通过本地 OpenSpec 版本和 CI 不一致在 package.json 里锁定openspec版本重新npm ci大部分校验问题其实都是路径引用、格式拼写这类小错误。遇到报错时先把完整错误信息贴出来逐条拆解比盲目重装工具高效得多。5.2 独家避坑经验四条我踩过的坑第一别让 OpenSpec 变成“文档孤儿”。它最大的价值不是生成那堆文档而是它承载的“先定契约再开发”的流程。如果你只是把规范写完扔在仓库里不拿去生成类型、不启动 Mock、不在 CI 里校验那它和一个没人看的 Word 文档没有区别。工具只是流程的强化剂流程跑不起来工具就是摆设。第二规范文件也需要 Code Owner。项目大起来以后接口归属会变得模糊。我的建议是在规范库里配置CODEOWNERS文件把每个业务模块的规范指定给对应的技术负责人。这样谁改谁的模块Pull Request 就会自动分配给人审权责清晰不该随便动的地方不会有人乱动。第三善用openspec diff看变更影响。在评审变更提案时光看文字描述看不出影响范围。我习惯在本地运行openspec diff --from v1.2.0 --to current工具会自动列出两个版本之间的全部差异包括新增接口、删除字段、修改枚举等。把这个输出贴进 PR 描述里评审人一眼就能看清影响圈。第四小步提交别攒大事。有些人觉得规范是“大设计”喜欢一次憋一个很大的 PR把十几个接口一起提交。结果评审人看到几百行改动头都大了评审质量直线下降。我现在的节奏是一个迭代只提交一两个接口的规范或者一次只提交一个变更提案。小步提交、频繁合并配合自动校验规范和代码一样只有不断小步演进质量才能稳得住。最后再分享一个个人习惯我会在每个迭代开始的前一天固定留出 30 分钟把当前迭代要做的所有接口先用 OpenSpec 写一个粗稿发给上下游团队看一眼。很多人会怀疑“这不多了一道工序吗”可等你真正体会到“规范写完、前后端各写各的最后一次联调通过”的顺畅感就再也回不去从前那种“边写边改、边改边吵”的日子了。