<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Toolformer on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/toolformer/</link><description>Recent content in Toolformer on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 31 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/toolformer/index.xml" rel="self" type="application/rss+xml"/><item><title>第三话｜模型为什么能生成工具调用？</title><link>https://zh.renxinblog.cn/post/c03-function-calling-training/</link><pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c03-function-calling-training/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c03-function-calling-training-cover-v2.png" alt="Featured image of post 第三话｜模型为什么能生成工具调用？" /&gt;&lt;p&gt;上一话跑通了 Function Calling 协议，但留了个更根本的问题没收尾：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模型凭什么知道该从 &lt;code&gt;tools&lt;/code&gt; 菜单里挑 &lt;code&gt;get_weather&lt;/code&gt;、又凭什么把“北京”填进 &lt;code&gt;city&lt;/code&gt;？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;不是提示词写得好。协议只是给了模型一个“填在哪里”的位置，真正让模型知道“填什么”的，是它的训练。这一话就追这件事：模型是怎么学会“提出一次工具调用”的。&lt;/p&gt;
&lt;p&gt;先给结论，后面再拆：&lt;strong&gt;工具调用不是模型的内置能力，而是训练出来的“输出倾向”。训练改变的是它生成某些 token 序列的概率，不是给它装了一个真的会退款的函数。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一模型学的仍然是下一个-token"&gt;一、模型学的，仍然是下一个 token
&lt;/h2&gt;&lt;p&gt;模型里没有一个叫“退款”的函数。它有的，只是“给定上文，预测下一个词”的能力。所谓“会调工具”，是它学会了：当上下文里出现“退款意图 + 工具定义”时，下一个该输出的，是 &lt;code&gt;refund_order&lt;/code&gt; 和 &lt;code&gt;order_id&lt;/code&gt; 这几个 token。&lt;/p&gt;
&lt;p&gt;训练样本把它要学的样子，直接摆出来：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;上下文：订单 O-100 已支付，用户要求退款；可用工具有 refund_order(order_id)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;目标：refund_order({&amp;#34;order_id&amp;#34;:&amp;#34;O-100&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;反复见这样的样本，模型参数里就长出一套倾向：&lt;strong&gt;“退款 + 已支付”这类上下文，会抬高 &lt;code&gt;refund_order&lt;/code&gt; 的概率；订单号 O-100，会抬高被填进 &lt;code&gt;order_id&lt;/code&gt; 的概率。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;注意，训练集还得有反例，否则模型会养成坏习惯：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户只问配送时间 → 直接回答，不调工具；&lt;/li&gt;
&lt;li&gt;订单状态不明 → 先追问，不调工具。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;少了这些负样本，模型就会变成“看见什么都想调工具”。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart LR
 A[训练样本&lt;br/&gt;意图、工具定义、目标调用] --&gt; B[模型参数&lt;br/&gt;形成输出倾向]
 C[本次请求&lt;br/&gt;用户消息、当前工具、订单状态] --&gt; D[模型生成候选 tool_calls]
 B --&gt; D
 D --&gt; E[Runtime&lt;br/&gt;校验、确认、执行或拒绝]&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;关键洞察：模型只是提出了候选调用。它没有因此获得订单事实、权限，也没有执行权。&lt;/strong&gt; 会“说”要干什么，和“真的干了什么”，是两码事。&lt;/p&gt;
&lt;h2 id="二这条倾向是靠什么训练出来的"&gt;二、这条“倾向”，是靠什么训练出来的？
&lt;/h2&gt;&lt;p&gt;问题来了：这些“上下文 → 正确调用”的样本，是怎么来的？人工一条条标注太贵，学界给了几条公开的路线。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Toolformer&lt;/strong&gt; 解决的是“数据怎么规模化造出来”。它的做法很有意思——让模型自己当标注员：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在一段普通文本里，让模型猜哪些位置可能需要调 API，采样出一堆候选调用；&lt;/li&gt;
&lt;li&gt;真的去执行这些调用，拿到结果；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键一步&lt;/strong&gt;：只保留那些“拿到结果后，模型预测后续文字更准了”的调用——具体说，就是比较“有结果”和“没结果”两种情况下，模型对后面 token 的预测损失，损失降得够多才留下；&lt;/li&gt;
&lt;li&gt;用过滤后的样本去微调模型。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一句话：&lt;strong&gt;模型自己提出调用、自己执行、再自己判断“这次调用到底有没有用”，有用的才拿来当训练样本。&lt;/strong&gt; 全程不需要人工标注“这里该不该调工具”。&lt;a class="link" href="https://arxiv.org/abs/2302.04761" target="_blank" rel="noopener"
 &gt;Toolformer&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gorilla&lt;/strong&gt; 解决的是另一个问题：API 会变。哪怕模型学过 &lt;code&gt;get_weather(city)&lt;/code&gt;，文档也可能改成 &lt;code&gt;get_weather(location, unit)&lt;/code&gt;。Gorilla 的做法是微调 + 文档检索结合：训练给模型调用能力，检索把“当前最新的接口契约”带进上下文，让它跟得上变化。&lt;a class="link" href="https://arxiv.org/abs/2305.15334" target="_blank" rel="noopener"
 &gt;Gorilla&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CoT&lt;/strong&gt; 常被顺带提起，但它其实是个对照，不是工具调用训练：它研究的是“在提示里给推理示例能不能改变输出”。它能改变当前这一轮的输出，但&lt;strong&gt;不改模型参数&lt;/strong&gt;——和 Toolformer/Gorilla 那种真正动参数的训练，是两回事。&lt;a class="link" href="https://arxiv.org/abs/2201.11903" target="_blank" rel="noopener"
 &gt;CoT&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="三一件必须分清的事哪些证据能信哪些不能"&gt;三、一件必须分清的事：哪些证据能信，哪些不能
&lt;/h2&gt;&lt;p&gt;讲到这里，得划一条诚实的边界。关于“模型为什么能调工具”，能拿到的证据分三类，&lt;strong&gt;它们不能互相冒充&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;证据类型&lt;/th&gt;
					&lt;th&gt;能说明什么&lt;/th&gt;
					&lt;th&gt;不能说明什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;论文公开的方法（Toolformer/Gorilla）&lt;/td&gt;
					&lt;td&gt;“存在这些训练工具调用的公开方法”&lt;/td&gt;
					&lt;td&gt;不代表某个商业模型就用了它&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;厂商披露（OpenAI 公告）&lt;/td&gt;
					&lt;td&gt;0613 模型经过微调、Structured Outputs 靠训练+约束解码&lt;/td&gt;
					&lt;td&gt;只限定到那几家、那段时间，不能外推&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;黑盒 API 能观察到的&lt;/td&gt;
					&lt;td&gt;输入什么、输出什么（比如 description 改了就选错工具）&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;看不到&lt;/strong&gt;训练语料、训练阶段、奖励方法&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最关键的一条是第三类：&lt;strong&gt;你从 API 拿到的结果再稳定，也反推不出厂商到底是怎么训练的。&lt;/strong&gt; 训练语料长什么样、分几个阶段、有没有用 RLHF——这些是黑盒，除非厂商自己说，否则只能标“未知”。&lt;/p&gt;
&lt;p&gt;所以别犯那个错：看到 &lt;code&gt;tool_calls&lt;/code&gt; 稳定输出，就说“这个模型用了 Toolformer”。稳定输出能证明“它训练得很好”，证明不了“它是怎么训练的”。&lt;/p&gt;
&lt;h2 id="四prompt-only-和-native-tools差的不只是格式"&gt;四、Prompt-only 和 Native tools，差的不只是“格式”
&lt;/h2&gt;&lt;p&gt;上一话讲了 Native tools 的协议形态，这里补一层它和训练/推理的关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt-only&lt;/strong&gt;：在 system prompt 里约定“需要退款时输出 JSON”。模型把 JSON 塞进普通 &lt;code&gt;content&lt;/code&gt; 里，Runtime 还得从正文里把命令抠出来——这里混着自然语言、Markdown、可能还有好几个 JSON。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Native tools&lt;/strong&gt;：在 API 的 &lt;code&gt;tools&lt;/code&gt; 字段传 schema，模型用独立的 &lt;code&gt;tool_calls&lt;/code&gt; 字段返回。Runtime 直接拿到工具名、参数和调用 id，不用猜哪里是命令。&lt;/p&gt;
&lt;p&gt;这个差异不是“格式好看点”，而是&lt;strong&gt;责任归属变了&lt;/strong&gt;：Prompt-only 把“保证输出长得像 JSON”的责任甩给了提示词和你的解析器；Native tools 把这层责任接了过去，一部分靠训练（让模型更会按 schema 输出），一部分靠推理阶段的约束解码（生成时卡住，只允许符合 schema 的 token 出来）。&lt;/p&gt;
&lt;p&gt;OpenAI 官方也明确说过：Function Calling 的可靠性，一部分来自模型的微调，一部分来自 Structured Outputs 的 constrained decoding。&lt;a class="link" href="https://openai.com/index/introducing-structured-outputs-in-the-api/" target="_blank" rel="noopener"
 &gt;Structured Outputs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这里有个真实的坑能说明这层差异。我们 Java 版最初只写了“需要时输出 JSON”，没说清 &lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;arguments.order_id&lt;/code&gt; 的形状。结果 Native tools 模式仍能返回结构化调用，Prompt-only 模式却解析不出来。把提示词补成明确的 JSON 模板后，Java 的 8 个场景才全过。这不是“Java 不适合做 Agent”，而是&lt;strong&gt;Prompt-only 把输出协议的一部分责任，留在了你的提示词和解析器上&lt;/strong&gt;——而 Native tools 帮你卸掉了一部分。&lt;/p&gt;
&lt;h2 id="五动手看一次description-才是选哪个的开关"&gt;五、动手看一次：description 才是“选哪个”的开关
&lt;/h2&gt;&lt;p&gt;我们用一套虚构订单做实验，Python 和 Java 21 各跑一遍。Runtime 先注入已验证的订单状态，模型只做一次工具选择，程序只记录候选调用，&lt;strong&gt;绝不执行退款或取消&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;四种场景，正确行为是这样：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th&gt;正确行为&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;已支付 O-100，要求退款&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;refund_order(O-100)&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;未支付 O-200，要求取消&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;cancel_order(O-200)&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;无订单号和状态&lt;/td&gt;
					&lt;td&gt;追问，不调用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;无退款/取消意图&lt;/td&gt;
					&lt;td&gt;自然语言回答，不调用&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;基线下两种语言都过。然后做一个故障注入：&lt;strong&gt;只把 &lt;code&gt;refund_order&lt;/code&gt; 和 &lt;code&gt;cancel_order&lt;/code&gt; 的 description 对调&lt;/strong&gt;，其余全不变——工具名、模型、用户请求、参数 schema 都一样。&lt;/p&gt;
&lt;p&gt;结果很干净：已支付退款稳定变成 &lt;code&gt;cancel_order(O-100)&lt;/code&gt;，未支付取消稳定变成 &lt;code&gt;refund_order(O-200)&lt;/code&gt;，而&lt;strong&gt;订单号仍然是对的&lt;/strong&gt;。恢复 description 后，回归全过。&lt;/p&gt;
&lt;p&gt;这个实验说明两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具选择（选哪个）靠的是 description&lt;/strong&gt;——模型是读描述来判断“这个工具是干嘛的”，描述写错了，它就选错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数提取（填什么）可以和工具选择分开看&lt;/strong&gt;——订单号还是从用户消息和 Runtime 注入的上下文里来的，两个工具的参数 schema 没变，所以订单号没错。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;但这能证明什么、不能证明什么，得说清楚：它证明了“description 影响路由”这个&lt;strong&gt;可观察行为&lt;/strong&gt;，&lt;strong&gt;证明不了&lt;/strong&gt;模型用了哪种训练法。description 写对了，也不代表参数永远填对——Runtime 照样得校验。&lt;/p&gt;
&lt;h2 id="六所以能信到什么程度"&gt;六、所以，能信到什么程度
&lt;/h2&gt;&lt;p&gt;绕了一圈，回到开头那个问题：模型为什么能生成工具调用？&lt;/p&gt;
&lt;p&gt;因为它被训练成了这样——训练样本让它形成了“意图 + 工具定义 → 结构化调用”的输出倾向。Toolformer、Gorilla 这些公开方法，展示了这条倾向可以怎么规模化地训练出来；厂商还叠加了微调和约束解码，让输出更稳。&lt;/p&gt;
&lt;p&gt;但“会提请求”不等于“可靠”。训练让模型更会“说”要干什么，不会让它“变聪明”——它照样可能选错工具、编造业务参数、请求一个无权执行的动作。&lt;strong&gt;tool_calls 永远是候选，不是命令。&lt;/strong&gt; 谁来判断“能不能执行、怎么执行”，是下一篇的事。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Wei et al., &lt;a class="link" href="https://arxiv.org/abs/2201.11903" target="_blank" rel="noopener"
 &gt;Chain-of-Thought Prompting Elicits Reasoning in Large Language Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Schick et al., &lt;a class="link" href="https://arxiv.org/abs/2302.04761" target="_blank" rel="noopener"
 &gt;Toolformer: Language Models Can Teach Themselves to Use Tools&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Patil et al., &lt;a class="link" href="https://arxiv.org/abs/2305.15334" target="_blank" rel="noopener"
 &gt;Gorilla: Large Language Model Connected with Massive APIs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a class="link" href="https://openai.com/index/function-calling-and-other-api-updates/" target="_blank" rel="noopener"
 &gt;Function calling and other API updates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;OpenAI, &lt;a class="link" href="https://openai.com/index/introducing-structured-outputs-in-the-api/" target="_blank" rel="noopener"
 &gt;Introducing Structured Outputs in the API&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>