跳到主要内容

导致你的智能体重试机制失效两周的工具 Schema 迁移

· 阅读需 12 分钟
Tian Pan
Software Engineer

弃用通知是在周二发出的。下游团队更改了其搜索工具的响应结构 —— results[].snippet 变成了 results[].excerpt,这是一个干净的重命名,有六周的窗口期,文档中有横幅提醒,还给工程邮件列表发了三封提醒邮件。每一个人类用户都迁移了。但 Agent 没有,因为 Agent 不读邮件。在 14 天的时间里,重试循环静默地解析新的有效负载,发现它正在寻找的字段缺失了,抛出了一个 KeyError,并将其计为可重试的故障。重试命中了相同的端点,得到了相同的新结构,抛出了相同的错误,在尝试三次后放弃,并向用户返回了一条致歉消息。重试预算仪表盘在那段时间里一直显示为绿色 —— 重试次数从未 耗尽,它们只是在 预算内永久失败。在该路径上,从工具层测得的成功率为零。但没有人察觉,因为没有触发报警。

这是 2026 年最让工程师头疼的失败形式:不是那种戏剧性的停机,而是隐蔽的契约漂移(Contract Drift)。在这种情况下,面向人类的迁移已经顺利完成,而面向 Agent 的迁移甚至从未开始,因为没有人意识到还需要进行迁移。弃用流程完全按照设计运行,服务于它所设计的使用方。而 Agent 却是一个不在名单上的使用方。

这个故事最吸引人的部分在于,每个组件在隔离状态下的行为都是正确的。下游团队发出了通知。弃用窗口期很充裕。错误处理干净地捕获了解析失败。重试策略是有界限的。可观测性堆栈通过结构化日志捕获了每一次工具调用。每一个部分独立来看都能通过设计评审。失败发生在它们之间的缝隙中 —— 发生在于“弃用公告能传达到必须迁移的实体”这一假设,以及“绿色的重试预算意味着重试正在生效”这一假设。

Agent 未订阅的弃用通道

API 弃用运行在人类的基础设施之上。变更日志被发布,文档横幅出现,邮件发送到某人的收件箱,破坏性变更的日历邀请被接受。现在的行业最佳实践是在弃用警告和正式停止服务之间留出 6 到 18 个月的时间,这个窗口期的存在完全是为了让人们有时间阅读、确定优先级、安排计划并交付迁移工作。

调用相同端点的 Agent 只订阅了这些通道中的一个:响应体(Response Body)。它收不到邮件,也看不到横幅。虽然 DeprecationSunset HTTP 标头作为标准存在,但 Agent 的工具包装器(Wrapper)几乎肯定会在响应到达模型之前将它们剥离,因为包装器的编写初衷是提取 LLM 关心的部分 —— 即答案。弃用元数据停留在一个 Agent 层从未连接的侧信道中。

这在结构上与 Agent 错过人类操作员读取的其他操作信号的方式完全一致:速率限制标头、Retry-After 提示、状态页横幅、弃用通知。模型精通 JSON,却对它旁边的一切视而不见。保护每个普通用户的迁移窗口对你的 Agent 用户没有任何保护作用,因为这种保护机制是“阅读你的邮件”。

第一个教训是管理层面的,而非技术层面的:Agent 依赖的每个工具都需要一个负责人,该负责人必须在该工具上游的弃用通知名单中。不是指“团队”,而是一个具体的人,其工作职责包括“监控此工具的契约”。如果你无法指名道姓,那么你的 Agent 就没有迁移路径,只能等待故障发生。

为什么重试循环让情况变得更糟

一个出于好意的重试策略,正是将“明显的故障”转化为“持续两周的静默故障”的罪魁祸首。

当响应结构发生变化时,第一次调用的解析器抛出了异常。Agent 套件在工具执行中看到了异常,将其归类为瞬态故障 —— 与网络超时归为同一类 —— 并进行了重试。第二次调用返回了相同的结构。第三次调用也返回了相同的结构。在三次重试后,Agent 放弃并将该工具报告为不可用,编排器随后通过绕过该工具来处理。用户得到了一个更糟糕的答案,而不是一个错误。

三件事共同导致了这种静默:

1. 重试策略将解析错误视为瞬态错误。 大多数重试库默认对任何异常进行重试,因为另一种选择是罗列每个可恢复的错误类别并不断更新该列表。Schema 不匹配并不是瞬态的 —— 在代码更改之前,每次重试都会以同样的方式失败 —— 但重试层无法将其与网络波动区分开来。因此,它消耗了三次尝试和三组 Token 来证明同一件事。

2. 重试预算指标测量的是上限,而不是底限。 当每次调用都耗尽其三次重试机会时,“每小时使用的重试次数”仍保持在预算范围内 —— 三次毕竟只是三次。本应捕捉到这一点的仪表盘面板不是“已用重试次数”,而是“已恢复的重试”。从未恢复的重试是解析错误,而不是瞬态故障,你需要将它们分开。

3. 回退路径过于优雅。 编排器被设计为在工具不可用时平滑降级,当工具确实不可用时,这是正确的设计。但当工具总是失败时,这就是错误的设计,因为平滑降级与该工具根本不在计划中是无法区分的。Agent 静默地停止使用搜索工具,用户得到了更多自信满满的错误答案,却没有任何标记指出这一点。

能够捕捉到这一点的模式是按可恢复性层级对工具错误进行分类。网络超时是可恢复的 —— 重试它。带有 Retry-After 标头的 5xx 错误是可恢复的 —— 退避并重试。而针对已知 Schema 的解析错误在 Agent 侧是不可恢复的;Schema 改变了,重试多少次都无法修复。正确的行为是立即报警、显著地报错,并呈现预期结构与接收结构之间的差异(Diff),以便人类能清楚地看到哪个字段发生了变动。将这些层级混为一谈,正是导致长达 14 天静默故障的原因。

你以为你拥有的指标

“重试预算”(Retry budget)是那种听起来像安全带,最后却发现只是个虚荣指标的数据。预算限制了你的支出上限,但它并不能告诉你这些支出是否换来了价值。仪表盘上显示“重试率:1.2%,远低于 10% 的阈值”,值班工程师扫一眼就滑过去了,因为面板显示一切正常——没有触发告警。

而在绿灯掩盖下的真实情况是:

  • 一小部分调用正在不断重试。
  • 所有的这些重试都失败了。
  • 该路径上底层工具的成功率为零。
  • 整体 Agent 任务的成功率下降了,但由于它是针对多种工具和多种任务类型取的平均值,这种下降仅表现为 1% 的波动,看起来就像是正常的噪声。

那段描述中的每一个指标都在被监测,但没有一个被组合成能发出“重试层在此路径上已无法实现恢复”信号的告警。最接近有效告警的应该是:对于任何 (tool, error_class) 组合,当重试恢复率(retry-recovery-rate)连续一小时跌至零时发出告警。这与“重试预算”的指标形态完全不同——它按工具和错误类型进行细分,并观察一个导数指标(是恢复率,而非绝对计数)。这也是那种构建起来很麻烦、极易被略过的指标,而这恰恰是为什么没人拥有它的原因。

更深层的教训是:任何在所代表的事物已损坏时仍能保持健康值的指标,都不是指标,而是装饰品。在 Agent 系统中,重试率、错误率和预算利用率都属于这一类。真正有效的指标是恢复率、端到端任务完成度以及基于质量评分的输出检查——这些指标在用户体验下降时会变红,而不是在某个计数器超过某个数字时变红。

Schema 是 Agent 无法领会其言外之意的契约

下游团队的表现并不差。他们挑选了一个更准确的字段名。他们遵循了教科书般的弃用流程:发布公告、编写文档、双写过度、最后关停。迁移窗口期比大多数团队给出的都要长。从他们的角度来看,这是一个干净利落、执行良好的变更。

失败的原因在于,工具与 Agent 之间的契约并非 OpenAPI 规范或 README——而是响应负载(payload)精确且当前的形状。这个形状在 Agent 的工具封装器(tool wrapper)编写那一刻起就被锁定了,直到有人重新部署。将 snippet 重命名为 excerpt 在这个边界上是一个向后不兼容的变更,即使它在其他任何地方都是向前兼容的。因为消费者对该字段没有语义理解——它在 Python 中编译了一个 data["snippet"] 的查找逻辑,在有人修改查找逻辑之前,它在遇到 data["excerpt"] 时必定会失败。

以下几种实践可以缩小故障的影响范围:

将工具封装器视为编译后的客户端,而非配置。 它们应该有版本号。该版本号固定了 Schema 的形状。当 Schema 发生变动时,封装器的版本随之提升,并通过部署来发布新的封装器。Agent 层应拒绝调用版本低于工具当前次要版本(minor version)的封装器,就像你不会部署一个依赖项尚未解析的服务一样。

添加针对在线工具运行的契约测试。 这是一个每日运行的任务,使用已知输入调用每个已注册的工具,并根据封装器预期的 Schema 验证响应。当测试变红时,你能在几小时内发现契约已偏离,而不是几周后。这种工作虽然枯燥但行之有效,在它第一次触发告警时就值回票价了。

暴露弃用元数据。 如果上游发送了 DeprecationSunsetWarning 等 HTTP 头部,工具封装器必须转发这些信息——将其作为结构化元数据转发,而不是塞进模型的上下文窗口(context window)。Agent 编排器随后可以发出内部告警:“工具 X 报告将在 30 天后关停,目前尚无封装器更新计划。” 这是 Agent 层必须自己构建的旁路通道,因为没人会替你做。

显式地为工具契约标注版本。 如果封装器声明它预期的是 Schema v3,而上游滚动到了 v4,封装器要么升级,要么锁定版本。在未标注版本的形状之间悄无声息地漂移,就是让六周的弃用窗口变成两周的事故处理期的罪魁祸首。

“健康”究竟长什么样

最终目标并不是“我们永远不会有 Schema 漂移”。上游会不断弃用字段,工具形状会不断变动,这没问题——唯一的替代方案是让世界停滞不前。最终目标是 Schema 漂移能在几小时内被检测到,而不是几周,且这种检测机制不依赖于人类替 Agent 阅读电子邮件。

这需要三件事的结合:一个按工具和错误类型细分的恢复率指标,使永久性故障不会隐藏在健康的预算之下;一个能区分 Schema 不匹配与瞬时故障的解析器,并拒绝重试前者;以及一个独立于用户流量、定期探测每个工具在线响应形状的契约测试。每一项工作都很小,但结合在一起,它们能将静默故障窗口从几周压缩到一个工作日。

大多数团队在亲历此类事故之前都不会构建这三项机制。而在经历过之后,他们会发现这些工作的成本远低于事故造成的损失。缺失的一块拼图并非工程能力,而是意识到 Agent 是它所触达的每一个 API 的消费者,而任何消费者都不应该对其提供者——或对其自身——保持隐身。

重试预算仪表盘会为了它设计之初要捕捉的问题保持绿灯。但它永远捕捉不到这个问题。构建那个能捕捉问题的指标,并写下那个代表 Agent 阅读弃用邮件的人的名字。这就是你的迁移计划。

References:Let's stay in touch and Follow me for more thoughts and updates