Function Calling:LLM 是怎么学会调工具的
目录
- [Function Calling是什么](#Function Calling是什么)
- 工具定义怎么喂给模型
- 模型为什么能生成工具调用
- 模型怎么决定调不调工具
- 两层定义的关系
- [和原生 JSON 输出的区别](#和原生 JSON 输出的区别)
- 小结
Tool Use 的 Agent 侧实现通常比较容易理解:LLM 输出工具名称和参数,Agent 根据名称从工具注册表中找到对应函数,然后执行调用(参考笔者主页的《Tool Use:Agent是如何调用工具的》)。但这里还有一个关键问题经常被忽略:LLM 为什么能够生成符合要求的工具调用结构?本质上,LLM 仍然只是一个文本生成模型,它并不会真正调用工具,只是在根据上下文预测下一个 token。类似 {"name":"bash","command":"ls -la"} 这样的结构化请求,并不是模型内部存在一个"调用工具"的动作,而是在提示词约束、训练数据以及模型接口规范的共同作用下,让模型按照约定格式生成一段文本,随后由 Agent 框架解析这段文本,并映射到实际的工具函数执行。因此,LLM 负责产生调用意图和参数,Agent 框架负责完成工具发现、参数校验和实际执行,二者共同组成了完整的 Tool Use 流程。
Function Calling是什么
Function Calling 是让 LLM 输出结构化工具调用请求的机制。模型在推理时拿到工具定义,判断用户意图是否需要调用某个工具,如果需要,就输出一段符合约定格式的 JSON;如果不需要,就正常回答。
可以把 Function Calling 理解成"填工单"的过程。用户提了一个需求,模型判断这个需求需要某个部门配合,于是填了一张工单(工具名 + 参数),交给外部系统去执行。模型本身不执行任何操作,它只负责判断要不要填工单、填哪个、参数怎么写。
和上一篇 Tool Use 的关系是这样的:

Function Calling 是上游,Tool Use 是下游。一个管"怎么生成工单",一个管"怎么执行工单"。
工具定义怎么喂给模型
模型不是天生就知道有哪些工具可用。你需要把工具的定义告诉它,方式很直接:拼进 prompt。
来看一个实际的工具定义。假设我们有一个查询天气的工具,用 Anthropic 的格式定义:
python
tools = [
{
"name": "get_weather",
"description": "查询指定城市的当前天气",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 '北京'、'上海'"
}
},
"required": ["city"]
}
}
]
OpenAI 的格式稍有不同,字段名用的是 parameters 而不是 input_schema:
python
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的当前天气",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称"
}
},
"required": ["city"]
}
}
}
]
| 维度 | Anthropic | OpenAI |
|---|---|---|
| 外层字段 | input_schema |
parameters |
| 工具定义位置 | 直接在对象内 | 嵌套在 function 内 |
| 响应格式 | content 数组中的 tool_use 块 |
message.tool_calls 数组 |
| 结果回传 | tool_result content block |
role: "tool" 消息 |
格式不同,但本质一样:把工具的名字、用途、参数格式以结构化的方式传给模型。从模型能力的角度看,工具定义最终会以模型能够理解的上下文形式参与推理,本质上是向模型提供了一组关于工具名称、用途和参数约束的信息。不同厂商在接口层可能会采用不同的编码方式(比如通过特殊 token 或 hidden instruction 注入),但目的都是让模型获得工具描述。
把工具定义序列化后,模型实际看到的内容大概是这样的(以 Anthropic 格式为例):
You have access to the following tools:
<tool_name>get_weather</tool_name>
<tool_description>查询指定城市的当前天气</tool_description>
<tool_input_schema>
{
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"]
}
</tool_input_schema>
对模型来说,这就是 prompt 的一部分。它通过理解这段文本来理解"我有一个叫 get_weather 的工具,接收一个 city 参数"。没有编译器,没有类型检查,纯粹是文本理解。
这也解释了为什么 工具的 description 字段那么重要 。模型决定用哪个工具,靠的不是代码逻辑,是语义匹配。你写 "查询天气" 和写 "获取指定地理位置的气象数据,包括温度、湿度和风速",模型的判断行为会不同。description 写得越准确,模型选对工具的概率越高。
模型为什么能生成工具调用
LLM 的基础能力是续写文本,给它一段话,它预测下一个 token。那它是怎么学会在合适的时机输出一段合法 JSON 的?
通常可以理解为两个阶段:先通过 SFT 等监督训练让模型掌握工具调用的格式和基本模式,再通过偏好优化、强化学习或大量工具使用数据训练,让模型进一步学习什么时候应该调用工具、调用哪个工具以及如何填写参数。
第一步:SFT 学格式
SFT(Supervised Fine-Tuning,监督微调)的核心是给模型看大量的"标准答案"。训练数据是 (query, expected_tool_call) 的配对:
输入:
用户: "北京今天天气怎么样?"
可用工具: [{"name": "get_weather", ...}]
输出:
{"name": "get_weather", "arguments": {"city": "北京"}}
输入:
用户: "帮我读一下 config.yaml"
可用工具: [{"name": "read_file", ...}, {"name": "bash", ...}]
输出:
{"name": "read_file", "arguments": {"file_path": "config.yaml"}}
输入:
用户: "你好"
可用工具: [{"name": "get_weather", ...}, {"name": "read_file", ...}]
输出:
你好!有什么可以帮你的吗?
第三个例子很关键,模型不仅要学会"怎么输出工具调用",还要学会"什么时候不该调工具"。用户说"你好",没有任何工具调用的意图,模型应该正常回复。
通过大量这样的样本训练,模型学会了一个模式:当用户的问题匹配某个工具的用途时,输出符合该工具 input_schema 的 JSON;否则,正常生成文本。
上面的例子用简化 JSON 来展示工具调用意图。实际格式会根据模型厂商和 API 协议不同有所差异,比如 OpenAI 的工具调用会被包装在 tool_calls 数组中,Anthropic 则以 tool_use content block 的形式返回。但无论外层格式怎么包装,模型内部做的事情是一样的:生成工具名和参数。
来看一个 SFT 训练循环的伪代码:
for each (query, tools, expected_output) in training_data:
# 拼接 prompt:system + 工具定义 + 用户消息
prompt = build_prompt(system_msg, tools, query)
# 模型生成输出
predicted = model.generate(prompt)
# 计算 loss:模型输出和标准答案的差异
loss = cross_entropy(predicted, expected_output)
# 反向传播,更新模型参数
loss.backward()
optimizer.step()
训练完成后,模型就具备了"看到工具定义 → 输出结构化调用"的基础能力。但 SFT 有一个问题:它教的是"照着样本抄",模型学到的是格式,不是判断力。
第二步:偏好优化与工具使用训练
SFT 让模型学会了格式,但在边界情况下经常犯错:
- 用户只是随口聊天,模型却调了工具(不该调的时候调了)
- 用户明显需要工具帮助,模型却在那瞎编答案(该调的时候没调)
- 输出的 JSON 格式对了,但参数值是编的(hallucination)
这个阶段的训练手段不只是 RLHF,还包括 DPO(Direct Preference Optimization)、IPO 等偏好优化方法,以及大规模合成工具调用数据的训练。不同厂商的方案各有侧重,但目标一致:让模型学会更精准的调用策略。
以偏好优化为例,训练的核心思路是给模型展示"好"和"差"的对比:
用户提问: "帮我查一下什么是量子计算"
│
├── 候选 A:正常回答,不调工具 → 偏好(正例)
└── 候选 B:调用了搜索工具 → 不偏好(负例)
用户提问: "帮我查一下北京今天多少度"
│
├── 候选 A:调用了 get_weather 工具 → 偏好(正例)
└── 候选 B:瞎编了一个温度 → 不偏好(负例)
模型通过大量这样的对比样本,逐渐学会了一个更精细的判断:不是所有包含"查"、"读"、"获取"的问题都需要调工具。"帮我查一下什么是量子计算"和"帮我查一下北京今天多少度",表面结构很像,但前者是知识问答,后者才需要调天气工具。
两步训练的分工:
| 训练阶段 | 学什么 | 解决什么问题 |
|---|---|---|
| SFT | 输出格式、工具名匹配 | 模型知道工具调用长什么样 |
| 偏好优化/强化学习 | 调用时机、参数准确性 | 模型知道什么时候该调、什么时候不该调 |
SFT 是"学会填工单",后续的偏好优化和工具使用训练是"学会判断什么时候需要填工单"。两者缺一不可。
模型怎么决定调不调工具
从 token 生成的角度看,模型做的其实是连续的条件概率计算。
假设用户说"北京今天天气怎么样",模型逐 token 生成输出。当生成到第一个 token 的位置时,它面前有两条路:
路径 1: {" → 继续输出工具调用 JSON
路径 2: 北 → 继续输出自然语言回答("北京今天...")
模型根据 prompt 中的工具定义、用户意图、以及训练时学到的模式,计算下一 token 的概率分布。在采样过程中,如果工具调用相关的 token 序列具有更高概率,最终就可能生成工具调用结构。
之后每一步都是同样的过程。当模型输出 {"name": "get_weather" 的时候,剩下的参数选择也是一系列 token 级别的概率计算。

stop_reason 是 API 层返回的状态标识,用于告诉客户端本次生成是否因为工具调用而结束。当模型判断不需要调工具时,它会正常生成文本直到结束,stop_reason 为 end_turn;当它判断需要调工具时,输出工具调用块后停止,stop_reason 为 tool_use。不同厂商的实现方式不同,但这个字段帮助 Agent 代码快速判断下一步该做什么:拿到 tool_use 就去执行工具,拿到 end_turn 就把结果返回给用户。
两层定义的关系
Tool Use 那篇我们提过,工具定义了两遍:JSON Schema 给 LLM 看,注册表给代码看。现在从模型侧来看这两层的关系。
JSON Schema 定义了工具的名字、参数类型、必填字段,模型通过理解这些信息来生成调用请求。当它决定调用某个工具时,按照理解到的格式输出 JSON。
真正的执行在 Agent 侧。Agent 代码拿到模型输出的 JSON,解析出工具名,去注册表里查找对应的执行函数。两层定义通过"工具名"这个字符串桥接:
工具定义(JSON Schema)
│
│ 拼接进 prompt
▼
LLM 理解工具的 name、description、parameters
│
│ 输出 {"name": "bash", "command": "ls"}
▼
Agent 代码解析输出
│
│ 用 "bash" 去注册表查找
▼
注册表 → execute_bash 函数 → 执行命令
模型不需要知道注册表的存在,注册表也不需要知道工具定义长什么样。两者通过工具名解耦。这也是为什么不同厂商的 JSON Schema 格式可以不同(Anthropic 用 input_schema,OpenAI 用 parameters):格式不同不影响模型理解工具,也不影响 Agent 侧的执行。
一个容易忽略的细节:工具的 description 比 schema 本身更影响模型行为 。模型选工具靠的是语义匹配,不是代码解析。同样一个 read_file 工具,description 写"读文件"和写"读取指定路径的文件内容,返回 UTF-8 编码的文本",模型在遇到"帮我看看这个文件里写了什么"时的匹配概率是不同的。
和原生 JSON 输出的区别
你可能会想:让模型输出 JSON 不是早就能做了吗?直接在 prompt 里说"请以 JSON 格式回复",模型也能输出 JSON。Function Calling 和这有什么区别?
区别在于语义。
| 维度 | Function Calling | 原生 JSON 输出 |
|---|---|---|
| 目的 | 告诉外部系统"我要调用某个工具" | 输出结构化数据 |
| 输出语义 | 工具名 + 参数,有明确的调用意图 | 任意 JSON,没有调用意图 |
| 是否触发副作用 | 是,Agent 会根据输出执行工具 | 否,纯数据,不执行操作 |
| 训练方式 | SFT + 偏好优化/强化学习 | prompt engineering 即可 |
| 可靠性 | 高,格式基本稳定 | 低,模型可能输出非法 JSON |
用一个具体例子说明。用户问:"北京今天多少度?"
Function Calling 输出:
json
{"name": "get_weather", "arguments": {"city": "北京"}}
这是一个工具调用请求。Agent 收到后会执行 get_weather(city="北京"),拿到真实数据返回给用户。
原生 JSON 输出(prompt 要求以 JSON 回复):
json
{"city": "北京", "temperature": "25°C", "condition": "晴"}
这是一个普通的数据对象。但这个温度是模型编的,不是真实数据。模型没有调用任何工具,它只是按照 JSON 格式输出了它"认为"的答案。
Function Calling 的价值不在于"输出 JSON",而在于模型通过训练学会了两个判断:什么时候该调工具 ,以及调哪个工具、参数怎么填。原生 JSON 输出没有这个判断能力,它只是格式化了模型的文本生成。
小结
Function Calling 的本质并不是让 LLM 获得执行工具的能力,而是让模型具备在特定上下文下生成结构化调用意图的能力。LLM 负责理解用户需求、选择合适的工具并生成参数,Agent Runtime 负责工具校验、实际执行以及结果回传,二者结合形成完整的工具调用闭环。
在实际项目中接入 Function Calling 时,工具定义的设计非常重要。其中,工具的名称、description 以及参数 schema 都会影响模型的调用效果。清晰准确的工具描述能够帮助模型更好地理解工具用途,减少错误选择和参数填写问题。同时,还需要结合实际运行情况观察模型的调用稳定性,例如是否存在误调用、漏调用或参数错误等问题,再通过优化提示词、调整工具设计、增加训练数据等方式持续改进。