契约优先的 SDK 生成与人工评审流程
如何基于 API 契约生成 SDK,又不让用户拿到一个冷冰冰的机器产物:哪些该生成、哪些该手写、以及真正关键的那道人工评审关。
内容复查说明
复查日期: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 做根,然后是 APIError、AuthError、RateLimitError、ValidationError、NotFoundError。在 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 发布前至少用真实示例跑一次调用链。
SDK 评审不是看生成器输出多漂亮,而是看调用方是否能稳定处理错误和升级。
可复制产物:契约评审包
当工作涉及 API 行为、schema、事件、重试或消费者预期时使用。它会把兼容性和发布证据提前摊开。
API 契约评审包:契约优先的 SDK 生成与人工评审流程 本次要做的决策: - 确认契约变化是否兼容,消费者需要什么迁移动作,发布后如何观察风险。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: 契约边界:没有兼容性分类、消费者影响、重试行为和回滚说明,不进入发布。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
编辑复核记录
复核日期:2026-05-06。本次补充了专题阅读路径,按相关主题 Hub 检查文章定位,并收紧下一步链接,让页面更像可操作参考。
专题阅读路径
继续阅读
编辑说明与免责声明
最近复核:2026-05-06。编辑部检查了示例、专题内链和可复制评审片段,确保内容更适合真实项目使用。
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面