跳到主要内容

下线一个 Planner 已产生依赖的 Agent 工具

· 阅读需 11 分钟
Tian Pan
Software Engineer

你从工具目录中注销了 lookup_account_v1,换上了 lookup_account_v2,并修改了系统提示词中的一个段落来指向新名称。测试通过了。三天后,支持工单开始提到助手“一直尝试调用不存在的东西”,或者——更令人不安的是——它用自信、看似合理的数字回答客户问题,却根本没有查询数据库。弃用并没有在通信层失败,它在规划器(planner)中失败了。

!["https://opengraph-image.blockeden.xyz/api/og-tianpan-co?title=%E4%B8%8B%E7%BA%BF%E4%B8%80%E4%B8%AA%E8%A7%84%E5%88%92%E5%99%A8%E5%B7%B2%E5%AD%A6%E4%BC%9A%E4%BE%9D%E8%B5%96%E7%9A%84%E6%99%BA%E8%83%BD%E4%BD%93%E5%B7%A5%E5%85%B7"]

这是将工具弃用视为语法变更与将其视为行为迁移之间的差距。智能体不仅是在注册表中拥有你的函数;它还拥有数月的计划、多步配方(recipes)以及通过该函数作为检查点的 few-shot 示例。撤掉它更像是停用下游服务非正式硬编码的内部 API——只不过下游服务是一个你无法通过 grep 搜索其习惯的模型,而且当它偏好的工具消失时,它的兜底方案是编造一个。

工具退役(Tool retirement)是智能体工程中最容易导致 API 版本化策略失效且代价昂贵的部分。调用已删除端点的 REST 消费者会收到 404 和堆栈跟踪。而“调用”已删除函数的智能体可能会针对旧名称发出幻觉工具调用,被运行时拒绝,尝试使用幻觉出来的参数形状进行重试,最后——在耗尽重试预算后——给出一个流利的回答,满足用户可见的交互,同时悄悄跳过它本应执行的查询。通信层(wire-level)的失败很容易被发现。而行为上的失败才是真正上线的隐患。

模型究竟对你的工具内化了什么

当工程师认为“智能体正在使用 search_orders 工具”时,他们往往会想象一个清晰的依赖链:提示词 → 规划器 → 工具规范 → 工具调用。现实情况要复杂得多。模型对工具至少有四个重叠的表示,当你退役工具时,每一个都必须更新。

第一种是运行时注册表中的 schema。这是你唯一真正拥有并能进行原子更改的表示。第二种是模型在当前轮次中看到的上下文工具列表——通常是从注册表渲染的,但可能缓存你的提示词模板中,有时为了提示词缓存的稳定性而固定在特定版本,偶尔还包含着没人注意到的旧描述副本。第三种是系统提示词中的 few-shot 示例,它们演示了成功调用的规范形式:“要通过 SKU 查找订单,请调用带有 {sku, region}search_orders。”这些示例是六个月前针对旧工具调整的,现在它们在可用工具方面对规划器撒了谎。

第四种,也是最令人不安的,是模型在预训练或微调过程中吸收的与你的 API 模式类似的模式。如果你的工具命名很通用——比如 searchget_userlookup——模型很有可能会产生这种形状的调用,而不管你的注册表里写了什么,因为在许多类似语境下,这是高概率的补全。重新训练注册表并不能消除这种先验(prior)。补救措施是结构性的:使用独特的名称、独特的参数形状,并针对已知会触发先验的提示词进行激进的 eval 覆盖。

前两个你可以通过一次部署来更新。第三个需要你找到代码库中引用旧工具的每个系统提示词和 few-shot 示例——而这正是大多数团队忘记查看的地方。第四个则需要你假设存在敌对的先验信念,并围绕它们进行设计。

阶段性退役模式

能够实现平稳退役的规范借鉴了十年前 protobuf 操作员处理字段弃用的方式:永不破坏,始终重叠,观察行为,然后移除。

标记弃用,不要移除。 第一步是更新注册表中的工具描述,用模型能读懂的平实语言声明弃用。“DEPRECATED(已弃用)——请使用 lookup_account_v2。此工具将于 YYYY-MM-DD 移除。”模型每一轮都会读取工具描述。这句话是你最廉价的行为引导,只需花费几个 token。关键在于,工具在此窗口期间仍然有效——调用它会返回相同的数据。你没有破坏任何东西;你只是在宣传变更。

使用不同名称双重注册新工具。 不要抵制诱惑,不要将 lookup_account_v2 作为同名的直接替代品发布。独特的名称为规划器提供了可以感知的“新鲜度”信号,并允许你构建对比两者的 eval。新工具的描述应明确引用旧工具:“替代 lookup_account_v1。除了现在需要 region 外,协议完全相同。”这种交叉引用是规划器可以消费的提示目录,而无需你重写每个系统提示词。

带有遥测的软删除窗口。 在迁移期间,对已弃用工具的每次调用都会成功——并发出你的平台可以跟踪的弃用事件。你需要一个能回答两个问题的仪表板:哪些会话仍通过旧工具路由,以及哪些提示词产生了这种路由?第一个问题告诉你何时可以安全移除。第二个问题告诉你代码库中哪些 few-shot 示例和下游解析器仍需更新。弃用事件是元数据,而不是失败;它是你用来安排实际移除的信号。

带有结构化故障模式的硬删除。 当软窗口结束时,不要对旧工具的调用静默返回 404。返回一个模型可以恢复的结构化错误:“工具 lookup_account_v1 已于 YYYY-MM-DD 退役。请改用 lookup_account_v2。协议相同。”模型可以读取该错误并重新规划;而通用的异常则做不到。跳过这一步的团队会发现,他们的智能体通过对已失效的函数幻觉参数、陷入重试循环、最终在没有任何工具调用的情况下给出答案来响应移除。

这种模式的形状应该让人感到熟悉。它与任何版本化 API 运行的阶段性契约——弃用、双重发布、遥测、移除——是一样的。不同之处在于消费者:规划器比严苛的客户端更宽容(如果你足够坚定地指向它,它会采用新名称),但也更危险(当它无法采用时,它会自己发明一个)。

回退型幻觉故障模式

工具弃用(retirement)处理不当最糟糕的结果不是智能体(agent)大声报错,而是它在悄无声息中出错——生成一个看似合理但从未查阅过真实数据源(source of truth)的回答。

路径通常是这样的:规划器(planner)在其短期上下文中有一个多步骤方案:“要回答余额问题,先调用 lookup_account_v1,然后对结果进行格式化。”你删除了该工具。在处理下一个余额问题时,规划器发出了对 lookup_account_v1 的调用。运行时(runtime)拒绝了它。规划器在追踪(trace)中看到了拒绝。大多数现代智能体开发框架(harness)会重试一次,也许会使用稍微改变的参数形状(“也许它需要的是 account_id 而不是 id”)。重试同样失败。此时,规划器已经用完了本轮的工具预算,而方案中的下一步——“格式化结果”——没有结果可供格式化。它接下来的动作取决于框架、模型和提示词。

防御完善的框架会拒绝回答并暴露错误。而防御松散的框架则会让模型生成一段流利的文字,自信地讨论用户的“当前余额”,因为用户最近的消息中提到过一个数字,系统提示词引用了典型的余额范围,且模型处于一种生成听起来合理的数字答案是训练时最常见的补全模式的语境下。用户读到答案,觉得很合理,然后继续处理自己的事。而团队在一周后通过客户投诉才发现问题,因为那个数字根本是错的。

防御分为两个层面。首先,你的运行时永远不应允许智能体在所需的工具调用失败的情况下给出最终回答——这是一个框架层面的不变性(invariant),而非针对每个功能的决策。其次,你的弃用计划应包含已知历史上会路由到该弃用工具的评估(eval)提示词,并且这些评估应断言新方案能达到相同的结果。如果弃用后的方案生成了流利的回答但没有成功的工具调用,评估应当失败。这两层防御都已存在,但均未普及。

行为差异评估才是真正的测试

教科书式的工具弃用评估会对比弃用前后的最终回答准确性。这固然必要,但并不充分。它只能告诉你系统是否仍能得到正确答案;它无法告诉你系统是否以正确的方式得到答案。

你真正需要的是行为差异对比(behavioral diff):对于一组过去路由到该弃用工具的固定输入提示词,记录旧配置和新配置下的完整轨迹——每一次工具调用、每一个参数、每一个中间观察结果。逐步对比它们。你想要解答的问题不是“答案是否匹配”,而是“规划器是否触达了新工具,是否在第一次尝试时就传递了正确的参数,是否需要重试,方案长度是增加了还是减少了,它是否完全跳过了一个步骤?”

目前的智能体评估生态系统——如 AgentEvals 等框架和行为回归工具——很好地支持了轨迹级评估,但大多数团队将其用于模型升级而非工具变更。工具弃用恰恰是这些工具所针对的变更形式。你所需的数据集并不庞大;三十到五十个具有代表性的提示词(通过弃用遥测确认过去曾路由到旧工具)就能揭示新方案是否健康。在旧的和新的工具配置下运行差异对比,让轨迹之间的差异——而不只是答案的差异——成为发布的门槛。

这种投入在下一次变更时会再次获得回报。同样的轨迹数据集将成为你未来对该工具进行任何更改(包括下一次弃用、下一次参数形状更改以及下一次系统提示词重写)的回归测试套件。

将工具目录视为受版本控制的 API

这一切背后的架构认知很简单:智能体的工具目录(tool catalog)具有弃用合约(deprecation contract),就像任何其他受版本控制的 API 一样。消费者是模型而非服务这一事实,并不能免除目录的纪律约束;相反,它提高了门槛。

版本化 API 具有发布的日落请求头(sunset headers)、双重发布窗口、弃用端点使用的遥测以及迁移指南。工具目录需要对等物:模型可读取的模式级(schema-level)弃用标志、双重命名的工具版本、基于会话和提示词模板的弃用遥测,以及针对轨迹而非答案的迁移评估。如果团队通过代码更改(git rm、重新部署、更新提示词)来发布移除操作,那就是在将行为迁移视为语法迁移。而通过阶段性合约发布移除操作的团队,其智能体才不会悄无声息地开始幻觉出账户余额。

前瞻性的转变是将这种纪律推向平台级。如果你的工具注册表跟踪弃用标志,你的评估框架可以在允许标志从“软”转为“硬”之前,自动构建新旧版本之间的轨迹差异。如果在构建时扫描提示词模板中的工具名称引用,那么当弃用工具的名称仍出现在少样本(few-shot)示例中时,构建本身就会失败。目前大多数团队都是在发生事故后才偶然手动运行这些检查。而那些交付可靠智能体的团队则默认在事故发生前运行它们。构建这种纪律的成本是一个工程师周的平台工作。而跳过它的代价是无限期地每季度发生一次事故。

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