<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Function-Calling on Renxin's Blog</title><link>https://zh.renxinblog.cn/tags/function-calling/</link><description>Recent content in Function-Calling on Renxin's Blog</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Tue, 14 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://zh.renxinblog.cn/tags/function-calling/index.xml" rel="self" type="application/rss+xml"/><item><title>第四话｜模型说调 refund_order，Runtime 怎么不把事情搞砸？</title><link>https://zh.renxinblog.cn/post/c04-tool-registry/</link><pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c04-tool-registry/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c04-tool-registry-cover-v2.png" alt="Featured image of post 第四话｜模型说调 refund_order，Runtime 怎么不把事情搞砸？" /&gt;&lt;p&gt;第三话结束时，我们留下一个问题没解决：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;code&gt;tool_calls&lt;/code&gt; 永远是候选，不是命令。谁来判断&amp;quot;能不能执行、怎么执行&amp;quot;？&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这就是第四话要回答的。模型提出的工具调用，哪怕 JSON 合法、参数齐全，也可能是错的、越权的、有副作用的。Runtime 不能照单全收——它得先校验，再执行，而且要在执行出问题时不把局面搞得更糟。&lt;/p&gt;
&lt;p&gt;这里最危险的，不是选错工具，而是&lt;strong&gt;一个有副作用、结果又说不清的操作&lt;/strong&gt;。比如模型要调 &lt;code&gt;refund_order&lt;/code&gt; 退款：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;handler 执行到一半超时了。钱到底退了没有？直接重试，可能重复退款；不重试，用户又以为没退成。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这一话就沿着这条退款主线，讲清楚 Runtime 怎么把&amp;quot;模型说要调用&amp;quot;变成&amp;quot;安全地执行&amp;quot;——以及最关键的，超时之后怎么根据真实状态做决定，而不是靠猜。&lt;/p&gt;
&lt;h2 id="一模型说调-refund_orderruntime-先干什么"&gt;一、模型说&amp;quot;调 refund_order&amp;quot;，Runtime 先干什么？
&lt;/h2&gt;&lt;p&gt;模型返回的 &lt;code&gt;tool_calls&lt;/code&gt; 里，是一段结构化申请单：工具名 &lt;code&gt;refund_order&lt;/code&gt;、参数 &lt;code&gt;order_id&lt;/code&gt; 和 &lt;code&gt;idempotency_key&lt;/code&gt;。但它是模型生成的，&lt;strong&gt;是外部输入，不是已经校验过的命令&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;Runtime 拿到它，第一件事不是执行，是问三个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这个工具名，是允许执行的吗？&lt;/li&gt;
&lt;li&gt;参数齐全、类型对吗？&lt;/li&gt;
&lt;li&gt;这个工具现在可用吗？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;任何一个不过，就不进 handler，而是返回一个&lt;strong&gt;结构化的错误&lt;/strong&gt;，让上层能稳定地分支处理。&lt;/p&gt;
&lt;p&gt;为什么要结构化，不能是一段&amp;quot;调用失败，请稍后再试&amp;quot;的文案？因为文案里没有可编程的信息——程序没法可靠地区分&amp;quot;工具不存在&amp;quot;和&amp;quot;参数缺了&amp;quot;和&amp;quot;执行到一半挂了&amp;quot;。而这三种情况，处理方式完全不同：&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;&lt;code&gt;UNKNOWN_TOOL&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;模型报了个不存在的工具名&lt;/td&gt;
					&lt;td&gt;拒绝，修正工具选择&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;INVALID_ARGUMENT&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;参数不满足契约&lt;/td&gt;
					&lt;td&gt;补参数或拒绝&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;TOOL_UNAVAILABLE&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;工具已下线或不健康&lt;/td&gt;
					&lt;td&gt;告知不可用，不能硬调&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;EXECUTION_ERROR&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;handler 或下游执行失败&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;前三个错误，handler 根本没执行——它们发生在执行之前。第四个才是真正进到了执行阶段，也是最麻烦的。&lt;/p&gt;
&lt;h2 id="二执行时出了问题最怕的是结果说不清"&gt;二、执行时出了问题，最怕的是&amp;quot;结果说不清&amp;quot;
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;refund_order&lt;/code&gt; 通过了校验，handler 真的去调订单服务了——然后超时。&lt;/p&gt;
&lt;p&gt;超时意味着什么？&lt;strong&gt;意味着&amp;quot;结果不确定&amp;quot;&lt;/strong&gt;。请求可能压根没到达订单服务，也可能已经到达、退款都成功了，只是响应没传回来。你分不清。&lt;/p&gt;
&lt;p&gt;这就是最危险的地方：如果 Runtime 把&amp;quot;超时&amp;quot;直接当成&amp;quot;没执行&amp;quot;，再调一次，就可能退了两笔钱。而如果当成&amp;quot;失败了&amp;quot;，用户这边又显示退款失败，但其实钱已经退了——两边对不上。&lt;/p&gt;
&lt;p&gt;所以规则只有一条：&lt;strong&gt;超时之后，先查这笔操作的真实状态，不要直接重试。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;怎么查？靠 &lt;code&gt;idempotency_key&lt;/code&gt;（幂等键）。它是发起退款时就带上的、这笔操作的唯一标识。领域服务按这个键记录&amp;quot;这笔退款到底执行了没有&amp;quot;，Runtime 超时后先去查它，而不是再发起一笔。&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 A[refund_order 超时] --&gt; B[按 idempotency_key 查询真实状态]
 B --&gt;|SUCCEEDED| C[回放成功结果&lt;br/&gt;不再调 handler]
 B --&gt;|NOT_EXECUTED| D[确认没执行&lt;br/&gt;才重试一次]
 B --&gt;|UNKNOWN / 执行中| E[等待或人工核验]&lt;/pre&gt;&lt;p&gt;三条分支，对应三种真实状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SUCCEEDED&lt;/code&gt;&lt;/strong&gt;：钱已经退了。Runtime 把成功结果回放给用户，&lt;strong&gt;不再调 handler&lt;/strong&gt;。这是最容易犯错的场景——明明已经成功，却因为&amp;quot;没收到响应&amp;quot;而再退一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;NOT_EXECUTED&lt;/code&gt;&lt;/strong&gt;：确认没执行过。这时重试一次是安全的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;UNKNOWN&lt;/code&gt; / 执行中&lt;/strong&gt;：状态也查不到。这时既不能重试，也不能谎报成功，只能等待对账或转人工核验。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;跑一遍主线 demo，你会看到这样一条 Trace：&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;refund_order response=EXECUTION_ERROR
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;idempotency_status=SUCCEEDED
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;runtime_action=REPLAY_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;final=SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;refund_order handler_execution_count=1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最后一行是整条链路的关键证据：&lt;strong&gt;&lt;code&gt;handler_execution_count=1&lt;/code&gt;&lt;/strong&gt;。它证明 Runtime 虽然经历了&amp;quot;超时 → 查状态 → 回放&amp;quot;，但真正执行退款的 handler &lt;strong&gt;只跑了一次&lt;/strong&gt;。没有重复退款。&lt;/p&gt;
&lt;h2 id="三把谁负责什么钉死"&gt;三、把&amp;quot;谁负责什么&amp;quot;钉死
&lt;/h2&gt;&lt;p&gt;超时恢复这件事，看着是 Runtime 一个人的活，其实牵涉三个角色，职责不能混：&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;领域服务（&lt;code&gt;RefundDomain&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;记录和查询&amp;quot;退款到底发生没有&amp;quot;&lt;/td&gt;
					&lt;td&gt;不负责重试策略，也不知道 Runtime 怎么编排&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Runtime&lt;/td&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;td&gt;不拥有执行权，更不能拿它的话当退款状态&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话：&lt;strong&gt;有副作用工具的超时恢复，首先是领域状态问题，其次才是 Runtime 的重试策略问题。&lt;/strong&gt; 退款有没有发生，是领域事实，只能问领域服务；Runtime 能做的是照着这个事实去编排下一步，而不是自己猜。&lt;/p&gt;
&lt;p&gt;反过来看，如果领域服务不记录幂等状态，Runtime 再聪明也没用——它没有可以查询的真相，超时之后只能抓瞎。所以幂等这件事，&lt;strong&gt;得从工具设计那天就带上&lt;/strong&gt;，而不是等出了超时才补。&lt;/p&gt;
&lt;h2 id="四把这条链路跑起来"&gt;四、把这条链路跑起来
&lt;/h2&gt;&lt;p&gt;配套 demo 在 &lt;code&gt;code/agent-tutorial/c04-tool-registry/&lt;/code&gt;，Python 和 Java 各一份，语义一致。它不调用 LLM、订单或支付系统——所有 handler 都是本地确定性模拟，所以它验证的是 &lt;strong&gt;Runtime 的控制流&lt;/strong&gt;，不是退款系统的可用性证明。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; code/agent-tutorial/c04-tool-registry/python
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 registry.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 -m unittest test_registry.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ../java
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gradle run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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;工具不存在&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;UNKNOWN_TOOL&lt;/code&gt;，不进 handler&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;工具下线&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;TOOL_UNAVAILABLE&lt;/code&gt;，不能硬调&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;参数缺失&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;INVALID_ARGUMENT&lt;/code&gt;，补参或拒绝&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;正常调用&lt;/td&gt;
					&lt;td&gt;一次执行，&lt;code&gt;SUCCEEDED&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;超时但已成功&lt;/td&gt;
					&lt;td&gt;查幂等 → &lt;code&gt;SUCCEEDED&lt;/code&gt; → 回放，&lt;code&gt;handler_execution_count=1&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;超时且未执行&lt;/td&gt;
					&lt;td&gt;查幂等 → &lt;code&gt;NOT_EXECUTED&lt;/code&gt; → 重试一次&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中&amp;quot;超时但已成功&amp;quot;是最值得盯着看的一条：它模拟了 &lt;code&gt;handler&lt;/code&gt; 执行成功后、响应却丢失的场景——退款已经发生，&lt;code&gt;SUCCEEDED&lt;/code&gt;，但 Runtime 收到的是超时。正确的 Trace 必须是&amp;quot;回放成功结果&amp;quot;，而不是&amp;quot;再退一次&amp;quot;。&lt;/p&gt;
&lt;h2 id="五还差的执行成功--永远可靠"&gt;五、还差的：执行成功 ≠ 永远可靠
&lt;/h2&gt;&lt;p&gt;跑通这条链路，Runtime 能安全地执行一次有副作用的调用了。但这离&amp;quot;可靠&amp;quot;还有距离，几件事本篇点到为止，先立个边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一次超时不等于工具坏了&lt;/strong&gt;。订单服务偶发抖动，不能立刻把 &lt;code&gt;refund_order&lt;/code&gt; 永久下线；连续失败达到阈值才考虑熔断。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;熔断后的探测，不能用一笔新退款去试&lt;/strong&gt;。要用无副作用的健康检查——否则&amp;quot;检查服务恢复没有&amp;quot;这件事本身，又制造了一笔业务动作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime 不替代鉴权、审批和审计&lt;/strong&gt;。工具能不能调，除了契约校验，还涉及&amp;quot;这个用户有没有权限&amp;quot;&amp;ldquo;要不要二次确认&amp;quot;&amp;ldquo;留不留审计留底&amp;rdquo;——这些是 Runtime 与领域服务的责任，本篇没展开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都属于&amp;quot;从单次安全执行到生产级可靠&amp;quot;的延伸，先把边界划在这，不抢后续章节的戏。&lt;/p&gt;
&lt;h2 id="到下一话问题才真正变难"&gt;到下一话，问题才真正变难
&lt;/h2&gt;&lt;p&gt;现在，Runtime 已经能把&lt;strong&gt;单个&lt;/strong&gt;候选调用安全地执行，并在超时后不搞砸。&lt;/p&gt;
&lt;p&gt;但它还不会根据第一次工具的结果，决定第二步做什么。&lt;/p&gt;
&lt;p&gt;第五话才进入真正的 ReAct 循环：第二个 Action 必须依赖第一个 Observation。那时，今天建立的 ToolResponse 和幂等恢复，会成为循环里可复用的执行底座。&lt;/p&gt;</description></item><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><item><title>第二话｜Function Calling：谁把“调用工具”做成了一套协议？</title><link>https://zh.renxinblog.cn/post/c02-function-calling/</link><pubDate>Tue, 17 Mar 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c02-function-calling/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c02-function-calling-cover.png" alt="Featured image of post 第二话｜Function Calling：谁把“调用工具”做成了一套协议？" /&gt;&lt;p&gt;上一话我们搞懂了两件事：模型只会补全文字；想让它干活，得让它把“想干什么”说成某种格式，再由外面的代码去执行。&lt;/p&gt;
&lt;p&gt;但上一话那个办法很脆——用提示词逼模型输出 JSON，模型经常给你残缺的 JSON、在前后多写几句废话、或者干脆忘了格式。于是就有了 Function Calling：它把“让模型提出一次工具调用”这件事，从碰运气的提示词技巧，做成了一套规规矩矩的协议。&lt;/p&gt;
&lt;p&gt;这一话就讲这套协议：它长什么样，谁先做出来的，以及为什么它能让模型“说清楚要干什么”。&lt;/p&gt;
&lt;h2 id="一协议要解决的就是上一话那个脆"&gt;一、协议要解决的，就是上一话那个“脆”
&lt;/h2&gt;&lt;p&gt;先看上一话的提示词 hack 是怎么翻车的。你在 system prompt 里写“想查天气就输出 &lt;code&gt;{&amp;quot;tool&amp;quot;:&amp;quot;get_weather&amp;quot;,&amp;quot;city&amp;quot;:&amp;quot;...&amp;quot;}&lt;/code&gt;”，然后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模型输出的不是纯 JSON，前后还夹着“好的，我帮你查一下”这种话；&lt;/li&gt;
&lt;li&gt;JSON 本身残缺，少个引号、少个逗号，&lt;code&gt;json.loads&lt;/code&gt; 直接崩；&lt;/li&gt;
&lt;li&gt;换了个问法，模型就把格式忘得一干二净。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每次翻车，你都得靠更长的提示词、更严的正则去兜，兜到最后还是不可靠。&lt;/p&gt;
&lt;p&gt;协议的意义就在这里：&lt;strong&gt;别再用文字去“暗示”模型输出格式，而是给一个结构化的位置，让模型把调用填进去。&lt;/strong&gt; 这个位置是 API 里明确规定好的，模型要么填对，要么 API 直接拒绝，没有“碰运气”的中间地带。&lt;/p&gt;
&lt;h2 id="二协议长什么样tools-进tool_calls-出"&gt;二、协议长什么样：&lt;code&gt;tools&lt;/code&gt; 进，&lt;code&gt;tool_calls&lt;/code&gt; 出
&lt;/h2&gt;&lt;p&gt;整个协议，一进一出两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;请求里传 &lt;code&gt;tools&lt;/code&gt;&lt;/strong&gt;：你告诉模型“现在有哪些工具可以用”。这是一份菜单，写清楚每个工具叫什么、干什么、参数长什么样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;响应里收 &lt;code&gt;tool_calls&lt;/code&gt;&lt;/strong&gt;：模型告诉你“我想调哪个工具、带什么参数”。这是一张申请单。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是一份声明了本地天气工具的菜单：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;function&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;function&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;查询指定城市的天气演示数据&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;parameters&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;object&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;properties&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;city&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;string&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;description&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;城市名，如北京&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;required&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;additionalProperties&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意，&lt;code&gt;tools&lt;/code&gt; 不是把 Python 函数“上传”给模型。它只是一份描述：函数叫 &lt;code&gt;get_weather&lt;/code&gt;、查天气的、需要一个 &lt;code&gt;city&lt;/code&gt; 字符串参数。模型拿到的是这些文字，不是那个函数本身。&lt;a class="link" href="https://api-docs.deepseek.com/api/create-chat-completion" target="_blank" rel="noopener"
 &gt;Chat Completions API&lt;/a&gt;&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-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;assistant_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;请查询北京现在的天气。&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;required&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;响应里我们关心的不是自然语言，而是这张申请单：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;call_00_...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;function&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;function&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;arguments&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;{\&amp;#34;city\&amp;#34;: \&amp;#34;北京\&amp;#34;}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;拆开看每个字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;：这条调用的唯一标识，回传结果时要靠它关联；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：固定是 &lt;code&gt;function&lt;/code&gt;，标记这是一次函数调用；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;function.name&lt;/code&gt;：想调哪个工具；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;function.arguments&lt;/code&gt;：一个 &lt;strong&gt;JSON 字符串&lt;/strong&gt;——注意它还是字符串，不是对象，代码里得再 &lt;code&gt;json.loads&lt;/code&gt; 一层才能用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它表达的就是一句话：“请调用 &lt;code&gt;get_weather&lt;/code&gt;，参数是北京。”到这里，模型既没有运行 Python，也没有拿到天气数据。它只是把“想干什么”用协议规定的格式写清楚了。&lt;/p&gt;
&lt;h2 id="三这套协议是谁先做出来的"&gt;三、这套协议，是谁先做出来的
&lt;/h2&gt;&lt;p&gt;它不是我编的，也不是模型厂商某天灵机一动。时间线很清晰：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2022 年，&lt;strong&gt;ReAct&lt;/strong&gt;（Yao et al., arXiv 2210.03629）提出“推理 + 行动”交替，用提示词让模型按 &lt;code&gt;Thought / Action&lt;/code&gt; 的格式输出，再让代码去执行；&lt;/li&gt;
&lt;li&gt;2023 年 2 月，&lt;strong&gt;Toolformer&lt;/strong&gt;（Meta AI, arXiv 2302.04761）证明“模型自己决定何时调 API”这个能力是能被训练出来的；&lt;/li&gt;
&lt;li&gt;2023 年 5 月，&lt;strong&gt;Gorilla&lt;/strong&gt;（UC Berkeley, arXiv 2305.15334）开始微调模型学写 API 调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这几篇走的都是“提示词 + 解析”的路线——也就是上一话讲的那个脆办法。&lt;/p&gt;
&lt;p&gt;真正的转折在 2023 年 6 月 13 日：&lt;strong&gt;OpenAI 首次发布原生 Function Calling&lt;/strong&gt;，随 &lt;code&gt;gpt-4-0613&lt;/code&gt; 和 &lt;code&gt;gpt-3.5-turbo-0613&lt;/code&gt; 两个模型快照一起推出。从这一天起，“让模型提工具调用”不再靠提示词暗示，而是变成了 API 里的标准字段——就是我们第二节看到的 &lt;code&gt;tools&lt;/code&gt; 进、&lt;code&gt;tool_calls&lt;/code&gt; 出。&lt;/p&gt;
&lt;h2 id="四别看就一个-tool_calls背后做了很多工作"&gt;四、别看就一个 &lt;code&gt;tool_calls&lt;/code&gt;，背后做了很多工作
&lt;/h2&gt;&lt;p&gt;有个问题自然冒出来：同样是“让模型输出调用”，为什么提示词 hack 天天翻车，而协议一出来，模型就能稳定地输出合法参数、不再给你残缺 JSON？&lt;/p&gt;
&lt;p&gt;不是因为提示词写得好了。是厂商在背后做了工作——&lt;strong&gt;模型是被训练成“会正确提出调用”的&lt;/strong&gt;，这个能力不是运气，是训练出来的。&lt;/p&gt;
&lt;p&gt;这里只说结论，不展开原理（那是下一话的事）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;微调&lt;/strong&gt;：训练阶段喂了大量“工具调用对话”样本，让模型学会判断要不要调、选哪个工具、生成合法参数；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;约束解码&lt;/strong&gt;：生成的时候，在输出层卡住模型，让它只能吐出符合 schema 的内容，从根上杜绝“残缺 JSON”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两件事把“正确提出调用”从提示词的玄学，变成了模型本身的能力。至于模型内部到底是怎么学会的、这两件事的具体机制——那是第三话要专门讲的问题，这里先按下不表。&lt;/p&gt;
&lt;h2 id="五协议只负责提出不负责执行"&gt;五、协议只负责“提出”，不负责“执行”
&lt;/h2&gt;&lt;p&gt;回到那张申请单。&lt;code&gt;tool_calls&lt;/code&gt; 拿回来，协议的部分就结束了。接下来是你的代码接手：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arguments_json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;不允许执行未知工具：&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;get_weather 参数必须且只能包含 city&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city 必须是非空字符串&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模型输出在这里被当成&lt;strong&gt;外部输入&lt;/strong&gt;处理：先校验工具名，再校验参数，然后才真正执行。&lt;code&gt;25℃&lt;/code&gt; 来自 &lt;code&gt;get_weather()&lt;/code&gt; 的返回值，不是模型编的：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;weather&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;北京&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;多云，25℃，东北风3级&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;上海&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;小雨，22℃，东南风2级&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;weather&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;暂无&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;的天气演示数据&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协议让模型“说清楚要干什么”，执行永远在模型外面——这是上一话那条结论的延续，在这里变成了可运行的代码。&lt;/p&gt;
&lt;h2 id="六把结果交回去也是协议的一部分"&gt;六、把结果交回去，也是协议的一部分
&lt;/h2&gt;&lt;p&gt;函数执行完，Python 手里有了天气文本，但用户还没看到一句话。模型上一次只停在“我想调什么工具”，所以要把两样东西放回消息历史：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assistant_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 模型提出的调用请求&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;role&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;tool&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# 工具执行结果&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;tool_call_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# 关联到上面那条 tool_calls&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;role=tool&lt;/code&gt; 这一步&lt;strong&gt;不是可选的，是协议强制要求的&lt;/strong&gt;。这里有个真实的坑：有人把 &lt;code&gt;role=tool&lt;/code&gt; 写成了 &lt;code&gt;role=user&lt;/code&gt;，API 直接报 HTTP 400——&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;An assistant message with &amp;#39;tool_calls&amp;#39; must be followed by
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tool messages responding to each &amp;#39;tool_call_id&amp;#39;.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;意思很直白：你给模型看了一张 &lt;code&gt;tool_calls&lt;/code&gt; 申请单，就必须逐条配上一份对应的 &lt;code&gt;tool&lt;/code&gt; 结果，用 &lt;code&gt;tool_call_id&lt;/code&gt; 对上号。少一条、错一条，协议层面就拒绝，根本轮不到模型来猜。这个报错本身，就是“协议不是约定俗成、而是硬约束”的最好证据。&lt;/p&gt;
&lt;p&gt;补上结果后再问一次模型，它才能把“多云，25℃”组织成一句人话。事实来自函数，句子由模型生成，两边分得清清楚楚。&lt;/p&gt;
&lt;h2 id="七回到最开始的问题到底谁调用了工具"&gt;七、回到最开始的问题：到底谁调用了工具？
&lt;/h2&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;th&gt;不负责什么&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;模型&lt;/td&gt;
					&lt;td&gt;用户问题与 &lt;code&gt;tools&lt;/code&gt; 菜单&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;tool_calls&lt;/code&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;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;code&gt;role=tool&lt;/code&gt; 结果&lt;/td&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;Function Calling 没有把函数“装进模型”。它做的，是把上一话那个脆弱的提示词 hack，变成了一套 &lt;code&gt;tools&lt;/code&gt; 进、&lt;code&gt;tool_calls&lt;/code&gt; 出的标准协议——模型提请求，你的代码执行，模型再说话。&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;模型请求：get_weather({&amp;#34;city&amp;#34;:&amp;#34;北京&amp;#34;})
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;程序执行：多云，25℃，东北风3级
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;模型回答：北京现在多云，气温 25℃，东北风 3 级。
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;配套 Python 与 Java 实现在 &lt;a class="link" href="https://github.com/renxin2024/GYA/tree/main/c02-function-calling" target="_blank" rel="noopener"
 &gt;GYA 仓库&lt;/a&gt;与 &lt;a class="link" href="https://github.com/renxin2024/GYA-Java/tree/main/c02-function-calling" target="_blank" rel="noopener"
 &gt;GYA-Java 仓库&lt;/a&gt;的 &lt;code&gt;c02-function-calling&lt;/code&gt; 目录，按 README 的命令跑，不复制这篇文章里零散的代码块。&lt;/p&gt;
&lt;p&gt;协议跑通了，但一个更根本的问题还悬着：&lt;strong&gt;模型凭什么知道，该从菜单里挑 &lt;code&gt;get_weather&lt;/code&gt;、该填北京？&lt;/strong&gt; 这已经不是协议能回答的了——得回到模型训练本身。下一话。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DeepSeek, &lt;a class="link" href="https://api-docs.deepseek.com/guides/tool_calls/" target="_blank" rel="noopener"
 &gt;Tool Calls&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DeepSeek, &lt;a class="link" href="https://api-docs.deepseek.com/api/create-chat-completion" target="_blank" rel="noopener"
 &gt;Chat Completions API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yao et al., &lt;a class="link" href="https://arxiv.org/abs/2210.03629" target="_blank" rel="noopener"
 &gt;ReAct: Synergizing Reasoning and Acting in 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;/ul&gt;</description></item><item><title>第一话｜大模型只会补全文字，Agent 的“手脚”是怎么来的？</title><link>https://zh.renxinblog.cn/post/c01-chat-only-model/</link><pubDate>Tue, 03 Mar 2026 00:00:00 +0000</pubDate><guid>https://zh.renxinblog.cn/post/c01-chat-only-model/</guid><description>&lt;img src="https://zh.renxinblog.cn/images/c01-chat-only-model-cover.png" alt="Featured image of post 第一话｜大模型只会补全文字，Agent 的“手脚”是怎么来的？" /&gt;&lt;p&gt;几年前，我还在用 ChatGPT 查技术资料、翻译文档、写文案前先理个大纲。那时候它是“聊天窗口”：我打字，它回文字，聊完就关掉。&lt;/p&gt;
&lt;p&gt;后来出现的 Agent 工具，真的能帮我干活了——写代码、执行命令行、调用 API 查数据。&lt;/p&gt;
&lt;p&gt;从“聊”到“干”，这几年发生了什么？&lt;/p&gt;
&lt;p&gt;要回答这个问题，得先看清一件被很多人忽略的事：&lt;strong&gt;大模型从头到尾，只会做一件事。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="模型只会补全文字"&gt;模型只会补全文字
&lt;/h2&gt;&lt;p&gt;不管外面把它包装成聊天助手还是 Agent，模型干的事情始终只有一件：&lt;strong&gt;给你一段上文，它预测下一个最可能出现的词，然后把这个词接上去，再预测下一个，一直循环，直到吐出一段完整的文字。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个过程有个名字，叫“自回归生成”。它的输入是文字，输出还是文字。&lt;/p&gt;
&lt;p&gt;所以模型天生有几个边界，跟它聪不聪明没关系，是它的结构决定的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它&lt;strong&gt;没有手&lt;/strong&gt;，不会真的去点一个按钮、执行一行代码；&lt;/li&gt;
&lt;li&gt;它&lt;strong&gt;没有网络&lt;/strong&gt;，不会自己发一个 HTTP 请求去查天气；&lt;/li&gt;
&lt;li&gt;它&lt;strong&gt;没有文件系统&lt;/strong&gt;，不会自己创建一个文件、改一行代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它唯一能做的，是根据上文，猜下一段文字该怎么写。&lt;/p&gt;
&lt;p&gt;理解了这一点，再回头看那个问题就清楚了一半：一个只会补全文字的模型，是怎么“干起活”来的？&lt;/p&gt;
&lt;h2 id="让它说出要干什么"&gt;让它“说”出要干什么
&lt;/h2&gt;&lt;p&gt;答案藏在一个很朴素的技巧里。&lt;/p&gt;
&lt;p&gt;模型只会写字，那我们就&lt;strong&gt;让它把“想干什么”用固定格式写出来&lt;/strong&gt;。&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;如果你想查天气，就严格输出下面这个格式，别的什么都别写：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;{&amp;#34;action&amp;#34;: &amp;#34;get_weather&amp;#34;, &amp;#34;city&amp;#34;: &amp;#34;北京&amp;#34;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后用户问“北京今天天气怎么样”。模型还是只会补全文字，只不过这一次，它补全出来的不是一段聊天，而是一行长得很像代码的 JSON：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;action&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;city&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;北京&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;到这里，模型仍然什么都没做。它只是“说出”了：我想调 &lt;code&gt;get_weather&lt;/code&gt;，参数是北京。&lt;/p&gt;
&lt;p&gt;真正动手的是模型外面的程序。它拿到这行 JSON，解析出 &lt;code&gt;action&lt;/code&gt; 是 &lt;code&gt;get_weather&lt;/code&gt;、&lt;code&gt;city&lt;/code&gt; 是北京，然后&lt;strong&gt;自己去调用真正的查天气函数&lt;/strong&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;re&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 模型输出的那行文字&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;model_output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;{&amp;#34;action&amp;#34;: &amp;#34;get_weather&amp;#34;, &amp;#34;city&amp;#34;: &amp;#34;北京&amp;#34;}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 程序解析它&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;action&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;get_weather&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;city&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="c1"&gt;# 真正查天气的是这行代码&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;拿到天气结果后，程序再把这个结果塞回给模型，模型补全成一句人话：“北京今天多云，25 度”。&lt;/p&gt;
&lt;p&gt;看明白了吗？整条链路里，&lt;strong&gt;模型从头到尾没执行过任何东西&lt;/strong&gt;。它只是把“想干什么”用固定格式说了出来，动手的是外面那段解析加执行的代码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;看起来模型“长出了手脚”，其实是外面套了一双手。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="这套做法其实是-function-calling-的前身"&gt;这套做法，其实是 Function Calling 的前身
&lt;/h2&gt;&lt;p&gt;上面那套“提示词逼出格式、代码解析执行”的做法，不是谁凭空想出来的玩具。在原生 Function Calling 出现之前，它是开发者让模型调用工具的标准办法——先用提示词约定好输出格式，再用正则表达式或者 JSON 解析器去抠出工具名和参数。&lt;/p&gt;
&lt;p&gt;它好用，但也很脆。模型会输出残缺的 JSON、在 JSON 外面多写一句“好的，我帮你查”、或者干脆忘掉约定格式。这些问题，逼着人们后来把这件事从“提示词技巧”升级成了“协议”——也就是第二话要讲的 Function Calling。&lt;/p&gt;
&lt;p&gt;但不管怎么升级，&lt;strong&gt;底层逻辑从没变过&lt;/strong&gt;：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;模型只负责用固定的格式，把“要干什么”说清楚；真正的执行，永远发生在模型外面。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;想通这一点，整个 Agent 世界就有一把钥匙了。你之后会看到的所有花活——多步循环、记忆、MCP、Skill——都在做同一件事的不同部分：&lt;strong&gt;让模型把意图说清楚，让外面那套代码把事做成，再把结果喂回去。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而外面这套代码，我们给它起了个名字，叫 Runtime。它才是 Agent 真正“长”出来的那只手。&lt;/p&gt;
&lt;p&gt;下一篇，我们就从这套做法的标准化形态开始：模型怎么用一套协议，规规矩矩地提出一次工具调用——Function Calling。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;参考资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI, &lt;a class="link" href="https://developers.openai.com/api/docs/guides/function-calling" target="_blank" rel="noopener"
 &gt;Function Calling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Yao et al., &lt;a class="link" href="https://arxiv.org/abs/2210.03629" target="_blank" rel="noopener"
 &gt;ReAct: Synergizing Reasoning and Acting in Language Models&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>