协议版本与弃用窗口

8 分钟读完

O23
运维 · 智能体运维:部署与运营

协议版本与弃用窗口。

你的智能体平台如今多了一项依赖:它按别人的日历到期,而且坐在一条你只拥有其中一端的连接的两头——那就是协议版本。这跟模型弃用不一样:模型弃用是一家厂商、一个日期、一次由你掌控的切换;协议你根本升不了级,因为另一端什么时候升,取决于它自己。你能做的是「有意地同时跑两个版本」,而让这件事撑得住的那个运维单位,是一个几乎没人记录的数字:你线上流量里协议版本的分布。

STEP 1

这不是模型弃用,区别在于「谁得动」。

模型弃用固然烦人,但是可解的:厂商公布一个日期,你拿评测跑一遍继任者,切过去,而你服务的每一个调用方都跟着一起走,因为它们本来就没直接跟模型说话。整场迁移都在你的部署边界之内。那是模型弃用与迁移的题目,而它那套打法搬不到这里来。

协议弃用是一个双方问题。如果你跑的是工具服务器,你的客户端是别人按自己的发布节奏做出来的智能体宿主,其中一些被交付进了每季度才更新一次的环境。如果你跑的是智能体宿主,你的服务器是你钉不住版本的第三方部署。无论哪种,属于你的都恰好只有一端。

  • 你没法逼另一端动。最接近「逼」的做法,是在你自己公布的日期上直接报错,而那等于把一个兼容性问题换成了一次由你造成的故障。
  • 这种变更常常不是「只加不减」。这个生态里近期的协议工作删掉过握手、退役过请求头、整族地弃用过特性——这些都不是你能安全忽略的字段。
  • 你的库是第二本日历。「你的 SDK 实现了哪个版本」和「规范发布了哪个版本」是两件不同的事实,两者之间的落差经常以月计。上一层的同类问题见第三方工具漂移

在这里会失灵的那个直觉,是把「升一个协议版本」当成「升一个依赖」——改版本号、跑测试、发布。当依赖完全在你的进程之内时,这套是管用的。而协议住在两个进程之间的那道缝里,你手上的测试只覆盖了你这一侧。

STEP 2

你没有的那个数字:生产环境里按客户端分的版本分布。

问一个运维「你们的工具服务器说哪个协议版本」,你会得到答案。问「你们的调用方都说哪些版本、各占多少」,你多半只会得到一个耸肩。而后面这个数字,才是本页每一个决定的全部输入,并且它几乎是免费的——因为版本就写在请求里。

  • 在每一次请求上记录协商出来的版本,连同客户端标识和 SDK 的 user-agent。就是在你本来就在打的指标上加一个低基数标签。
  • 既要按流量占比报,也要按去重后的调用方个数报。这两者会严重背离:0.3% 的请求可能是 40% 的集成方,而后一个数字才是产生工单的那个。
  • 长尾要按身份拆,不要按流量拆。那个卡在两个版本之前的客户端,通常是某一个部署被冻结的重要客户,而你想在切换评审会之前就知道他们的名字,而不是之后。
  • 对「首次出现的新版本」告警。客户端升级会早于你的察觉;在一个你还没测过的版本上收到的第一个请求,是可能得到的最便宜的一次预警。

没有这个数字,每一个弃用决定都是凭感觉做的,而失败模式高度一致:团队要么因为无法证明「没人在用」而把老版本永远留着,要么直接砍掉、然后在事故群里认识那些用户。埋点做法见链路追踪与可观测性

STEP 3

用一套部署同时服务两个版本,而不是两套。

对双版本窗口,最显而易见的答案是两套部署:旧端点、新端点、DNS 分流。它同时也是最贵的答案,因为它把你真正在乎的东西叉开了——你的工具实现、你的鉴权检查、你的限流——而这道分叉会跟这个窗口一样长,比任何人计划的都长。

更好的做法是一套部署:按连接协商、向内归一,内部只有一种调用表示,版本这件事在边缘处理掉。

  • 按连接协商,而不是按部署协商。这是选框架时该找的一个属性——有的框架替你办了,有的把它扔给你。
  • 在边界上归一。把两个版本立刻翻译成同一种内部请求形状。一旦有一个「按版本分叉」的分支渗进了业务逻辑,你就是把两台服务器塞进了一个二进制里。
  • 让兼容层可删。把它收在一个模块里,并把移除日期写在文件里,这样退役一个版本就是删掉一个目录,而不是一场考古。
  • 报错要响亮而具体。真到了砍版本那天,错误信息必须点名版本、替代方案和一个链接,而不是一个 400。剩下那批掉队者里有一半是无人值守的任务,它们的负责人只会读到恰好一行日志。
STEP 4

先咬到你的是特性弃用,不是版本弃用。

团队盯着版本号,然后被版本内部的特性伏击。一个协议完全可以让版本保持稳定,同时给单个能力标上弃用、各走各的钟——通常是一个公布出来的最短窗口,常见是一年上下,从宣布它的那次发布开始算。这些钟到期的时候,你的版本还好端端地是最新的。

要单独跟踪它们:

  • 盘点你实际调用了哪些已弃用的能力,从链路数据里盘,而不是从代码评审里盘——那些你是「无意中」用上的(经由某个框架默认值),恰恰是 grep 找不到的那些。
  • 拿每一份发布说明去对这份清单做 diff。代价高的弃用都是安静的那些:一种传输被标为遗留,一个能力从核心里挪进了可选扩展。
  • 扩展不是保证。当一个特性从规范挪进扩展,它的可用性就变成了「因实现而异」。原先假定它一直都在的代码,现在需要一次能力探测和一条降级路径;见优雅降级与兜底
  • 把这些日期放进已经装着到期日的那本日历。凡是管着你证书和密钥轮换的日历,就该管这些。一个没有负责人、没有日期的弃用,是你会在事故里认识的那个弃用。
STEP 5

在 CI 里有意地留一个钉死在旧版本的客户端。

你手上所有的测试,跑的都是「你的服务器」对「同一个 commit 出来的客户端」——而这恰恰是生产环境里从不出现的那个组合。你发布出去的那个回归就是兼容性,而它对一套同版本的测试套件完全不可见。

  • 钉一个客户端在你仍然支持的最老版本上,每次构建都拿完整工具套件对它跑一遍。这个任务变红的那天,你是找到了下线日期,而不是造出了一次事故。
  • 再钉一个在最新版本上,包括预发布版。这是你对下一个窗口的早期预警,代价是多一个 CI 任务。
  • 断言要落在协商上,而不只落在结果上。一个会静默降级的客户端能顺利通过你的功能测试,同时对新版本的事一个字都不告诉你。
  • 把 fixture 放进仓库。按版本录下来的请求/响应对,比产生它们的那些 SDK 版本活得久;而当某个厂商声称「向后兼容」时,你拿来 diff 的就是它们。

这件事属于你那套保真度台阶的一部分——见给智能体做预发布环境

STEP 6

按版本滞后选库,不要按跑分选库。

公开的协议库对比讲的都是吞吐和内存,而对于一台「干活就是去调别人 API」的服务器,这些数字被上游那次往返彻底压住了——在一个耗时几百毫秒的响应里,差的是那么两三毫秒。真正会让你赔掉一个季度的那个数字,是这个库把一个新版本实现出来要花多久,以及它是不是规范维护者盯着的那个实现。

  • 用历史数据把滞后量成天数。对最近三个版本各记三个点:规范发布日、第一个实现它的版本、第一个稳定版。三个数据点对第四次的预测力,胜过任何路线图。
  • 确认「最流行的库」和「被跟踪的库」是不是同一个。在不止一门语言里,社区宠儿和官方维护的实现是两个不同的项目,处在不同的版本上——流行度不等于时效性,而换过去是一次重写,不是一次升级。
  • 明确的多版本支持,胜过一句承诺。README 里的「向后兼容旧版本」比路线图上的一条待办值钱,而且用 STEP 5 里那个钉死的客户端今天就能验。
  • 算的是迁移,不是移植。换库的成本不在重写那些 handler,而在把鉴权集成、链路追踪、限流以及围着它们的那些测试重新立起来。

这周做两件事,本页其余部分就会变成例行公事。第一,把协商出来的协议版本作为标签打在每一次请求上,并在错误率旁边放一块面板,同时按流量占比按去重调用方个数展示版本分布——没有它,这里一个决定你都做不了。第二,加一个 CI 任务,拿你声称仍然支持的最老版本上钉死的客户端跑一遍你的套件;它变红的那天,是你选的日期,而不是被塞给你的日期。之后,把已弃用能力的日期放进和证书到期同一本日历,并在下一次续期之前重新核一遍你那个库的历史版本滞后。延伸阅读:灰度与版本化看兼容层怎么发出去,生产中的 MCP 运维看这些事最先会在哪个协议上咬到你。