跳到主要内容

工具 Schema 演进陷阱:当一个可选参数改变了你 Planner 的先验分布

· 阅读需 11 分钟
Tian Pan
Software Engineer

在某个周二,一个全新的可选参数被添加到了工具描述中。这个改动很小——在 diff 中只有六行代码,没有破坏性的签名变更,没有更新调用者,也没有触及任何评估用例。PR 描述写着“为现有搜索工具添加了可选的 language 过滤器支持”。两名评审员批准了,随后上线。

一周后,成本仪表板显示,搜索工具的调用频率比之前的基准线增加了 18%。受影响的 agent 延迟也以大致相同的比例攀升。没人能指出哪一个评估用例失败了。新参数在使用时表现正常;在不使用时,也无关紧要。然而,planner 显然改变了它对何时使用该工具的看法——而评估套件(用于衡量工具的“正确性”)对于工具“频率”的变化却无话可说。

这就是工具 schema 演变陷阱,它几乎困扰着每一个在生产环境中发布 agentic feature 超过 18 个月的团队。这个陷阱是无形的,因为这种失败模式看起来并不像失败。没有调用中断,没有输出畸形。根据任何合理的定义,该 schema 都是向后兼容的。唯一改变的是模型的先验知识(prior)——即在 planner 步骤的参数中编码的隐含信念,关于这个工具对于流经 agent 的查询有多大的能力和适用性。工具描述是 prompt surface 的一部分。它们在每一轮对话中都会被读取。对描述的编辑就是对 prompt 的编辑。

工具描述就是 Prompt 补丁

大多数团队对待工具 schema 的方式就像对待 OpenAPI 规范一样:将其视为存在于代码中、由工程团队所有、并通过常规代码审查进行更改的机器可读契约。相比之下,prompt 被视为行为表面——它们有 A/B 测试框架,有评估门控,还有标注了促使每次编辑的失败模式的版本历史。

这种拆分在理论上是合理的。Schema 是结构化的,prompt 是自由格式的。Schema 由 agent 运行时确定性地消耗;prompt 则由模型以概率方式解释。两种产物,两个领域。

在实践中,模型在每一轮对话中都会读取这两个产物,它并不知道你的团队认为哪个是哪个。一条写着“当用户询问可用性时,使用此工具搜索产品目录”的工具描述,其改变先验知识的权重与 system prompt 中写着“当用户询问可用性时,调用搜索工具”的句子完全相同。模型无法区分它们。它们被连接到同一个上下文窗口中,由同一个 tokenizer 进行分词,并被同样的注意力头所关注。

这一推论对于大多数团队组织工作的方式来说是不太舒服的。负责 agent 行为(prompt)的团队并不拥有工具描述。而拥有工具描述的团队(工程团队)却没有针对它的行为测试套件。两者之间的交接陷入了缝隙,而缝隙两边的每一次编辑,在另一边看来都是一次未经评估的 prompt 编辑。

为什么可选参数特别棘手

这种特定的失败模式——一个新的可选参数导致工具调用频率虚高——有一个值得命名的机制。当模型看到一个带有 queryfilters 参数的工具时,它会根据描述和参数集形成一个关于该工具适用于哪些查询的先验。当你添加第三个可选参数(比如 language)时,三件事会同时发生。

首先,描述为了解释新参数而变得更长,而更长的描述往往会被理解为功能更强大的工具。其次,参数列表本身扩展了模型对该工具功能的心理模型;即使参数是可选的,它的存在也是一种能力信号。第三,周围的示例或提示(如果你为了演示新参数而添加了任何示例)会将 planner 推向与这些示例相似的查询,即使用户的实际查询与其无关。

这些影响都不是 bug。如果新参数真的扩大了工具的适用范围,那这些正是你想要的结果。但添加参数的团队很少考虑“适用范围”。他们考虑的是某个需要语言过滤的特定上游功能。他们预计在 95% 不受影响的调用中,工具的行为将保持不变。这个预期是错误的,而且评估套件无法告诉你真相。

工具描述的膨胀会加剧这种效应。Anthropic 的内部测试显示,58 个工具可能会消耗约 55,000 个 token,在复杂的目录中,组合 schema 可能会为每个请求增加数千个 token——即使是 agent 从不调用的工具也是如此。当工具选择准确率因目录膨胀从 43% 骤降至 14% 以下时,团队并不总能判断这种退化是来自最新的编辑,还是来自过去一个季度累积的膨胀。

针对工具模式的语义化版本 (Semver)

捕获这类回归的纪律,始于像对待任何其他版本化 API 一样对待工具模式(tool schemas)——但对“破坏性变更”的解释要从行为角度出发,超越签名兼容性。

修改参数列表的模式编辑至少是一个次要版本(minor version)。改变措辞的描述编辑,如果可能改变规划器的先验倾向——例如“使用此工具进行”(use this for)对比“这可以”(this can)对比“你可以”(you may)——也至少是一个次要版本。真正琐碎的编辑,比如修复不改变含义的拼写错误,则是补丁版本(patch)。版本控制的目的不是为了搞合规性表演,而是在 PR 提交时标注哪些编辑需要行为评估门禁,而哪些不需要。

次要模式版本的评估门禁必须对频率进行评分,而不仅仅是正确性。团队需要一个关于每个工具在代表性生产流量中被调用频率的基准——称之为工具调用频率基准(tool-call frequency baseline)——如果编辑后在相同流量下的频率差异超过某个阈值(5% 是一个合理的起点;根据生产环境进行调整),门禁就会拦截该 PR。

这是目前大多数技术栈中所缺失的评估。工具调用评估通常关注模型是否为给定查询选择了正确的工具,以及它生成的参数是否符合模式。这两者都是正确性衡量标准。但两者都无法回答以下问题:规划器现在伸向某个工具的频率是否超出了应有的范围?或者一个原本作为最后手段的工具,现在是否变成了第一直觉?

描述风格作为一种行为表层

除了版本控制,团队还需要一份描述风格指南,列出已知会改变规划器先验倾向的措辞。这份指南应该简短且具体,因为其效果是具体的。

“使用此工具进行 X”(Use this for X)读起来像是一条指令,会让规划器偏向于调用。 “这可以执行 X”(This can do X)读起来像是能力陈述,更为中性。“当你需要 X 时,你可以使用此工具”(You may use this when X)则进一步留有余地,让规划器偏向于不调用。偏向动词的能力列表(“搜索、过滤、排序”)读起来比偏向名词的能力列表(“一个产品搜索接口”)更广泛。嵌入在描述中的示例查询会比其前面的抽象能力描述更强烈地将规划器锚定在示例的表层形式上。

这一切都不是民间传说。在任何对比同一流量下两种工具描述并测量调用频率的 A/B 测试框架中,这都是可以观察到的。大多数团队观察不到这一点的原因是他们没有这种测试框架,而没有框架的原因是他们将工具描述视为代码而非提示词。解决方法是建立测试框架,在未通过行为评审的描述编辑上运行一次对比,让团队通过数据看到频率的变化。在那之后,这种纪律就会自我维持,因为打破过一次的工程师不想再打破第二次。

行为债务的弃用路径

一些仓促添加的参数将无法偿还其行为成本。团队为了某个上游功能添加了 language 过滤器,该功能在两个季度后被降低了优先级,但该参数却永远留在描述中——持续增加调用频率,持续在每次请求中消耗 token,持续编码一项团队不再想要宣传的能力。

工具参数的弃用路径需要像任何其他功能表层的弃用路径一样严谨。每个可选参数都应该有一个元数据块——所有者、动机、添加时的行为成本、预期使用率。每季度的审计会遍历目录,查看哪些参数在实际生产调用中被使用(不仅仅是智能体 可以 使用它们进行的调用,而是规划器实际上 选择 使用它们的调用),并修剪那些使用率低于其频率膨胀成本所能辩解的阈值的参数。

审计工作并不光鲜。它正是那种不会出现在 PR 描述中、不会出现在晋升材料中、也不会引起注意的工作,直到没做这项工作的团队开始看到与最近任何单一变更都对不上的工具调用成本激增。做了这项工作的团队每次审计能发现一两个值得修剪的参数,节省几个百分点的工具选择准确率,并为真正重要的调用收回一些规划器的注意力预算。

架构层面的认知

工具描述是提示词表层(prompt surface)的一部分。这句话很短,但其含义是结构性的。这意味着你的工程团队在 git 中进行版本控制的产物,位于提示词仓库过去两年一直在建立的同一套纪律下游——评估门禁、A/B 测试框架、弃用路径、风格指南、行为变更日志。

尽早意识到这一点的团队,可以在模式工程(schema engineering)和提示词工程(prompt engineering)的产物分化之前,设计好它们之间的接缝。意识到得太晚的团队会在成本仪表盘无缘无故飙升时发现,他们刚刚观察到的故障模式已经默默积累了数月——一打细小的描述编辑,虽然每个单独看起来都是合理的,但共同改变了整个工具目录中规划器的先验倾向,而撤回回归意味着要撤销那些原始动机已被遗忘的编辑。

一旦建立了纪律,这项工作并不昂贵。频率基准只需对一天采样流量进行一次批处理作业。工具描述的 A/B 测试框架就是团队已经在为系统提示词运行的同一个框架,只是指向了不同的产物。版本控制公约只是 CONTRIBUTING.md 中的一个段落和一行 CODEOWNERS。每季度的审计只是一个日历事件和一次查询。

昂贵的是事后才发现陷阱。以痛苦方式吸取教训的团队会在混乱的故障电话、对没人记得写过的描述编辑进行事后溯源分析,以及逐渐意识到版本控制系统永远不会发现这种回归中付出代价,因为回归不在 diff 中。回归在于 diff 对模型意味着什么——而这种意义是团队出于组织惯性决定不去衡量的东西。

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