用 Spec Skills 做 API 契约评审
过去八个月我一直用 Spec Skills 做 API 契约评审,真正让我决定把它留在流程里的,是一个近乎让人脸红的案例。某个 diff 工具顺顺当当地批准了一份 PR,而那次变更里,一个错误码的语义已经被悄悄改掉了。结构完全一致,语义却整个翻转。Spec Skills 在第一轮就抓住了它,因为我向它问了正确的问题。
结构与语义之间的评审盲区
结构 diff 工具有一件事做得很好:比较 OpenAPI 或 GraphQL 文档的形态,告诉你哪些字段挪了位置、哪些变成了可选、哪些端点消失了。这是实打实的价值,但也是它们覆盖的全部范围。它们看不见的那一部分,恰恰是在生产环境里烧你的那部分。
前面提到的那次发布,diff 只报出了一处变化:403 响应的描述字符串。改之前,403 的含义是"forbidden, the caller lacks permission";改之后,403 的含义变成了"rate limited, back off and retry"。同样的状态码、同样的 payload 外壳,期望的客户端行为却完全不同。结构等价,语义相反。基于生成器的检查会告诉你契约没问题,而一个把 403 映射到"登出并重新鉴权"流程的消费者,此刻正在惩罚那些重试太快的用户。
这就是 Spec Skills 在评审桌上值得占据一个席位的原因。语言模型并不是要替代 schema diff;它会把 diff 和周围的散文放在一起读,然后注意到:形态虽未变,含义已经漂移。
我是怎么组织评审提示词的
我每次调用 Spec Skills 都会一次性喂给它四样东西:旧契约、新契约、一段对消费者是谁的简短说明,以及一份要求结构化评审的固定提示词。提示词不是自由发挥式的,它要求按顺序产出四类具体输出:
- 破坏性变更(Breaking changes),按照违反的兼容性规则分类(字段移除、类型收紧、必填参数顺序调整、状态码被复用但含义变更)。
- 语义漂移(Semantic drift),形态没变,但 description、枚举标签、Header 契约或错误码的含义已经偏了。
- 弃用处理(Deprecation handling),字段被标记为 deprecated 但消费者仍在依赖,或者弃用窗口短得不够诚实。
- 消费者影响评估(Consumer-impact estimate),按客户端类型切分(移动 App、合作方集成、内部服务),让值班评审人清楚要先通知谁。
每一条发现都必须引用 diff 里的具体行。如果 Spec Skills 指不出一段具体片段,这条发现就不算数。光这条规则就把我的误报率砍掉了一半。
Spec Skills 能抓住 oasdiff 抓不到的东西
我依然在用 oasdiff。它快、确定性强,能给我一份结构上的 ground truth。它做不到的事情是:读 description 字段,注意到其中一句关键承重的话被改了。Spec Skills 把这层语义叠加在结构层之上。
最近几次评审里,下面这三个案例在结构工具看来都是"非破坏性"的:
- 枚举扩展。我们往一个响应的
status枚举里加了一个新值。oasdiff 把它标记为 additive。Spec Skills 则把它标为"对穷尽匹配客户端具有破坏性",并点名了三个内部服务——它们在对这个枚举做 switch 时根本没有 default 分支。这在 Go 和 Rust 写的服务里就是一枚随时爆炸的运行时 panic。 - 可空性翻转。一个字段从 non-nullable 变成了 nullable。oasdiff 记下了这个变化。Spec Skills 则把这种不对称讲得很透:把输入改成可空,对旧客户端是安全的;把输出改成可空,是破坏性变更,因为消费者根本没有处理 null 的分支。我在开始认真读 AI 解释之前,已经把这两种情况搞混了好几年。
- Header 契约漂移。自定义的
X-Rate-Limit-Resetheader 从 Unix 秒切换成了 ISO-8601。Header 名字没变、响应码没变、文档里写的用途也没变。Spec Skills 注意到示例值的格式变了,并对那些假定为整数的客户端标出了解析风险。
一个具体的例子:枚举扩宽
下面是最近一次评审里非常具体的发现,只改动了项目名这种细节。变更本身是 OrderStatus 上的一行新增:
- enum: [pending, confirmed, shipped, delivered, cancelled] + enum: [pending, confirmed, shipped, delivered, cancelled, returned]
oasdiff 报告:enum value added, non-breaking。Spec Skills 的输出(已截短):
severity: high
category: breaking-for-exhaustive-match
quote: "+ enum: [pending, confirmed, shipped, delivered, cancelled, returned]"
finding: New enum value "returned" introduced without default-case guidance.
consumer_impact:
mobile_app: low — TypeScript exhaustive switch will fail compile on rebuild,
but runtime traffic from old binaries will see an unknown string
and likely render as empty state.
partner_integrations: medium — partners using generated client stubs on
strict enum validators will reject the response.
internal_services: high — three Go services in the fulfillment domain
switch on this enum without a default arm.
recommendation: Ship behind a version header, or require consumers to add
default handling in a prior release before the producer emits
the new value.
这就是我想要的评审。它点出风险,点出谁会受影响,还给出了一个不会引发 panic 的部署顺序。
契约评审的验收标准
我把评审流程本身的验收标准以 Given/When/Then 的形式留着,因为评审如今已经是一块基础设施,它也需要自己的契约。
- Given a pull request that modifies an OpenAPI or GraphQL file
When CI runs the Spec Skills contract review job
Then the review output is posted as a PR comment with severity tags and
inline quote references to the diff
- Given the review reports one or more findings of severity high
When a human reviewer reads the comment
Then merge is blocked until either the finding is resolved or a named
owner records a written acknowledgement with a migration plan
- Given the review reports only low-severity findings
When the CI check completes
Then the PR is eligible for normal review without contract-specific blocks
Spec Skills 不知道的事情
模型不知道我们的合同义务。它不知道 Partner A 有一份 SLA 要求破坏性变更必须提前 90 天通知,不知道移动 App 有六周的应用商店审核滞后,也不知道某个内部团队每周发一次消费者,而另一个团队是一季度一次。这部分上下文必须由评审人补上。
我是吃过亏才明白这点的——有一次 Spec Skills 把一个弃用窗口评为"宽裕",我就按计划发了,结果发现某个合作方在一份没人写成机器可读形式的合同里谈好了更长的窗口。那不是模型的锅,是我们这边知识没采集下来。现在我们在仓库里放了一份简短的 review-context.md,列出 SLA、消费者部署节奏以及已知的脆弱集成,并把它一起塞进提示词。
误报以及它们会在哪里出现
Spec Skills 并不是每次都对。按我的日志,大约每六条发现里有一条是被误报为高风险的安全变更。常见的误报形态也很固定:把它以为是对外的内部字段当成重命名拉出来;因为措辞变了,就把文档润色当成语义变更;或者字段只在请求侧,却把"可空性增加"当作破坏性变更。人依然要做核对,评审是一份草稿,不是判决。
误报的代价比真正命中所节省的小得多。比起让一次"403 其实是另一回事"的意外上到生产,我宁可花两分钟驳回一条噪音发现。
在 CI 里跑起来,挂到 PR 上
这套集成刻意做得很无聊。一个 GitHub Action 在任何触碰 contracts/ 下文件的 PR 上触发。它拉出新旧版本,用固定提示词加上 review-context.md 调 Spec Skills,然后把结构化输出作为一条 sticky PR 评论贴上去。每次 push 评论都在原地更新,thread 保持干净。高风险发现会把一个 required status check 打成失败;由评审人确认或作者修复。整个闭环就这些。
回报是:契约评审不再是高级工程师在 sprint 结束时凭记忆做的事。它在 PR 打开时就发生,每次都一样的提示词、一样的结构,而人类评审则专注在模型做不了的判断性决策上。
最后的结论
结构 diff 抓的是你能看见的东西,语义评审抓的是你想表达的东西。Spec Skills 不是魔法,它是一个认真的阅读者——我可以拿着一份具体的问题清单指向 diff,它会回给我一份结构化的、引用了证据的回答。配上一个结构工具和一位懂业务上下文的人,它就把"schema 能编译"和"消费者周一还能正常跑"之间的那道鸿沟填上了。这就是我多年来想拥有的那种评审,现在它就是一条命令的事。
API 合同评审包应该长什么样
我会让 Spec Skills 先整理评审包,再让人读。评审包不是摘要,而是把 reviewer 需要检查的证据放在一起:schema diff、示例响应、旧客户端 fixture、错误码变化和回滚方式。
Contract review packet: - openapi diff: additive field only - response fixture: old client parses new field - error taxonomy: no new retry category - versioning: no URL or header version change - rollout: new field hidden until partner SDK release - rollback: feature flag disables serializer branch
边界:Spec Skills 可以标出风险,但不能替 API owner 批准兼容性。只要外部消费者可能受影响,最终签字必须来自契约 owner。
可复制产物:Spec Skills 工作流包
把这段用于真实的 Spec Skills 运行前。它会把输入、边界和人工评审点放在同一处,避免输出看起来完整但责任不清。
Spec Skills 工作流包:用 Spec Skills 做 API 契约评审 本次要做的决策: - 把这篇文章里的工作流应用到一个真实工单,并确认输入、边界、输出和人工评审点。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: 工具边界:模型可以起草结构和问题,范围、契约行为、发布风险仍然必须由责任人确认。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
编辑复核记录
复核日期:2026-04-28。本次补充了可复用产物,按相关主题 Hub 检查了文章定位,并收紧下一步链接,让页面更像可操作参考,而不是孤立长文。
专题阅读路径
这篇文章归入 API 契约 主题。先读 Hub,再结合下面的清单、模板或工具落到具体项目里。
继续阅读
编辑说明与免责声明
最近复核:2026-04-28。编辑部检查了示例、内链和可复制评审片段,确保内容更适合真实项目使用。
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面