🧭 从纠结的开局说起:模型本质上只会“吐文字”
先说一个容易被忽略的事实。大语言模型的底层能力,说穿了就是“给定一段文字,预测下一个词”。它没有手,没有眼睛,也没有网络连接,天生就是个“闭门造车”的文本生成器。那它是怎么“伸出手”去调用天气 API、搜索引擎、代码执行器这些外部工具的呢?
答案是,工具调用本质上是一种约定好的文本格式协议。模型并不是真的“调用”了什么,它只是被训练成,在恰当的时机,吐出一段符合特定格式的文本,比如一段 JSON,或者一段带有特殊标签的伪 XML。这段文本会被应用层的代码捕获、解析,然后由你写的程序去真正执行那个函数,把结果拿回来塞进对话历史,再喂给模型继续生成 。
整个链路可以用下面这张图直观表示:
flowchart LR
A[用户提问] --> B[模型decoding阶段]
B --> C{是否需要调用工具}
C -- 是 --> D[生成结构化工具调用请求]
D --> E[应用层解析JSON/XML]
E --> F[执行外部函数或API]
F --> G[结果作为新消息注入上下文]
G --> B
C -- 否 --> H[直接生成自然语言回答]
可以看到,这其实是一个循环(agent loop),模型每次只是决定要不要“喊一声工具”,真正干活的还是外部代码 。
🔧 技术细节一:工具定义如何“喂”给模型
要让模型知道有哪些工具可用、每个工具需要什么参数,开发者需要在请求里附上一份工具清单,通常是 JSON Schema 格式。这份清单会被拼接到系统提示词(system prompt)里,成为模型上下文的一部分。
以 OpenAI 的接口风格为例,一个工具定义大致是这样的:
<span>{</span>
<span>"name"</span><span>:</span> <span>"get_weather"</span><span>,</span>
<span>"description"</span><span>:</span> <span>"获取指定城市当前天气情况"</span><span>,</span>
<span>"parameters"</span><span>:</span> <span>{</span>
<span>"type"</span><span>:</span> <span>"object"</span><span>,</span>
<span>"properties"</span><span>:</span> <span>{</span>
<span>"city"</span><span>:</span> <span>{</span>
<span>"type"</span><span>:</span> <span>"string"</span><span>,</span>
<span>"description"</span><span>:</span> <span>"城市名称,例如北京"</span>
<span>}</span><span>,</span>
<span>"unit"</span><span>:</span> <span>{</span>
<span>"type"</span><span>:</span> <span>"string"</span><span>,</span>
<span>"enum"</span><span>:</span> <span>[</span><span>"celsius"</span><span>,</span> <span>"fahrenheit"</span><span>]</span>
<span>}</span>
<span>}</span><span>,</span>
<span>"required"</span><span>:</span> <span>[</span><span>"city"</span><span>]</span>
<span>}</span>
<span>}</span>
模型在训练阶段见过大量类似“工具定义 加 用户问题 加 正确的调用输出”的样本,学会了一种模式匹配——看到用户问天气,就联想到 get_weather 这个函数名,然后按 Schema 要求填参数 。
Anthropic 家的 Claude 走了一条略有不同的路,它更依赖 XML 风格标签来做结构化分隔。这不仅用在工具调用上,也是 Claude 处理复杂提示词的核心方式,官方文档明确建议用 XML 标签把指令、上下文、示例区分开,因为这种标签在训练数据里出现得非常密集,模型对它极其敏感,解析起来几乎不会出错 。
🎯 技术细节二:解码阶段的“强约束”
这是很多人容易忽略的一点。仅靠训练让模型“大概率”输出合法 JSON,其实是不够的,模型偶尔还是会漏个逗号、多个括号,导致解析失败。于是各家在推理引擎层面都上了一层约束解码(constrained decoding),也叫结构化输出(Structured Outputs)。
原理大致是这样,在模型逐词生成的过程中,系统会实时检查当前已生成的文本是否仍然符合目标 JSON Schema 的语法结构。如果某个候选 token 一旦选中就会导致语法出错,那这个 token 在采样阶段直接被“掐掉”,概率清零,模型只能从剩下合法的 token 里选 。这就好比给模型戴了一副“语法紧箍咒”,保证它吐出来的一定是能被程序解析的合法结构,而不再是靠运气。
这项技术把工具调用的可靠性从“大概率对”提升到了“几乎必定对”,也是 2024 年之后各家平台敢把“Structured Outputs”当作正式功能推出的底气所在 。
🔁 技术细节三:多轮循环与并行调用
真实场景往往不是问一句调一次工具就完事。比如你让智能体“帮我查一下三个城市的天气再做个对比”,这就涉及到并行工具调用(parallel tool calls),模型一次性生成多个调用请求,应用层并发执行后再统一汇总结果喂回去。
整个多轮交互的上下文管理也很讲究。随着对话轮次增加,历史信息会越堆越多,Anthropic 的工程博客里提到,实际生产环境中会做上下文压缩和摘要,把不那么关键的历史工具调用结果精简掉,避免上下文窗口被无关信息占满,影响模型后续判断质量 。
下面这张图展示了带并行调用和上下文管理的完整闭环:
sequenceDiagram
participant U as 用户
participant M as 大模型
participant T1 as 工具A
participant T2 as 工具B
U->>M: 提出复合请求
M->>M: 判断需并行调用
M->>T1: 调用请求1
M->>T2: 调用请求2
T1-->>M: 返回结果1
T2-->>M: 返回结果2
M->>M: 汇总結果生成回答
M->>U: 输出最终答案
📊 三大平台实现方式对比
各家大模型厂商在工具调用协议上的设计理念不完全一样,但核心思路殊途同归。
| **厂商 / 模型** | **协议风格** | **约束解码支持** | **典型特点** |
|---|---|---|---|
| OpenAI GPT 系列 | JSON Schema 原生 function calling | 支持 Structured Outputs | 与 Responses API 深度整合,工具定义规范化程度高 |
| Anthropic Claude | XML 标签 加 结构化 tool\_use 块 | 部分支持 | 依赖标签分隔,长上下文场景解析稳定性好 |
| Together AI 等开源生态 | 兼容 OpenAI 格式的 JSON 模式 | 支持 | 更灵活,适配多种开源模型 |
不难看出,虽然表面协议不同,底层逻辑却高度一致,都是靠训练让模型学会格式,靠解码约束保证格式正确,靠应用层代码完成真正执行 。
🧠 训练层面是怎么“教”会模型的
前面说的都是推理阶段的机制,那模型是怎么在训练时学会“看情况调用工具”这个技能的呢?简单来说分两步。
第一步是监督微调(SFT),研究团队构造大量“用户问题 加 正确工具调用 加 工具返回结果 加 最终回答”的完整轨迹样本,让模型见多识广,模仿这种问答模式。
第二步是强化学习(RL),针对工具调用是否成功、参数是否合理、最终答案是否准确,设计奖励信号,让模型在探索中进一步优化调用策略,减少瞎调用或者调用参数瞎填的情况 。
这也是为什么早期大模型经常“乱调用工具”或者“该调用时不调用”,而经过专门训练的新版本模型在工具选择的准确性上会有明显提升。
🧩 一段实际调用代码示例
为了让整个流程更具体,这里给一段简化的 Python 伪代码,展示应用层大致是怎么接住模型的工具调用请求并执行的。
<span>import</span> json
<span>def</span> <span>get_weather</span>(<span>city, unit=<span>"celsius"</span></span>):
<span># 这里省略真实API调用,返回模拟数据</span>
<span>return</span> {<span>"city"</span>: city, <span>"temperature"</span>: <span>23</span>, <span>"unit"</span>: unit}
<span>def</span> <span>handle_model_response</span>(<span>response</span>):
<span>if</span> response.get(<span>"tool_calls"</span>):
<span>for</span> call <span>in</span> response[<span>"tool_calls"</span>]:
func_name = call[<span>"name"</span>]
args = json.loads(call[<span>"arguments"</span>])
<span>if</span> func_name == <span>"get_weather"</span>:
result = get_weather(**args)
<span># 将结果作为新的消息注入对话历史</span>
append_to_context(role=<span>"tool"</span>, content=json.dumps(result))
<span>else</span>:
<span>print</span>(response[<span>"content"</span>])
这段代码的核心逻辑,就是把模型生成的结构化请求“翻译”成一次真实的函数调用,再把结果原样塞回对话,让模型接着往下说。整个过程模型本身没有任何“执行权限”,一切风险控制、权限校验都掌握在应用层代码手里,这也是工具调用设计里非常关键的一个安全边界 。
💡 小结
工具调用听起来玄乎,拆开来看其实就是三件事情叠加的结果,训练阶段让模型学会用结构化文本表达调用意图,推理阶段用约束解码保证格式绝对合法,应用层负责真正执行并把结果喂回去形成闭环。理解了这套机制,你就会发现,所谓智能体、自动化助理,底层跑的都是这同一套“文本协议加循环执行”的老套路,只是在工具丰富度和调用精准度上不断打磨升级罢了。
参考资料
The guide to structured outputs and function calling with LLMs, agenta.ai
Structured Outputs with LLMs JSON Mode Function Calling and When to Use Each, towardsdatascience.com
Announcing function calling and JSON mode, together.ai
What Is LLM Function Calling, blaxel.ai
Prompting best practices Claude Platform Docs, platform.claude.com
Effective context engineering for AI agents, anthropic.com
文章把工具调用拆成协议、解码、执行三层,讲解清晰,适合想理解 Agent 底层机制或落地 Function Calling 的开发者快速建立全局认知。