文章指出升级前最常见的错误是只看目标版本的说明,而应该比较所有中间版本;介绍了从容器镜像或lockfile精准定位实际运行版本的方法,以及只过滤可能影响生产行为的变更的技巧。
先确定实际的范围
最常见的错误是只读目标版本的发布说明。你需要的是从实际运行的版本到目标版本之间所有的发布说明,这个数量通常比任何人预期的都要多,因为 manifest 里的版本是一个范围,而生产环境的版本是 lockfile 在几个月前解析出的具体值。
# 实际安装的版本,而不是 manifest 允许的版本
pip show openai | grep -i version
npm ls openai --depth=0
# 在容器里,问镜像而不是仓库
docker run --rm your-image:prod pip freeze | grep -i '^openai'
对着运行中的产物做这件事,而不是你的笔记本。没有重新安装过的笔记本会愉快地报告生产环境早已停用的版本。如果你是从镜像部署,镜像才是权威来源。
把两个版本号都记下来。下面所有的操作都限定在它们之间的范围内,而这个范围就是当 changelog 过于简略时你要粘贴到仓库比较视图里的内容。
四个类别,按优先级排序
把范围通读一遍,把每个条目归入四个类别之一。不要追求理解去读,要分类去读。
最高优先级类别,也是公告最少的类别,因为在语义化版本下一个默认值的变更不是 API 变更,可以随小版本发布。要找的是任何改动默认超时时间、默认重试次数或退避策略、默认 base URL、默认请求头,或者客户端现在开始发送而之前省略了的参数的条目。这些条目会产生没有任何代码 diff 却导致生产图表变动的情况——这正是版本固定(pin)存在的意义,要让这种故障变得可追溯。这个类别里的任何内容在发布前都需要测试,而不是只读一遍。
方法、类、模块路径、异常类型、枚举成员。这些会报出 loud 错误,所以成本低:类型检查器或测试套件会找到它们,工作是机械性的。记下来,但不要在这里消耗你的注意力。经典案例是 OpenAI Python 客户端的 1.x 重写,把模块级别的 openai.ChatCompletion 入口点完全移除了——这种升级不可能在生产环境里悄无声息地过去,因此不是需要担心的那种。详见修复页面对这类 break 的处理。
涉及重试、退避、超时、连接池、流式关闭或取消的条目。这些在所有 happy path 里都是不可见的,却决定了你下一次供应商事故时会发生什么。变更的重试默认值同时属于第一类和第三类,如果你看到一个,先去读代码而不是相信摘要行。
新增到类型联合的模型、新增的可选参数、文档字符串、生成代码的刷新、仅测试的变更。扫一眼标题然后跳过。你不传的新可选参数不会改变你发送的内容。
如果 changelog 很长,机械地做第一遍。把这个范围的发布说明保存到一个文件,然后搜索标记第一到第三类目的词汇——发布方在格式上不一致,但在用词上相当一致。
grep -inE 'break|breaking|default|remov|renam|deprecat|retry|retries|backoff|timeout|stream|error|exception|required|no longer|now sends|behaviou?r' CHANGELOG-range.md
列表里有两个词因为非显而易见的原因值得保留。required 捕获了之前可选的参数变成必填的情况,这会在你没有覆盖到的代码路径上导致运行时失败。no longer 捕获了发布方在移除行为但不称之为移除时的措辞——"the client no longer retries on connection errors" 是一个写成了散文的第三类条目。
读每一个匹配结果。丢弃掉那些涉及你没有用到的表面的条目。剩下的通常在几百条里只剩三到六条,这才是你的升级真正涉及的内容。
当 changelog 不够用的时候
对于一个重度生成的客户端,changelog 经常只说"update API shapes"然后就完事了。当摘要行太单薄无法分类时,去源头:两个 tag 之间的比较视图展示了真正的 diff,对于生成式客户端,有意思的文件既小又少。看请求构造路径和客户端构造器默认值;跳过生成的模型类型,它们很大且几乎总是增量的。
两个提供商公开发布这些,直接读构造器默认值比去找别人的摘要花的时间更少——openai-python 仓库和 anthropic-sdk-python 仓库都在 releases 页面保留了发布说明,diff 只需点击一下。
这些 API 的客户端库基本是代码生成的,所以一个版本号可以跳好几个小版本而没有任何行为变更,一行条目可能隐藏一个默认值的变更。两种方向都无法从版本号推断出来,这就是为什么要读范围而不是估算。
从生产产物记录已安装的版本和目标版本。两个版本号,写下来。
获取这个范围内每次发布的发布说明,不只是目标版本,拼接成一个文件。
运行上面的 grep。读每一个匹配结果并归入四个类别。完全丢弃第四类。
对于每个第一类条目,写一个测试来断言你实际发送的请求体。一个 recorded-HTTP fixture 就够了:断言序列化的 body 包含你期望的字段,且不包含你从未设置的字段。这是能捕获客户端开始替你发送新内容的测试,无需部署或 API key 就能运行。详见 SDK 升级后默认参数变更的测试。
对于每个第三类条目,把新的默认值与你服务显式设置的超时和重试值进行对比。当 SDK 默认值现在对你有影响时,停止依赖它并在代码里设置这个值——显式设置的值不受下一次变更影响。
在分支里升级,跑测试套件,再在版本边界上跑一个固定输入的比较:output-change 测试正是为这种情况存在的,属于同一个 pull request。
把升级单独部署,不在 release 里夹杂其他变更,然后观察一个完整流量周期内的 p95 延迟、按类别分类的错误率,以及每个请求的 token 使用量。这三个图表是漏掉第一类条目后会显现的地方。
版本固定实际上保护你免受什么影响
自动 SDK 依赖升级后的破坏性变更修复
主要 SDK 版本升级后默认参数变更的测试