Spec Kit 实战:把 API 规格当代码管理,终结文档与代码脱节

📅 发布时间:2026/10/19 2:55:55
Spec Kit 实战:把 API 规格当代码管理,终结文档与代码脱节
作为常年跟接口文档较劲的开发者我第一次听到 GitHub Spec Kit 这个说法时第一反应是“这估计又是某个把一堆配置文件包装成新概念的工具”。真正研究之后才发现它代表的是一套非常务实的工程思路把 API 规格当成正式工件用代码开发的节奏去管理它而不是等项目做完了再补一份没人看的文档。如果你正在用 OpenAPI 描述接口、为前后端联调设计契约、或者被“文档和代码对不上”反复折磨那这篇文章就是给你准备的。我会从最基础的“Spec Kit 到底是什么”开始讲到怎么搭一个最小可用的规格工作区再把我实操过程中踩过的坑和解决方法一并整理出来。提示我在这里说的 Spec Kit不是某个单一软件的命令行工具而是一整套围绕 API 规格的“写作 校验 生成 自动化”工作流。它的最终目标只有一个让接口契约像代码一样被测试、被审查、被版本化。1. 先把概念摆清楚Spec Kit 到底在解决什么问题1.1 从接口文档的“信任危机”说起很多团队都有这样一个场景项目启动时后端同学手工维护一份接口文档前端对着文档写页面测试照着文档造数据。项目跑了半年后文档里的字段和实际返回不一样了前端联调时发现接口多了一个参数测试说响应结构变了没人通知。最后大家得出结论“文档写个大概就行一切以代码为准。”这个说法的潜台词其实是接口文档已经失去了可信度。为什么因为文档是静态的、游离在开发流程之外的。代码提交了、合并了、上线了文档还停在几周前的某个版本。你可以靠制度要求大家每次改接口都去更新文档但谁也不能保证每个人都能记住。Spec Kit 的出发点就是先解决这种“没有信任基础”的问题。它把接口规格从一篇说明文变成了一个可以被程序读取、被规则约束、被机器校验的项目文件。只要自动化流程挂上了接口和规格不一致的情况就会在提交阶段暴露而不是拖到联调时才发现。1.2 Spec Kit 的构成拆解从书写到交付拆开来看Spec Kit 主要由四类能力组成第一类是规格书写第二类是规则校验第三类是代码与文档生成第四类是流程集成。第一类解决“怎么写”。早期大家习惯把所有接口定义塞进一个巨大的 YAML 文件几千行起步改一个字段都要在一堆路径里翻找。Spec Kit 鼓励把规格文件按模块拆开比如路径、数据结构、公共响应、参数定义各放一处再用引用关系把它们串起来。这样每个文件的规模都不大查看和修改成本降低也方便多人分工。第二类解决“写得对不对”。通过一套可配置的规则比如“每个接口必须有明确的描述”“禁止出现未定义的响应码”“布尔字段必须写明默认值”让机器定期扫描规格。规则严不严、定多少条完全可以按团队自己的承受能力来而不是一刀切。第三类解决“写一次用多处”。规格本身是结构化的数据可以借此生成 Markdown 格式的接口文档、生成客户端 SDK、生成服务端接口骨架甚至生成模拟数据服务。以前这些工作靠手工重复劳动现在都交给模板和生成器。第四类解决“能不能被持续信任”。把校验、生成、差异对比这些动作挂到持续集成流水线里。每次有人改动规格系统自动跑一遍跑挂了就打回修改跑过了才允许并入主干。这样一来规格质量就不再依赖个人的自觉性。1.3 谁适合在这个工具链上花时间我见过最适合引入 Spec Kit 的是那些正在做前后端分离、或者有多个外部调用方的团队。后端提供接口前端需要知道参数边界测试需要验证返回结构第三方接入方需要保持稳定契约这几类角色都能从规格工程化里得到好处。如果你是一个人维护一个小的内部项目或者接口只在本机调用那确实没必要为了用工具链而用工具链。但只要你经历过“口口相传接口结构”“群里发了新的文档链接”“代码版本和文档版本对不上”这类尴尬场景就值得花一个下午把最小闭环跑起来。2. 为什么要把 API 规格当代码来管理2.1 手工维护规格的三个痛点先说一个生活类比盖房子之前要有设计图装修完还要有一份和实际情况一致的水电图。电气师傅如果只看最初的水电图去做维护大概率会凿错墙。接口文档就是系统的水电图图纸和实际情况一旦脱节后面的维护工作就开始“盲打”。手工维护规格的痛点第一个是不一致。同一个字段在文档里叫 userId在代码里叫 user_id在测试环境返回又是 UID。三个入口三个叫法没人知道哪个是权威来源最后只能靠“问老同事”来确认。第二个痛点是反馈链路太长。手工文档发现错误时往往已经影响到正在开发的功能这时的修改要同时通知开发、测试、前端再等文档维护者更新中间人的沟通成本远高于机器检查。第三个痛点是没法自动化。接口文档如果只是一个人写的文字它不能驱动任何工具链。你不能让文档自动生成模拟服务也不能让文档自动生成调用代码不能和测试接口的响应结果做对比一切都停在人工阶段。2.2 先确定契约再动手写功能传统的开发顺序经常是先把功能写完再补一套接口描述。这个过程看起来效率很高实际上隐患很多。因为代码一旦成型接口的参数、结构、状态码就基本定了后补文档只是对代码的机械记录它约束不了设计质量。Spec Kit 的理念更适合反过来先设计契约校验通过后用契约生成 Mock 数据前后端并行开发。后端按契约去实现逻辑前端按契约去调用接口测试按契约构造场景。契约先行听起来像多了一个环节但它把接口设计中的问题提前暴露了。举个例子你在契约里定义了“分页接口必须有 page 和 pageSize 参数且 page 从 1 开始”那前端就不会为了第一页传 0 而和后端争论语义。这些约定在代码层面看可能只是一行注释但在契约层面看就是所有协作方共同遵守的标准。2.3 规格即配置配置即事实把规格当作代码来管理意味着规格不再是某个人的笔记而是团队共同维护的资产。它需要走代码评审需要被版本控制需要纳入测试覆盖。常见的做法是专门建一个仓库来放规格文件或者放在业务仓库的 api-spec 目录下跟着业务一起演进。规格文件被纳入版本管理后每一次改动都有迹可循。为什么某个接口加了参数组会上讨论过什么方案通过提交历史和评审意见就能还原。这种可追溯性对长期项目来说非常值钱。而且规格和软件一样要讲究兼容性。当你对外提供一个接口不能随便改参数类型或者删字段。只有把规格版本化才能明确回答“当前第几个版本”“哪些字段被标记废弃”“什么时候可以移除”。我见过太多团队因为缺少版本概念直接修改线上接口把调用方打得措手不及。规格当代码管理本质上是在用工程纪律来对抗这种混乱。3. 第一次上手搭建最小的 Spec 工作区3.1 规划目录结构我第一次尝试时把所有内容塞进一个 openapi.yaml 文件结果不到一个月就后悔了。每次跑校验都要等待找某个路径定义得用搜索功能改一个通用 schema 要全局替换。如果你也准备开始我的建议是从头就花十分钟把目录拆好。以下是我常用的布局api-spec/ ├── openapi/ │ ├── openapi.yaml │ ├── info.yaml │ ├── paths/ │ │ ├── ping.yaml │ │ └── users.yaml │ ├── schemas/ │ │ ├── user.yaml │ │ ├── page.yaml │ │ └── error.yaml │ └── parameters/ │ └── common.yaml ├── rules/ │ └── lint-rules.yaml ├── scripts/ │ └── validate.sh ├── generated/ │ ├── docs/ │ ├── mock/ │ └── client/ └── README.md拆分的核心思路是“按变化频率分层”。路径定义变化最频繁单独放 paths数据结构和响应结构相对稳定放 schemas公共查询参数可以被多个接口复用放 parameters生成产物一律丢进 generated不参与手工修改。注意generated 里的文件属于“可再生资产”每次由工具重新生成。不要手动改这些文件否则下一次生成时你的修改会被覆盖容易造成团队间的混乱。3.2 编写第一份 OpenAPI 规格最小规格不需要追求多完备先把链路跑通。以我习惯的写法为例一个健康检查接口在 openapi/openapi.yaml 里可以这样定义openapi: 3.0.3 info: title: 示例项目X version: 0.1.0 description: 用于演示 Spec Kit 工作流的接口规格 paths: /ping: get: operationId: pingService summary: 健康检查 responses: 200: description: 服务正常 content: application/json: schema: type: object required: - message properties: message: type: string example: pong这里的 openapi 字段表示使用的规范版本3.0.3 目前是兼容度比较高的一种info 里的 title 和 version 会出现在所有生成的文档里paths 下按请求方法和路径组织接口。operationId 是给操作取一个唯一的名字生成 SDK 时通常会把它作为函数名。为什么必须写 responses因为接口契约里最容易被忽略的就是响应结构。很多人写规格时只关心入参响应随便填一个 200结果前端完全不知道返回体长什么样。这种时候契约就失去了指导价值。3.3 用命令行完成本地校验规格文件写好后下一步是安装规则校验工具。不同团队选择的工具有差异但基本流程都是安装命令行工具、传入规则文件和规格文件、读取校验结果。以我本地环境为例用 npm 安装npm install -g openapi-lint openapi-lint validate openapi/openapi.yaml --rules rules/lint-rules.yaml规则文件的内容可以从一个最小集合开始rules: operation-summary-present: severity: error response-description-present: severity: error no-invalid-response-codes: severity: error boolean-field-has-default: severity: warn这套配置里error 级别会直接阻止通过warn 级别只是提醒。最开始别把所有规则都定成 error团队需要时间适应。先让规则跑起来再逐步收紧才是比较稳健的路径。校验通过后你可以把生成的文档和代码脚本也串起来openapi-lint generate-docs openapi/openapi.yaml --out generated/docs openapi-lint generate-client openapi/openapi.yaml --out generated/client --lang typescript这样每次你在本地修改规格就能立刻看到文档和 SDK 的变化形成“改规格 → 校验 → 生成 → 查看”的循环。3.4 接入持续集成让每次改动自动过检本地校验解决的是个人问题持续集成解决的是团队问题。你可以把下面的步骤配置合并进仓库的流水线只要规格文件一有改动就先做一次检查steps: - name: 检出代码 run: git pull - name: 校验接口规格 run: openapi-lint validate openapi/openapi.yaml --rules rules/lint-rules.yaml - name: 生成文档产物 run: openapi-lint generate-docs openapi/openapi.yaml --out generated/docs - name: 检查生成差异 run: git diff --exit-code generated/这里最容易被忽略的是最后一步。生成器版本升级后生成出的文档格式可能会变化。如果差异很大说明产物变更产生了额外影响不该静默合并。用 git diff 挡住意外差异可以让生成结果在可控范围里演进。我自己的经验是持续集成跑起来之后团队对规格的敬畏感会明显提高。每次提交都要面对一套机器检查结果糊弄不过去自然会先把接口定义想清楚再动手。4. 把规格写干净命名、复用与边界设计4.1 路径与方法命名要稳定接口路径和操作 ID 是所有生成环节的“锚点”。前端写死的调用地址SDK 生成出来的函数名文档里的链接全都依赖这些命名。我见过最矛盾的例子是同一个接口出现两种叫法文档里写 listUsers代码注释里写 fetchUserList结果生成的 SDK 类里出现了一堆别名。路径设计要尽量表达资源关系而不是动作。比如用 /users/{id} 表示“获取某个用户”用 POST /users 表示“创建用户”这比 /getUserById 这种写法更统一。切记不要在路径里塞动作名称接口行为应该靠 HTTP 方法表达。确定路径之后不要频繁改名。接口一旦上线调用方就可能把路径写进了生产代码里。如果确实需要调整应保留旧路径作为兼容入口新路径在新版规格里提供同时给旧路径标记废弃状态给调用方一个迁移周期。4.2 用组件化解耦重复结构一个系统里分页、错误对象、用户摘要这类结构会在多个接口里反复出现。如果在每个路径定义里都复制一份一旦分页结构里增加字段就要改十几个地方改漏一个就是隐患。正确的做法是把公共结构放到 schemas 目录下用 $ref 引用。以分页响应为例# schemas/page.yaml type: object required: - page - pageSize - total - items properties: page: type: integer example: 1 pageSize: type: integer example: 20 total: type: integer items: type: array items: type: object然后在路径文件里引用它# paths/users.yaml get: operationId: listUsers summary: 获取用户列表 parameters: - name: page in: query schema: type: integer - name: pageSize in: query schema: type: integer responses: 200: description: 分页结果 content: application/json: schema: $ref: ../schemas/page.yaml这样改一处所有引用分页对象的地方都同步更新。刚开始拆分时我的建议是“被引用超过两次才抽公共文件”避免为了拆分而拆分把本来就一目了然的小结构拆到多个文件里。4.3 参数描述与约束边界接口参数的约束写得越具体调用方的猜测空间就越小。拿分页参数举例只有 type: integer 是不够的。调用方会提问page 从 0 开始还是从 1 开始pageSize 最大能到多少默认值是什么这些问题都应该在规格里体现。我把常用的参数约束整理成一份速查清单参数维度常见表达规格字段类型整数、字符串、布尔type必填是否必须传递required默认值不传时的取值default枚举可选值集合enum长度/大小字符串长度、数值范围minLength / maxLength / minimum / maximum格式日期、邮箱、UUID 等format说明给人类读的解释description举个完整例子parameters: - name: page in: query description: 页码从 1 开始 required: false schema: type: integer minimum: 1 default: 1这里的 required 字段非常关键。如果一个参数不写 required默认就是非必填很多新手误以为列在 parameters 里就是必填结果双方理解错位。务必显式声明。4.4 版本、废弃与兼容性规则接口在演进过程中最忌讳的是“就地修改”。今天把某个字段的响应类型从字符串改成对象明天把某个参数从必填改成选填都会对现有调用方造成破坏。要管理好演进必须学会用版本和废弃机制。给接口标版本时我喜欢遵循“大版本对应不兼容变更小版本对应兼容变更”的原则。凡是会增加、修改、删除字段导致旧调用方无法正常工作的变更都该升大版本凡是新增可选字段、新增枚举值这类向后兼容的变更升小版本即可。废弃字段不要在下一个版本立刻删除而是保留一段时间在描述里写明替代字段和使用者应做的迁移动作。我在实际项目中见过直接把废弃字段删掉的案例直接导致外部系统的接口解析失败。契约的修改就像搬家通知要给人搬家时间不能提前一天要求所有人当天搬完。5. 常见问题与排查技巧实录5.1 校验规则过严导致项目推不动刚引入规则校验时最容易犯的错误是“从严要求”。把命名规范、字段描述、响应结构检查全部设成 error结果开发者每次提交都要面对一堆并不影响运行的问题效率骤降抱怨声四起。后来我把规则分成了三个梯度第一梯度是“格式错误”比如 YAML 语法错误、引用了不存在的文件这类直接拦截第二梯度是“契约缺失”比如响应没有描述、参数缺少约束这类作为主要检查目标第三梯度是“风格建议”比如字段命名、排版方式这类只警告不阻止。规则收紧是一个渐进过程。一个团队如果连最基本的格式检查都还没养成立即修改的习惯上来就强制所有规则只会让大家绕开流程。建议先保留 5 到 8 条核心规则稳定运行两个月后再逐步增加。5.2 引用循环与文件拆分过度多文件拆分有个典型的副作用引用关系变得复杂甚至出现循环引用。比如用户对象引用了团队对象团队对象又反过来引用成员列表成员列表里又出现用户对象。当校验器尝试递归展开时就可能死循环或报错。解决循环引用我的经验是重新梳理模型的层级关系。真正出现循环通常意味着设计上出了问题不该用“让校验器宽容一点”来掩盖。要么把其中一个对象的嵌套字段改成一个标识 ID要么把循环的部分抽成一个独立定义避免无限递归。还有一种情况是拆分过度。我见过有人把一个只有三个字段的小对象拆成三个文件每个文件还需要各自跨目录引用。这种做法的维护成本远大于收益。正常的分界点是结构超过十行、或被多处复用时才拆文件否则留在当前文件里即可。5.3 生成代码与手写代码的边界问题用规格生成客户端 SDK 很方便但团队里经常会争论生成代码能不能直接改我的回答是业务代码和生成代码必须边界分明。生成代码放进独立目录每次由工具统一覆盖业务代码手工维护需要调用 SDK 时通过标准接口接入。如果把业务逻辑写进生成代码里下一次规格更新后重新生成会把这些逻辑覆盖。很多人因此在更新 SDK 前要么备份要么自己 merge时间久了维护成本成倍增长。注意所有生成代码都应当被视为“编译产物”只可整体替换不可局部手工修改。如果急需调整行为建议在业务层包一层适配器而不是改动生成物。5.4 多人协作时的规格冲突怎么处理当多个人同时修改规格文件冲突几乎不可避免。尤其是大家同时新增接口时路径文件合并后会互相覆盖。我常用的规避手段是让路径按模块分文件存放比如用户模块的路径放 users.yaml订单模块的路径放 orders.yaml这样改动范围天然隔离。如果冲突还是出现了不要直接在冲突标记里删来删去。先把两边的改动拉出来看语义判断哪些是新增接口、哪些是修改旧接口再决定如何合并。这里最忌讳的是只看文本差异不看结构上下文。另一个减少冲突的建议是尽量在提交前跑一次本地校验和文档生成。如果冲突影响到了引用关系本地校验能提前发现问题而不是等持续集成跑挂后才被迫返工。我用这个方法之后冲突数量至少降了一半。6. 顺着 Spec Kit 还能延伸出的实践6.1 从静态规格走向契约测试规格文件写好了校验通过了这还只是静态层面。真正要保证接口和规格一致还得靠契约测试。简单来说就是把规格里的响应结构当成预期在测试环境下调用真实接口把实际返回和预期结构做对比。契约测试不需要覆盖所有业务逻辑它只关心“接口有没有收错参数、有没有漏字段、状态码是否合理”。它可以跑在部署后的环境上也可以在冒烟测试阶段执行。规格改动后契约测试会自动用新预期去检查旧接口一旦响应漂移测试就会失败。我在项目里做这件事的收益非常明显。过去接口变更后前端要先发消息问“这次改了什么”现在只要契约测试过了大家就知道接口仍然符合约定。配合统计消息或者群机器人通知整个过程非常省心。6.2 一鱼多吃文档、模拟服务与客户端生成规格文件结构化的优势就是可以被多个消费方读取。基于同一份规格至少能产出四类资产产物用途生成方式接口文档给前端、测试、外部调用方阅读规格结构转 Markdown / HTML模拟服务前端在接口未实现时联调基于响应示例生成 Mock 数据客户端代码让调用方直接拿到类型安全的 SDK按目标语言生成客户端服务端骨架给后端一个接口函数空壳生成控制器与 DTO 定义同一个接口字段在文档里体现为表头在 Mock 服务里体现为演示数据在 SDK 里体现为类型定义。它们的底层都来自同一份规格天然保持同步。这就是把规格当代码管理后的最大红利一份数据多方消费。6.3 我个人沉淀下来的几点经验一路实践下来我最大的体会是Spec Kit 的价值不在工具本身而在它逼着你先把契约想清楚。很多人觉得写规格是额外负担但只要熬过最初两周后面节省的沟通成本会非常可观。有几条经验值得持续提醒自己第一规格文件要先保“能被机器读懂”再谈可读性第二规则配置要逐步收紧避免一开始就把团队热情浇灭第三生成产物永远别手改宁可包一层适配器第四接口变更要及时影响调用方标记废弃比直接删除温和得多。如果你正准备在自己的项目里尝试建议从一个只有三五个接口的模块开始跑通编辑、校验、生成、自动化的闭环。不要一上来就规划上百个接口的复杂规范先感受到“规格能够驱动工具”的甜头再逐步扩大范围。工具链只是脚手架真正让接口协作变顺畅的是整个团队对“契约先于实现”的认同。