更好的模型,更差的工具调用表现

最近两天,一个非常奇怪的Pi 问题 把我引向了迷宫般的境地。简而言之,最新的 Claude 模型有时会在嵌套的 edits[] 数组中,额外添加一些虚构的字段,然后调用 Pi 的编辑工具。而且,这可不是 Haiku 或其他小型模型——而是 Opus 4.8。编辑本身通常没有问题,但参数与 Schema 并不匹配:模型自创了一些虚构的键值,因此 Pi 拒绝接受该工具调用,并要求重新尝试。

仅凭这一点并不算太令人意外,因为模型有时会发出格式错误的工具调用,尤其是那些规模较小的模型。 真正让我惊讶的是,随着 Anthropic 新型模型的不断升级,这一问题愈发严重——Opus 4.8 和 Sonnet 5 都出现了类似的现象,而旧型号却并未出现这种情况。 换句话说,这个家族中的最新模型在处理特定的工具 Schema 时,表现远不如它们的老一辈。”

如果你对 Fable 感兴趣的话:我特意没有对其进行测试,因为我担心他们所使用的分类器可能会在悄无声息间将我降级为 Opus。

工具调用是文本形式

如果你还没有花太多时间去研究 LLM 工具调用的内部机制,那么要理解的一点是:工具调用并非魔法,而是依赖于一种相当粗陋的带内信号传输方式。 模型会收到一段转录文本、一条系统提示以及一份可用工具列表。 服务器会将这些信息整合成一个包含特殊标记符的长篇提示。 由于该模型是在这种格式的示例上进行训练和强化的,在生成过程中,它会发出某种内容, 而 API 或客户端则会将其解读为:“请用这些参数调用这个工具。 ”

对于文件编辑工具来说,预期的调用负载可能如下所示:

随后,一个调度器会对这些参数进行验证,执行编辑操作,并将结果反馈回模型。如果验证失败,模型会看到错误信息,并通常会再次尝试。

至于 Anthropic 模型的具体格式化过程,目前尚不清楚; 不过,有些人已经成功获取了“ANTML”标记符,这些标记符有时也会泄露到公开通信中。 据我所知,上述调用在模型端的输出形式大致如下:

需要注意的是,尽管这个结构看起来像 XML,但它其实并不是真正的 XML。这只是他们为了便于分词和训练而特意设计的一种形式。 另外,值得注意的是,基本的顶级字符串参数是直接写入的,而对象数组则通过 JSON 序列化来实现。虽然我并不完全确定这究竟是如何运作的,但有一些迹象表明,这种做法其实并不离谱。 这一点稍后还会变得尤为重要。

有两种截然不同的方法可以促使模型生成这样的结构:

1. 你可以请求模型生成符合 Schema 规范的有效 JSON,并在之后对其进行验证。

2. 你可以限制采样器,从而从根本上杜绝无效的 JSON,甚至无效的 Schema 形式被采样。

第二种方法通常被称为“语法感知”或“约束解码”。 采样器会屏蔽那些违反语法的标记符。如果模型当前处于 JSON 对象中,而 Schema 规定只允许使用 oldText 和 newText,那么采样器就可以阻止其发出 "in_file" 或 "type"。语法感知解码既可以用来约束某段内容必须是语法上有效的 JSON,也可以用来强制指定特定的枚举值或键。

如果没有任何形式的约束,模型只是在遵循一种已学得的惯例。

失败的原因

Pi 的编辑工具支持在一次调用中进行多次精确的字符串替换。这也是为什么参数中包含一个 edits 数组。在失败的案例中,模型会生成如下的条目:

或者这样:

在反复试验中,我发现了大量虚构的尾部键:type,id,kind,unique,requireUnique,matchCase,in_file,forceMatchCount,children,notes,cost,oldText2,newText2,oldText_2,newText_2,甚至还有event.0.additionalProperties 这个键,它竟然出现在编辑对象内部。

最让人恼火的是,我在检查过的无效调用中发现,实际的 oldText 和 newText 数据 payload 却完全符合字节规范。事实上,模型本应生成正确的调用,却在对象末尾添加了毫无意义的内容。

此外,失败的结果还高度依赖于上下文。 像“编辑这个文件”这样全新的单轮提示,对我根本无法复现这种现象。 然而,当模型读取过文件、诊断出问题并完成多行编辑的代理式历史记录时,却能成功复现这一问题。 更令人沮丧的是,不是所有的转录文本都会表现出这种行为。 事实上,我甚至需要Petr Baudis 的转录文本,才能在我这里成功复现这一问题! 在那个用户的会话中,继续会话会导致 Opus 4.8 在大约 20% 的情况下失败。移除历史记录中的思考块,使失败率降低了近一半。 开启严格的工具调用功能,则在我的实验中彻底消除了这一问题。

为什么情况越来越糟

我最有力的假设是:这并非随机恶化,而是训练过程中产生的某种“人工产物”。

当较老的 Anthropic 模型进行训练时,它们所训练的工具(其中一些已经公开文档)都经过了专门的训练。 然而,当时的训练并没有像 Claude Code 那样,配备一个用户直接参与的调度器作为显而易见的目标。 现代 Anthropic 模型很可能有所不同,因为它们的训练后版本包含了 Claude Code,或者配备了与之极为相似的调度器。 模型学会了在这种环境下,成功的工具调用究竟应该是什么样子。 同时,它也学会了在这样的环境中,哪些错误是可以被容忍的。

Claude Code 自身的工具相对较为扁平。普通的编辑工具并不像 Pi 的嵌套 edits[] 结构那样复杂;它更接近于 file_path、old_string、new_string,以及一个可选的标志(replace_all)。 观察 Claude Code 的客户端,会发现它具备许多有用的特性:例如,它提供了针对格式错误工具调用的重试路径、 参数别名、类型转换、Unicode 修复,以及对未知键的过滤。换句话说,Anthropic 自己的客户端似乎能够容忍并接受相当程度的“偏差”,并且在大多数情况下,会默默地进行修复。

如果强化学习是在这样的调度器中进行,或者是在模拟环境中进行,那么即使工具调用略有偏差, 仍然可以顺利完成任务并获得奖励。调度器会完全吸收这些错误,几乎不会对自定义别名、 添加多余的字段,或使用附近的参数名称产生太大梯度。

更糟糕的是,模型可能会对标准的 Claude Code 编辑工具形状产生极其强烈的适应性。不同的调度器可能会呈现相同语义意图的工具, 却采用不同的 Schema。这样的工具往往更容易偏离原生环境。 训练得更好的模型,甚至可能更努力地与你对抗,因为它的先验知识更加深厚。

这并不算太令人意外,但与几个月前的情况相比,已经有了显著的变化。 当 Opus 4.5 上线时,它对其他编辑工具的适应能力异常出色。 事实上,我一度深信:我们正走在一条正确的道路上——只要指令足够清晰,模型就更有可能适应各种各样的工具形状。

如今,我对目前的进展感到有些担忧。替代的工具 Schema 不仅可能陌生,甚至可能在训练后被隐含地惩罚,因为训练过程专注于优化某一特定、 宽容的工具生态。而这种生态并没有得到充分的文档化。 虽然有一个文档化的“文本编辑器工具”,但你会发现,Claude Code 实际上并没有严格遵循这种格式。Claude Code 内部所做的工作(即封闭源代码的调度器)对你来说是隐藏的。

“偏差”调度器

Claude Code 显然属于封闭源代码,但我们可以通过查看其精简版代码,大致了解它到底在做什么。老实说,它对传入的数据非常宽容。

首先,Claude Code 会检查模型可见文本中是否泄漏了 gpt-oss, 它至少在某种程度上颇具趣味。这些模型被明确训练成使用 OpenAI 的harmony 响应格式,而且有大量的文档,至少能让我们了解到 OpenAI 人员对此的看法。

Harmony 将通道和工具调用的内容类型纳入提示格式之中。一个函数调用可以像这样:

关键在于 <|constrain|>json。 模型可以在带内表达,这条消息体是 JSON 格式,推理栈可以利用这一边界,切换到 JSON 约束采样,以处理工具调用的主体部分。 想必 Anthropic 的模型也存在类似的机制,至少在 strict 模式下,我猜想如此。

Harmony 中的标记符有助于采样器识别何时需要按照特定的语法进行采样,而且由于它也是转录文本的一部分, 因此让这一过程变得格外简单。对于托管的 GPT 模型,还可以选择提供LARK 语法,用于那些需要遵守类似规则的自定义工具。

不过,Anthropic 的模型似乎与之不同——尽管未必完全如此。 如果一个对象数组被表示为 JSON,正如它看起来那样,那么模型就必须在工具参数中编写 JSON。很可能在进行基础的语法约束采样,而这或许也能部分解释为何会出现额外的键。 对于嵌套的数组参数,JSON 中会将多行文件内容以转义形式嵌入字符串字面量中,位于一个标签之内。 那些意想不到的、自造的键,恰恰出现在这项任务的最高熵点:在关闭了一个数百个 token 的转义 newText 字符串之后,模型必须决定是使用 } 还是 , "..."。

Opus 4.8 和 Sonnet 5 似乎对编辑工具调用的外观有着更强的先验认知,而这种先验认知似乎与 Claude Code 的编辑 Schema 相吻合:一个扁平的“旧字符串/新字符串”对,再加上可选的 replace_all 标志。我猜测,Opus 学会了在编辑操作中,可能多出一个可选字段,但在 Pi 的嵌套 oldText/newText 结构下,它并没有为该字段训练出正式的名称。 因此,每次都会采样一个看似合理的名称,这也正是为什么失败的调用会生成数十个随机的键, 而不是一个稳定的别名。

由于 Anthropic 的 strict 模式似乎可以解决这个问题,我推测,服务器端会拒绝采样那些未被 JSON Schema 结构允许的键。这也就能解释,为什么在启用严格模式时,他们会对工具定义的复杂度设置上限。

到目前为止,我测试过的 Codex 模型并未出现这种类型的回归现象。我测试了所有可用的模型,除了 5.6,因为我目前还无法接触到它。

这对调度器意味着什么

令人不安的教训是:工具 Schema 并非中立的,至少在 Anthropic 模型中并非如此。我们总喜欢假想,Schema 是一种抽象契约,而模型则是通用的推理者,会遵从这一契约; 但对于某些工具来说,情况可能早已不再如此。

工具 Schema 位于分布的某个区间,有些形状与模型在训练后所见的场景十分接近,而另一些则相去甚远。 有些 Schema 对提供方来说很容易被隐藏的编码所“破解”(例如 ANTML 中的顶级属性),而有些则要求模型在长多行字符串之后,将大型转义 JSON 对象写入嵌套数组中。模型也许足够聪明,能够理解 Schema,却依然在压力之下难以准确采样出特定的形状。

如果这种模型行为持续下去,我开始思考:这对调度器又意味着什么? 显然,我们可以在 Anthropic 中开启 strict 采样,问题自然会迎刃而解。另一方面,模型出现这种行为,也说明了强化学习对它们的影响。 如果你想获得最佳的模型性能,恐怕很难与这种先验知识抗衡。

目前的现实是:Claude Code 并非开源,我们也无法真正了解他们在强化学习环境中究竟在做什么。 我们不能假设,Claude Code 训练出的行为会干净利落地迁移到你的工具中,除非它们彼此之间有很高的契合度。 训练后的过程越集中在单一主导的调度器中,其他调度器就越容易继承其种种怪癖。

我过去对严格语法约束的工具调用持更为怀疑的态度,因为约束解码可能会带来质量上的权衡。 我仍然认为,这种观点在一般情况下可能是成立的,但这次的 bug 严重改变了我的看法。如果最新的模型在完成任务时表现更好,却在忠实地发出替代工具 Schema 方面却显得力不从心,那么调度器在某个环节就需要更强大的保障措施。

如果你想了解更多,或者想就此展开讨论,请阅读 Pi 跟踪系统的相关议题。

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论