契约优先的 SDK 生成与人工评审流程

契约优先的 SDK 生成与人工评审流程
Spec Coding 编辑部 · Spec-First 工程实践内容

如何基于 API 契约生成 SDK,又不让用户拿到一个冷冰冰的机器产物:哪些该生成、哪些该手写、以及真正关键的那道人工评审关。

发布于 2026-03-10 · 阅读约 13 分钟 · 最近更新:2026-04-28 · 作者:Spec Coding 编辑部 · 审校:编辑与事实核查政策

内容复查说明

复查日期:2026-05-06。本文已作为可索引的专题参考重新放出,并与 API 契约 Hub 形成内链。内容保留了可复用的评审材料、失败模式和下一步阅读路径,适合直接用于实际团队讨论。

我真正愿意用的 SDK,从来都不是全自动生成的

我发布过完全由 OpenAPI 规范生成的 SDK,也发布过完全手写的 SDK。两种都是错的。生成版本从技术上说能跑,但读起来像一本翻译过的说明书,每个做集成的工程师都要在自己的代码库里重新堆一层便捷封装。手写版本非常优雅,直到 API 一下子新增十五个接口,SDK 就落后了两个小版本。

我反复回到的那个形态是双层结构:一层是严格跟随契约的生成式底层客户端,一层是让 SDK 在宿主语言里用起来像原生库的手写高层接口。评审关就卡在这两层之间。

生成器真正擅长的事

当我把一份 OpenAPI 3.1 文档喂给生成器时,我只要四样东西,多一点都不要:与 schema 完全对齐的类型(包括可空联合类型和带判别符的多态)、镜像 operationId 的方法签名、请求与响应的序列化(包括那些烦人的细节,比如 date-time 强制转换和 base64 二进制块),以及分页原语——返回一页数据加一个游标 token。

这些事情,生成器做得比我快、也比我准。契约一变,我重新生成一遍,类型错误会把每一个改动过的调用点都标出来。单凭这一点,就足以让整套工具链值得存在。

生成器不擅长的事,以及我为什么不再强求

错误的人体工学。重试策略。合理的默认值。符合语言习惯的命名。一切需要品味的东西。

生成器会毫不犹豫地给你生成一个叫 listInvoicesV2 的 Python 方法,因为 operationId 就是这么写的。它会返回一个 HTTPValidationError 对象,里面有个 detail 字段是 ValidationErrorDetail 的列表。它不会在 503 时重试,因为规范没说要重试。它不会刷新你的 OAuth token,因为规范没说要刷新。对生成器来说,这些都是正确行为;对一个 SDK 来说,这些全都错了。

我实际发布的双层拆分

生成的包放在 acme._generated 下,它不属于公开导入面。文档里不会提到它。用户如果真要从里面导入,那是他们自己的事,他们也清楚这一点。

公开的接口面在 acme 下。每一个公开方法都是手写的。它调用生成层,捕获生成层抛出的异常,把它们重新包装成 acme.errors 里的类型,然后返回符合 Python 习惯的形状。这种拆分让生成代码可以笨一点、跑得快一点,而手写层则保持慢、保持讲究。

一个具体例子:发票、分页,和那点痛感

假设 API 暴露了 GET /v2/invoices,带一个 page_token 查询参数,响应里带一个 next_page_token。生成器产出的东西大概长这样:

response = client.invoices.list_invoices_v2(page_token=None, limit=50)
# response.data: list[Invoice]
# response.next_page_token: str | None

能用吗?能用。符合 Python 习惯吗?不符合。没有人愿意在业务代码里手写一个围绕游标 token 的 while 循环。于是手写层加了一层:

for invoice in client.invoices.list_all(status="open"):
    process(invoice)

list_all 是二十行手写代码。它在一个循环里调用生成的方法,逐项 yield,自己处理游标,如果生成层冒出 429,它就抛出 acme.errors.RateLimitError。用户永远看不到 next_page_token,也看不到 list_invoices_v2。他们拿到的是一个迭代器——这才是 Python 用户想要的东西。

错误:从解析后的 payload,到语言原生的异常

生成层返回的是一个错误对象。我的手写层把它翻译成宿主语言真正会用的东西。在 Python 里,是一整套真正的异常继承关系:AcmeError 做根,然后是 APIErrorAuthErrorRateLimitErrorValidationErrorNotFoundError。在 Rust 或 Go 里,是 Result 类型加一个带类型的错误枚举。在 TypeScript 里,是一个可判别联合类型,调用方可以在上面做窄化。

用户永远不应该去 catch 一个 HTTPValidationError。他们应该 catch acme.ValidationError,检查 err.field_errors,然后继续干活。这种翻译,是手写层做的最有价值的一件事。

重试、鉴权和命名,都归手写层管

我不让生成代码重试。一次都不让。生成的客户端发一次请求,报告结果就结束。重试策略是产品决策:哪些状态码要重试、哪些方法重试是安全的、退避怎么走、每次尝试要不要打一个指标。这些全都住在包着生成调用的手写中间件里。

鉴权也是一样。生成器暴露一个钩子,用来注入一个 header。手写层拥有 token 刷新、凭证存储和轮换。token 在请求中途过期时,手写层捕获 401,刷新一次,再重试一次。生成层完全不知道发生过这些事。

命名遵循同一条规则。operationId 是为 URL 路由写的,不是为一个开发者凌晨两点要敲的东西写的。listInvoicesV2 要变成 invoices.list。生成的名字逃到公开接口面上,就是一个 bug。

SDK 要独立于 API 做版本管理

API 的版本和 SDK 的版本几乎一上来就会分道扬镳。API 在 v2.17,Python SDK 在 4.3.1,因为上个季度 SDK 在分页迭代器上发了一次破坏性变更,和 API 一点关系都没有。这很正常。SDK 通过一个 __api_version__ 常量记录自己是针对哪个 API 版本生成的,手写层则可以按自己的节奏演进。

发布前的人工评审关

重新生成是自动化的。发布不是。任何东西进 PyPI 或 npm 之前,都要有一个人过一遍生成部分的 diff。不是手写层的 diff(那是正常 PR),是生成层的 diff。

我盯着看的是惊喜。有没有哪个接口悄悄把字段改了名?有没有哪个响应类型悄悄变成可选了?有没有多出来一个必填参数?有没有枚举少掉了一个变体?大多数重新生成都平淡无奇,但每个季度总会蹦出一个本来会静默伤害用户的东西,在这里把它抓住,就是一次干净发布和一场售后火灾之间的差距。

SDK 的验收标准,用 Given/When/Then 写出来

- Given 一份 OpenAPI 规范新增了一个必填查询参数
  When 生成器运行且 diff 被评审
  Then 评审关识别出破坏性变更并阻止发布

- Given 一个在请求中途过期的访问 token
  When 生成层冒出 401
  Then 手写层刷新一次并透明地重试

- Given 一个分页接口
  When 用户写下 `for item in client.invoices.list_all()`
  Then 迭代器跨页 yield 所有数据项,不把游标 token 暴露给用户

如果有一个团队今天要开工,我会告诉他们

不要把生成产物当成你的 SDK 来发。把它当成 SDK 里的引擎来用。把时间花在手写层上,哪怕这意味着上线时覆盖的接口更少——一个小而地道的 SDK,胜过一个完整的机器产物。每次发布前,都要有一个真人站在评审关。让两层独立地打版本号。当有人说“要不我们直接把生成的客户端暴露出去吧,这样更快”,说不,态度要硬。

评审时看什么

这篇文章适合用在契约优先的 SDK 生成评审时。别从“原则”聊起,直接拿一条真实改动来对照,看看规格里还缺什么。

SDK 评审不是看生成器输出多漂亮,而是看调用方是否能稳定处理错误和升级。

可复制产物:契约评审包

当工作涉及 API 行为、schema、事件、重试或消费者预期时使用。它会把兼容性和发布证据提前摊开。

API 契约评审包:契约优先的 SDK 生成与人工评审流程

本次要做的决策:
- 确认契约变化是否兼容,消费者需要什么迁移动作,发布后如何观察风险。

责任人检查:
- 产品责任人:
- 工程责任人:
- QA 或运维评审:

范围边界:
- 本次包含:
- 本次不包含:
- 仍需确认的假设:

验收证据:
- 测试或 fixture:
- 日志、指标或截图:
- 人工复核步骤:

契约边界:没有兼容性分类、消费者影响、重试行为和回滚说明,不进入发布。

评审追问:
- 没参加需求会的人还会误解哪里?
- 哪个证据能证明这次改动足够安全,可以发布?

编辑复核记录

复核日期:2026-05-06。本次补充了专题阅读路径,按相关主题 Hub 检查文章定位,并收紧下一步链接,让页面更像可操作参考。

关键词:SDK 生成 · OpenAPI · 契约优先 · 客户端库 · API 设计 · 分页 · 重试策略

编辑说明与免责声明

最近复核:2026-05-06。编辑部检查了示例、专题内链和可复制评审片段,确保内容更适合真实项目使用。

本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。