当你为人类开发人员弃用一个 API 时,你会为此举行一整套“仪式”。你提升版本号,在 OpenAPI 规范中添加 deprecated: true,发送 Sunset HTTP 响应头,向开发人员邮件列表发送邮件,发布变更日志,并给人们六个月的时间进行迁移。信号到达阅读它的开发人员,他们提交工单,并在旧路径消失之前更新他们的客户端。
现在,将同样的弃用通知指向一个智能体。调用你工具的模型不会阅读你的变更日志,它不会订阅你的邮件列表。它永远看不到 Sunset 响应头,除非你刻意将其放在模型会查看的地方,即便如此,它也没有可靠的习惯去据此行动。你精心编写的弃用通知落入了一个没有读者的信箱。智能体会一直调用工具的旧形式,直到该形式彻底消失,然后它就会失败——通常是静默失败,通常是在生产环境中,通常是在凌晨 2 点。
这就是为智能体而非人类构建工具时所存在的隐性不对称。我们在 API 演进的二十年里建立的每一项准则,都假设在弃用和迁移之间坐着一个人类。把人类拿掉,整个机制就会失效。
这并非理论上的风险。对生产环境中智能体事故的从业者调查总会得出同样一个令人不安的数据:很大一部分智能体故障——有些团队认为高达 60%——可以追溯到工具和 Schema 的更改,而不是模型本身。模型没问题,是工具变动了,但没有人告诉模型。
为什么模型真的无法阅读通知
在“智能体无法阅读弃用通知”这一现象之下,隐藏着两种独立的失败,需要将它们理清,因为它们需要不同的解决方案。
第一种是训练截止(training-cutoff)问题。模型的权重编码了截止到某个日期的世界快照。如果你在该日期之后弃用一个函数、重命名一个参数或更改字段的含义,模型对此没有事后知识。一项关于 LLM 针对不断演进的库生成代码的实证研究发现,模型会自信地发出已弃用的 API 调用,恰恰是因为这些弃用的版本在它们的训练数据中占主导地位。模型并不是在忽略你的通知——它从未包含过你的通知,而且它对旧的处理方式有着强烈的先验偏好。
第二种失败是上下文问题,这也是你真正能控制的问题。即使一个模型在上下文窗口中拥有完美的当前工具 Schema,它也没有跨会话的持久记忆,也没有本能像工程师对待编译器警告那样对待 deprecated 注解。你可以交给模型一个在元数据字段中显示 deprecated: true 的工具定义,除非该弃用信息出现在模型被迫考虑的地方——例如描述文本、工具结果、明确的指令——否则它会欣然继续调用该工具。模型从未将其转化为推理过程的标志,就是不存在的标志。
将这两者结合起来,你就得到了核心的设计约束:针对智能体的弃用必须通过“带内”(in-band)传输,即在模型实际处理的有效负载内部,而不是在人类会参考的响应头、文档或仪表盘中“带外”传输。 通知必须成为对话的一部分,否则它根本就不是通知。
工具 Schema 是公共 API 合约——请像对待合约一样对待它
这是一个能修复大部分损害的认知重构。你的智能体看到的接口——函数名称、描述性文字、输入的 JSON Schema 以及输出有效负载的形式——就是一个公共 API 合约。其中的每一个字都至关重要,因为每一个字都会制约模型的行为。
REST 和 gRPC 团队在过去十年中通过惨痛的教训学到了这一点:你永远不要在不更改字段名称或提升合约版本的情况下更改字段的含义。增加式的变更是安全的。添加一个新的端点、一个新的可选参数、一个新的响应字段——这些都不会破坏现有的调用者。删除一个字段、重命名一个参数、更改一个类型或悄悄更改字段的含义:这些都是破坏性变更,一旦接触就会引发爆炸。
工具作者们由于刚接触这一领域,大多还没有内化这一点。更糟糕的是,工具合约的破坏范围比 REST 合约更大,因为描述文本也是接口的一部分。将参数描述从“用户的电子邮件”改写为“用户的主要联系方式”是模型可以感知的语义变化,即使 JSON Schema 没有发生一个字节的变化。对于 REST API 来说,这种修改只是文档更新;对于智能体来说,这是一种没有版本提升的行为变更。
这就是为什么“Schema 漂移”是如此可靠的隐形杀手。漂移不仅发生在类型中,还发生在名称、措辞以及工具用途的隐含合约中。因为模型在每次调用时都会根据当前文本重新推演其行为,所以漂移在你发布的那一刻就会生效——不需要客户端重新编译,不需要更新导入,任何人的编辑器中都不会出现红色波浪线。
从做得好的团队那里借鉴的一项实用准则:对智能体可见的整个表面——名称、描述、每个参数、输出 Schema——计算哈希值,并将该哈希值的任何变化视为 CI 中需要审核的事件。如果表面哈希值发生变动,必须由人工批准后才能发布。它将不可见的文字编辑转变为你可以实际推敲的差异(diff),这正是关键所在。
让弃用成为模型能够执行的操作 如果模型无法读取 Header,你就必须把弃用信息放在它能读到的地方:在循环(Loop)中。以下是几种模式,大致按成本从低到高排列。
在带内(In-band)警告,随输出一同返回。 当一个弃用的工具仍然可用时,将其结果封装在一个包含警告和数据的信封中——一个显式的 deprecation_warning 字段,带有停用日期和替代工具的名称。模型不仅得到了它想要的答案,还在它即将进行推理的 Payload 中收到了一个机器可读的、指向新路径的提示。这在 Agent 原生架构中等同于 Sunset Header,只不过它落在了模型真正处理的地方。
并行运行两个版本,然后迁移流量。 在迁移窗口期间,让旧工具与新工具并存。这是标准的 API 卫生习惯,但对 Agent 来说是不可商榷的,因为你无法强制客户端更新——这里没有客户端需要更新,只有需要引导的模型行为分布。有些团队会进行基于百分比的发布:将 5% 的调用暴露给新版本的工具,观察失败率和完成率,然后逐步提高比例。如果出现退化,将百分比设回零。即时回滚,无需重新部署。
永远不要悄悄地改变名称的用途。 如果一个工具的行为发生了实质性变化,请给它起一个新名字——例如 search_users_v2,或者一个完全不同的工具名——而不是在模型眼皮底下修改 search_users 的定义。新名字是模型无法忽视的信号,因为它必须主动选择调用它。而在不改变名称的情况下改变含义,是模型永远无法注意到的信号。
首选增量式演进。 最省钱的弃用是你永远不需要执行的那种。增加可选参数,永远不要删除必填参数;增加输出字段,永远不要改变现有字段的用途。如果旧的调用形式仍然有效,那么习得旧形式的模型就依然有效,这样你就赢得了时间,可以按照自己的进度而不是模型的进度进行迁移。
甚至协议层也在向这一点靠拢。Model Context Protocol 的生命周期策略现在定义了明确的 Active(活跃)→ Deprecated(弃用)→ Removed(移除)状态,并设有以月计的最小窗口期,正是为了确保弃用是一个有文档记录、分阶段的过渡,而不是突如其来的移除。无论在哪个层面,教训都是一样的:弃用是一个带有时间线的流程,而不是一个随手拨动的开关。
像对待破坏性变更一样对待工具合约 核心思想是,我们一直将工具变更归错了类。Prompt 的修改被 treated as 配置变更,工具描述的微调被 treated as 文档更新,参数重命名被 treated as 重构。但对于 Agent 而言,这三者都是对公开 API 合约的破坏性变更(Breaking Changes),它们理应获得与发布一个有上千付费客户依赖的 REST 端点 v2 版本同等的严谨对待。
具体而言,这意味着:对你的工具合约进行版本管理。在 CI 中对比 Agent 可见的完整表面(Surface),并在发生变动时要求审核。进行带内弃用,提供模型能在其上下文内部接收到的警告、明确的继任者以及真实的停用日期。让新旧版本并行运行足够长的时间,以观察失败曲线。当你最终移除旧路径时,请按照你宣布的时间表执行——这个时间表是给你自己看的(比如写在 Runbook 里),因为 Agent 也不会记得这件事。
你的 Agent 无法读取的弃用通知是一个更大鸿沟的缩影:我们正在将工具部署给一个没有记忆、没有变更日志订阅、并且对训练期间看到的内容有强烈先验偏好的消费者。你无法修复消费者,你只能让合约保持稳定,让变更可评审,并让弃用信号足够响亮,从而能够出现在模型唯一确定会看的地方——即它眼前的 Payload。为那个读者而构建,通知才会被真正阅读。
会员专享
余下内容仅对会员开放。 会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
— 完整文章,包含未公开存档的部分 — 可落地的工作框架,附带权衡与决策依据 — 新文章抢先看,先于公开发布 登录以继续阅读→ 随时取消 · 一次订阅,畅读全部