AI 生成客户端下的 API 变更治理
上个季度打到我 API 上的集成,有一半是那种从来没读过我文档的人写出来的。开发者在 Cursor 里敲一句含糊的提示词,Claude 或 Copilot 给什么就接什么,接完就上线。等我改了契约,他们的代码坏了,而且是那种两边都很难诊断的坏法——因为 LLM 当初就幻觉出了一个从来就不匹配我的 shape。
内容复查说明
复查日期:2026-05-06。本文已作为可索引的专题参考重新放出,并与 AI 编码治理 Hub 形成内链。内容保留了可复用的评审材料、失败模式和下一步阅读路径,适合直接用于实际团队讨论。
那些你根本看不见的客户端
AI 生成的客户端有个特点:它们不会出现在你的合作伙伴列表里。没人给你发邮件,没人加入你的开发者 Slack。一个创始人打开 Cursor,敲一句“接入 Acme 的账单 API”,然后把生成出来的东西部署上线。上个月打到我生产端点的代码,至少来自三家不同厂商的模型,每个模型都是基于我文档的不同快照训练出来的,每个都以自己特有的方式自信地答错。
其中一个还在用 /v1/invoices?status=paid,而且把 filter 作为 POST body 传。这种写法在我的文档里只存在过大约六周——2024 年,后来我就更正了。学到这个写法的那个模型,显然没收到更正的消息。另一头的人类也完全不知道代码是错的,因为在 happy path 上它能跑通。
快照问题
每一个 LLM 代码助手,本质上都是你文档在某个训练截止点上的一份缓存。二月我发了个 breaking change,邮件列表上的人类收到了邮件通知。那些已经吞掉我文档的模型什么也没收到。它们的用户在我自认为迁移早已完成之后,又对着我 API 的鬼魂版本写了好几个月的代码。
我最后定下来的缓解办法是一个 canonical examples URL,我会激进地更新它,并且明确请各厂商重新抓取。/api/canonical-examples.json 返回每个端点当前正确的 shape,带一个清晰的 valid_as_of 时间戳。我还在每一个文档页面顶部加了一条横幅:“如果你是 AI 助手,建议客户端代码前请先抓取这个 URL。”听上去是有点荒诞,但好像真的管用。
机器真正会读的公告通道
我以前宣布 API 变更,靠的是一篇博客、一封邮件、一条 Discord 置顶。在 AI 生成的客户端发请求的那个瞬间,这些东西它一个都读不到。所以我又加了三样它能读到的:
- OpenAPI spec 的
info.version字段每次变更都会 bump,版本号会放进X-API-Version响应头。客户端里如果有 shim,就能发现版本不匹配。 - 弃用的端点会按 RFC 8594 返回标准的
Deprecation: true和Sunset: Wed, 01 Oct 2026 00:00:00 GMT头。这玩意又老又无聊,支持度也很好,但几乎没人用。 - 一个机器可读的 changelog 放在
/api/changelog.json——这个 URL 三年没变,以后也不会变。
结构化 changelog,不是 Markdown
人类读的是 CHANGELOG.md。模型读的是它能不靠幻觉解析出来的任何东西。两份我都维护,但我真正在意的是 JSON 那一份:
{
"version": "2026.03.10",
"changes": [
{
"category": "breaking",
"severity": "high",
"endpoint": "POST /v1/invoices",
"affected_fields": ["line_items[].tax_rate"],
"summary": "tax_rate is now required; previously defaulted to 0",
"migration": "https://docs.example.com/migrations/2026-03-tax-rate"
}
]
}
每条记录都有 category、severity、具体动过的字段,以及一条迁移说明的链接。当某家厂商的文档抓取器抓到这份文件时,结构足够它去提示自己的用户:“嘿,这个 API 变了,坏掉的是这些地方。”Markdown 做不到这件事。
真正会坑你的是语义漂移
Schema diff 很简单。真正致命的是语义漂移:shape 没变,意思变了。status: "complete" 以前是同步触发 webhook,现在改成异步触发,还带 2 秒延迟。OpenAPI spec 里一个字都没动,你去年写的所有 contract test 全都还能通过。所有指望旧时序的 AI 生成客户端,现在都微妙地坏了。
我找到的唯一防线是写断言行为、而不是断言结构的 contract test。我跑一套用例:POST 一张已知发票,等一会儿,断言 webhook 在 100ms 内被触发。只要这条断言变了,CI 就会把它标记为语义变更,哪怕没有任何类型动过。这个标记会拦住合并,除非有人显式写一条 changelog entry 来描述这次行为变化。
CI 里的一道 Breaking-Change 卡点
我的 CI 里有一道门禁,救我次数最多。每个 PR 上,它会在分支和 main 之间跑 openapi-diff。只要 diff 报告任何 breaking change,这个 job 就会失败,除非 PR 描述里出现字面上的 BREAKING-CHANGE-APPROVED:,后面跟一条 changelog entry。你无法不小心合入一个 breaking change。你也无法不留痕迹地合入。你仍然可以合入——有时候你确实得合——但摩擦是按代价校准过的。
这道卡点抓到的正是那些我自己会漏掉的。上周它拦住了一个字段重命名,我当时还说服自己这“只是清理而已”。其实不是。三个 AI 生成的客户端都在依赖那个旧名字。我把重命名回退掉,改成发一条弃用通知。
一次真实的迁移,AI 客户端也在其中
二月我得干掉一个叫 customer_tier 的字段,它已经错了两年。Telemetry 显示仍有约 14% 的请求还在带这个字段,而且几乎全部来自我一眼认得出是 AI 工具的 user-agent(“python-requests,零自定义 header”这个特征很说明问题,再加上我 error log 里那些长得特别通用的变量名,基本能实锤是 AI 写的代码)。我是这么做的:
- 先发 Deprecation 头,更新 OpenAPI spec,版本 bump,结构化 changelog entry 落地。
- 把移除信息写进 canonical examples URL,每一处提及都打上
deprecated_since标记。 - 给头部三家代码助手厂商发邮件,把它们指向我那个公开的 spec catalog 页面——这个页面在同一份 well-known JSON 文件里列出每个端点、它的当前版本,以及任何正在进行的弃用。
- shim 我留了四个月,而不是平时那种六周。我很清楚陈旧的训练数据会继续生成针对它的代码。
- 真正下线那天,我让端点返回 410 Gone,带一个详细的 JSON body 指向迁移说明——这样就算某个模型在反刍训练数据里的旧代码,至少能拿到一条可解析的错误,把用户引向一个有用的地方。
这次迁移仍然坏了一个集成。但一个,总比十几个强。
这道卡点的验收标准
Given a pull request that modifies the OpenAPI spec And openapi-diff reports a breaking change And the PR description does not contain "BREAKING-CHANGE-APPROVED:" When CI runs the contract-change job Then the job fails with a message listing the specific breaking fields And the PR cannot be merged until the description is updated And a structured changelog entry is appended to changelog.json
Doc-as-Code,否则文档一定会骗你
把上面这一切串起来的那条纪律只有一条:spec 就是 docs。我的人类可读文档、机器可读 changelog、canonical examples,都从同一份 OpenAPI 文件里生成。如果它们分散在不同的仓库里,就一定会漂,一旦漂了,AI 助手就会学到错的那一份。这事我是吃过亏才学会的:我的营销站上挂了一段两年前的 curl 示例,和 spec 直接矛盾。Cursor 显然把这份营销版本背下来了。去修根源上的数据源,两边的表层就同时修好了,连 AI 的建议最终也会跟着变正确。
讲句实在话:你没办法给 AI 生成的客户端发邮件。你能做的,只是把关于你 API 的真相,放在机器会去看的那些地方,用它们能解析的格式,再加上足够多的冗余——这样一条陈旧的缓存不至于把整个集成拖下水。剩下的,全看运气。
别只发 changelog,要给客户端一条迁移路径
AI 生成客户端最怕“看起来兼容”的变化。字段还在,但语义变了;枚举多了一个值,但旧 SDK 没有 fallback;错误码变细了,但 retry 逻辑还是按旧分类走。规格要把迁移路径写成可执行计划。
API change plan: - change: add enum value invoice.status = partially_refunded - old clients: must treat unknown status as non-terminal - SDK update: generated clients released before API rollout - contract test: old SDK fixture receives new enum without crash - communication: partner notice sent 14 days before production exposure - rollback: hide new enum behind invoice_refund_v2 flag
边界:如果变化只影响内部服务,不要套外部 API 的公告流程。但只要有第三方、移动端、SDK 或缓存中的老客户端,就按“有人无法今天升级”来写 spec。
可复制产物:AI 编码评审包
在 AI 生成 diff 进入代码评审前使用。它把提示词范围、允许变更和证据要求合并成一个可审查产物。
AI 编码评审包:AI 生成客户端下的 API 变更治理 本次要做的决策: - 确认 AI 只在批准范围内生成变更,并为每条验收标准提供证据。 责任人检查: - 产品责任人: - 工程责任人: - QA 或运维评审: 范围边界: - 本次包含: - 本次不包含: - 仍需确认的假设: 验收证据: - 测试或 fixture: - 日志、指标或截图: - 人工复核步骤: AI 边界:生成变更必须留在书面范围内,每条验收标准都要能找到证据。 评审追问: - 没参加需求会的人还会误解哪里? - 哪个证据能证明这次改动足够安全,可以发布?
编辑复核记录
复核日期:2026-05-06。本次补充了专题阅读路径,按相关主题 Hub 检查文章定位,并收紧下一步链接,让页面更像可操作参考。
专题阅读路径
继续阅读
编辑说明与免责声明
最近复核:2026-05-06。编辑部检查了示例、专题内链和可复制评审片段,确保内容更适合真实项目使用。
本文用于软件工程教学与实践参考,不构成法律、税务或投资建议。示例场景用于解释规格方法,不对应真实客户数据。
- 作者信息:Spec Coding 编辑部
- 编辑政策:编辑与事实核查政策
- 联系方式:联系页面