19 KiB
title, draft, tags
| title | draft | tags | |||||
|---|---|---|---|---|---|---|---|
| Tool Calling 与 Acting:让模型学会做事 | false |
|
Tool Calling 与 Acting:让模型学会做事
一、从"说话"到"做事"
1.1 语言模型的本质限制
语言模型的能力:
给定输入 $x$,生成文本输出 $y$:
P_\theta(y|x) = \prod_{t=1}^T P_\theta(y_t | x, y_{<t}) \tag{1.1}
限制:
- 无法访问实时信息:不知道当前时间、天气、股价
- 无法执行操作:不能搜索网页、发送邮件、执行代码
- 无法与环境交互:只能处理文本,无法改变外部世界
1.2 工具调用的核心思想
核心洞察:
让语言模型不仅能"说话",还能"调用工具"——通过函数/API 调用执行操作,获取外部信息或改变外部状态。
形式化:
将语言模型扩展为:
P_\theta(y|x, \mathcal{T}) = \prod_{t=1}^T P_\theta(y_t | x, y_{<t}, \mathcal{T}) \tag{1.2}
其中 \mathcal{T} 是可用工具的集合。
工具调用示例:
用户:今天北京天气如何?
模型决定调用工具:
[TOOL_CALL] get_weather(location="北京")
[TOOL_RESULT] 晴,25°C
模型基于结果生成回答:
"今天北京天气晴朗,气温25摄氏度。"
二、Toolformer:模型自主学习工具调用
2.1 核心思想
Toolformer(Schick et al., 2023)的核心是:让模型自己决定何时调用哪些工具,以及如何调用。
关键创新:
- 工具选择:模型自己决定是否需要调用工具
- 工具调用:模型生成符合 API 格式的调用
- 结果融合:将工具返回结果融入下一步生成
2.2 Toolformer 的工具定义
定义(工具):
每个工具 t \in \mathcal{T} 由以下部分组成:
t = (name, description, parameters, return\_type) \tag{2.1}
示例:计算器工具
name: "calculator"
description: "计算数学表达式的值"
parameters: {
"expression": "str" # 要计算的表达式
}
return_type: "float" # 返回计算结果
2.3 Toolformer 的数据生成
核心优势:自监督自动构建
Toolformer 的关键创新在于无标注自监督自动构造工具调用样本——并非简单人工插入调用语句,而是让模型自行判断何地插入工具调用并验证结果,形成低成本大规模数据集。
Algorithm: Toolformer Data Generation
输入:语言模型 $\pi_0$,工具集 $\mathcal{T}$,工具调用数据集 \mathcal{D}_{\text{tool}}
步骤 1:API 相关采样
for each sample (x, y) in D:
# 判断是否适合插入工具调用
if should_insert_tool(x, y):
y' = insert_tool_calls(x, y, T) # 在适当位置插入工具调用
步骤 2:工具调用格式
在自然语言中插入特殊格式的调用:
这导致 45 + 23 = [TOOL_CALL] calculator("45 + 23") [TOOL_RESULT] 68 [TOOL_CALL_END]
所以结果是 68。
2.4 工具调用的训练目标
Toolformer 的损失函数:
在每个 token 位置 $t$,计算:
\mathcal{L}_t = -\log \pi_\theta(y_t | x, y_{<t}, \text{context}) \tag{2.2}
但只对以下 token 计算梯度:
- 自然语言 token
- 工具调用 token(API 名、参数)
- 不包含工具结果的 token
梯度阻断的三大 Token 类别:
| Token 类型 | 示例 | 梯度计算 |
|---|---|---|
| 自然语言 Token | 普通文本生成 | ✅ 回传梯度 |
| 工具调用格式 Token | [TOOL_CALL], calculator, "45+23" |
✅ 回传梯度(模型自主生成) |
| 外部工具返回结果 Token | [TOOL_RESULT] 68 |
❌ 冻结梯度(外部系统生成) |
梯度阻断的核心原因:工具返回结果由外部系统(如计算器、搜索API)生成,不属于模型自主生成内容,若对其回传梯度会导致模型学习外部系统的输出分布,破坏训练目标。
原因:工具结果是外部提供的,不应该从中学习。
2.5 工具选择的决策
何时调用工具?
模型通过 prompt 学习何时调用工具:
当你需要执行计算、查询信息或访问外部数据时,
使用以下格式调用工具:
[TOOL_CALL] tool_name("param1", "param2")
[TOOL_RESULT] result
[TOOL_CALL_END]
工业主流 JSON Function Calling 格式(OpenAI 生态标准):
{
"name": "get_weather",
"arguments": {
"location": "北京",
"unit": "celsius"
}
}
优势:
- 结构化程度高,参数类型安全校验
- 企业级落地首选方案
- 规避自定义标签解析歧义
零样本/少样本工具调用提示工程:
无需 SFT 微调,仅靠 Prompt 即可实现工具调用:
可用工具:
{
"name": "calculator",
"description": "计算数学表达式",
"parameters": {
"expression": {"type": "string", "description": "要计算的表达式"}
}
}
示例调用:
{
"name": "calculator",
"arguments": {"expression": "45 + 23"}
}
请遵循上述格式调用工具。
决策的数学表示:
P(\text{call tool} | x) = \text{softmax}(W \cdot h_T) \tag{2.3}
其中 h_T 是最后一层的隐藏状态。
三、ReAct:推理与行动的交织
3.1 核心思想
ReAct(Yao et al., 2023)的核心是:将**推理(Reasoning)和行动(Acting)**交错起来。
ReAct 的核心洞察:
人类智能是推理和行动的交织:边想边做,边做边想。
与 Toolformer 的区别:
| 方法 | 工具调用 | 推理方式 |
|---|---|---|
| Toolformer | 模型自主决定 | 工具调用穿插在生成中 |
| ReAct | 每个 step 都显式 | Thought → Action → Observation 循环 |
3.2 ReAct 的轨迹格式
ReAct 的轨迹由多个步骤组成:
输入:x
Step 1: Thought: 模型思考下一步该做什么
Action: 执行的操作(如 search, calculate)
Observation: 操作返回的结果
Step 2: Thought: 基于观察继续思考
Action: 继续操作
Observation: 继续观察
... (重复直到得到答案)
关键特性:双向交互与动态纠错
ReAct 支持环境反馈反向修正推理逻辑——可推翻之前错误思路重新规划,而非一条道走到黑。
Step 1: Thought: 我认为应该先搜索"北京天气"
Action: search("北京天气")
Observation: 结果显示"晴,25°C"
Step 2: Thought: 等等,用户问的是"明天"北京的天气,
而搜索结果显示的是"今天"的。
需要重新搜索"北京明天天气"
Action: search("北京明天天气")
Observation: 结果显示"多云,22°C"
形式化表示:
设 s_t 是第 t 步的状态,a_t 是行动,o_t 是观察。
\text{Traj} = (x, s_0) \xrightarrow{a_0} o_0 \xrightarrow{a_1} o_1 \xrightarrow{a_2} \cdots \tag{3.1}
3.3 ReAct 的数学框架
定义(状态):
状态 s_t 包含:
- 原始输入
x - 之前的思考
h_{<t} = (a_0, o_0, \ldots, a_{t-1}, o_{t-1}) - 当前任务描述
定义(行动空间):
\mathcal{A} = \mathcal{A}_{\text{search}} \cup \mathcal{A}_{\text{calc}} \cup \mathcal{A}_{\text{finish}} \cup \mathcal{A}_{\text{custom}}
其中:
- $\mathcal{A}_{\text{search}}$:检索类行动(如搜索网页、查询数据库)
- $\mathcal{A}_{\text{calc}}$:计算类行动(如执行数学运算、代码运行)
- $\mathcal{A}_{\text{finish}}$:终止行动(给出最终答案)
- $\mathcal{A}_{\text{custom}}$:自定义业务行动
定义(策略):
\pi(a_t | s_t) = P_\theta(a_t | s_t) \tag{3.2}
3.4 ReAct 的推理循环
Algorithm: ReAct
输入:问题 x,可用工具集 T
输出:最终答案 y
初始化:s_0 = (x, empty_context)
for step t = 1 to MAX_STEPS:
# 1. 思考(Thought)
thought_t = pi_thought(s_{t-1})
# thought_t 是对当前状态的推理
# 2. 行动(Action)
action_t = pi_action(s_{t-1}, thought_t)
# action_t 可以是 search/calculate/finish
# 3. 执行并观察(Observation)
if action_t.type == "search":
obs_t = search(action_t.query)
elif action_t.type == "calculate":
obs_t = calculate(action_t.expression)
elif action_t.type == "finish":
return action_t.answer
# 4. 更新状态
s_t = \text{Concat}\big(s_{t-1},\; \text{Thought}_t,\; \text{Action}_t,\; \text{Observation}_t\big)
3.5 ReAct vs CoT vs Toolformer
对比:
| 方法 | 推理结构 | 工具调用 | 反馈利用 |
|---|---|---|---|
| CoT | 线性推理链 | 无 | 无 |
| Toolformer | 工具穿插在文本中 | 自动插入 | 通过工具结果 |
| ReAct | Thought→Action→Obs 循环 | 显式决策 | 每步都有观察 |
各自适用场景:
- CoT:数学推理、逻辑推导(不需要外部信息)
- Toolformer:需要调用工具但不希望干扰生成流程
- ReAct:需要与环境交互、多步信息获取
四、工具调用的训练方法
4.1 监督微调方法
步骤:
- 数据收集:收集包含工具调用的轨迹数据
- 格式化为对话:将轨迹格式化为对话格式
- SFT 训练:在格式化数据上做监督微调
数据格式:
[USER] x
[ASSISTANT] thought: ...
action: search("query")
[SYSTEM] observation: ...
[ASSISTANT] thought: ...
action: finish("answer")
4.2 工具选择的训练
方法 1:分类头(判别式)
在语言模型上添加一个二分类头,判断是否调用工具:
P(\text{call tool} | x) = \sigma(W \cdot h_T + b) \tag{4.1}
方法 2:工具 embedding(生成式)
为每个工具学习一个 embedding,在生成时通过 attention 选择工具。
判别式 vs 生成式工具选择:
| 方式 | 核心思想 | 适用场景 |
|---|---|---|
| 判别式(分类头) | 二分类头判断是否调用工具 | 简单二选一场景 |
| 生成式(直接生成) | 直接生成工具名与参数(主流) | 复杂多工具选择 |
4.3 API 调用的格式学习
学习 API 调用格式:
模型需要学会:
- 生成正确的函数名
- 生成正确的参数(JSON 格式)
- 解析返回结果
格式约束:
使用特殊的 token 标记来约束格式:
[TOOL_CALL] calculator("45 + 23") [TOOL_RESULT] 68 [TOOL_CALL_END]
4.4 训练中的问题
问题 1:工具描述的利用
模型需要理解工具的 description 才能正确调用。
解决方案:
将工具描述作为输入的一部分:
可用工具:
- calculator: 计算数学表达式
- search: 搜索网页
请在需要时调用工具。
问题 2:工具结果的幻觉
模型可能伪造工具结果(没有调用工具但生成假结果)。
解决方案:
严格使用真实工具结果替换模型输出中的 [TOOL_RESULT] 部分。
工具调用三类典型幻觉:
| 幻觉类型 | 描述 | 治理方案 |
|---|---|---|
| 函数名幻觉 | 写错工具名称(如 get_whether 而非 get_weather) |
Schema 校验 + 相似度匹配限制 |
| 参数幻觉 | 入参格式/内容错误(如字符串未加引号) | 严格 JSON Schema 验证 |
| 结果伪造幻觉 | 未调用工具,凭空编造返回结果 | 强制真实结果替换 + 调用日志审计 |
高阶工具调用模式:
- 并行工具调用:同时发起多个独立工具请求(如同时搜索多个关键词)
- 嵌套工具调用:一个工具的输出作为另一个工具的输入
- 条件分支调用:根据中间结果选择不同工具分支
示例:并行调用
# 同时查询天气和股价
[TOOL_CALL] get_weather(location="北京")
[TOOL_CALL] get_stock(symbol="AAPL")
示例:嵌套调用
# search 结果作为 calculate 输入
result = search("北京的人口")
calculate(f"population * 2 = {result}")
五、多工具协作与规划
5.1 多工具调用序列
问题:一个任务可能需要多个工具。
示例:
任务:查找今天北京演唱会票价
Step 1: search("北京演唱会今天")
→ 获取演出名称和场馆
Step 2: calculate("场馆到最近地铁站的距离")
→ 需要先 search 地铁站信息
Step 3: 整合信息给出回答
多步依赖:
后一步的工具可能依赖于前一步的结果。
5.2 工具调用图
定义(工具依赖图):
\mathcal{G} = (V, E)
V = \{t_1, t_2, \ldots, t_n\} \quad \text{工具集合}
E = \{(t_i, t_j) | t_j \text{的输入依赖于 } t_i \text{的输出}\} \tag{5.1}
有向无环图(DAG):工具调度使用 DAG 结构规避循环调用死锁。若存在环路,拓扑排序失效,需检测并终止。
拓扑排序:
按照依赖关系排序工具调用:
t_{\pi(1)}, t_{\pi(2)}, \ldots, t_{\pi(n)} \quad \text{满足依赖顺序} \tag{5.2}
5.3 规划与重规划
静态规划:事先规划好完整调用序列
动态规划:根据每步结果调整后续调用
Algorithm: Dynamic Replanning
if last_action.failed:
# 根据失败原因调整计划
new_plan = replan(current_state, failure_reason)
else if last_observation.is_satisfactory:
# 继续原计划
pass
六、实际应用中的挑战
6.1 工具调用的可靠性
问题:模型可能调用错误的工具或参数。
解决方案:
- 参数验证:对工具参数做 schema 验证
- 错误处理:工具调用失败时重新尝试或换工具
- 置信度阈值:低于置信度阈值时不调用
6.2 工具数量与选择
问题:当工具数量很大时(如 100+),如何选择?
解决方案:
- 分层选择:先选类别,再选具体工具
- 语义匹配:用文本相似度匹配工具描述
- 工具 embedding:学习工具的向量表示
6.3 循环与终止判断
问题:如何知道任务完成了?
终止条件:
- 显式终止:调用了 finish 类型的工具
- 隐式终止:连续几步没有新信息
- 步数限制:达到最大步数
- 自反思终止:模型自我判断当前信息已足够回答,主动停止交互
自反思终止策略示例:
Step t: Thought: 我已经获取了足够的信息来回答用户的问题。
北京今天天气晴朗,气温25°C,与用户询问相符。
不需要进一步搜索。
Action: finish("北京今天天气晴朗,气温25摄氏度。")
6.4 工具结果的处理
长结果处理:
工具可能返回很长的结果(如搜索返回 10 条结果)。
解决方案:
- 截断:限制结果长度
- 摘要:对结果做摘要
- 选择性使用:只使用最相关的部分
七、数学公式速查
7.1 工具调用公式
扩展语言模型:
P_\theta(y|x, \mathcal{T}) = \prod_t P_\theta(y_t | x, y_{<t}, \mathcal{T}) \tag{7.1}
工具选择:
P(\text{call tool} | x) = \sigma(W \cdot h_T + b) \tag{7.2}
7.2 ReAct 公式
状态更新:
s_t = \text{Concat}\big(s_{t-1},\; \text{Thought}_t,\; \text{Action}_t,\; \text{Observation}_t\big) \tag{7.3}
策略:
\pi(a_t | s_t) = P_\theta(a_t | s_t) \tag{7.4}
八、总结
Toolformer vs ReAct:
| 方面 | Toolformer | ReAct |
|---|---|---|
| 工具调用方式 | 自动插入在文本中 | 显式 Thought→Action→Obs 循环 |
| 推理方式 | 工具调用穿插在生成 | 推理与行动交替 |
| 反馈利用 | 通过工具结果 | 每步都有观察 |
| 适用场景 | 需要工具辅助生成 | 需要交互探索 |
核心洞察:
Tool Calling 将语言模型从"纯文本生成器"扩展为"能够操纵外部世界的智能体"。ReAct 则进一步将推理和行动交织,使得模型能够在与环境交互中不断调整策略。
关键挑战:
- 工具选择的准确性
- 工具调用的可靠性
- 多步规划的效率
- 循环检测与终止
8.1 主流智能体框架适配
LangChain 工具调用封装:
from langchain.agents import AgentExecutor, Tool
from langchain.llms import OpenAI
tools = [
Tool(name="calculator", func=calculate, description="计算数学表达式"),
Tool(name="search", func=search_web, description="搜索网页")
]
agent = initialize_agent(tools, llm, agent_type="react")
agent.run("北京今天天气如何?")
LlamaIndex 工具调用封装:
from llama_index import download_loader
from llama_index.agent import ReActAgent
agent = ReActAgent.from_tools(tools, verbose=True)
response = agent.query("计算 45 + 23 的结果")
AgentScope 工具调用封装:
import agentscope
from agentscope.agents import ReActAgent
agent = ReActAgent(config={
"tools": [get_weather, calculator],
"model": "gpt-4"
})
8.2 工程实战落地方案
调用异常处理:
| 异常类型 | 处理策略 |
|---|---|
| 超时 | 重试3次,间隔2s/4s/8s指数退避 |
| 参数错误 | 自动纠错(如类型转换)+ 回退到自然语言回答 |
| 工具不可用 | 降级方案:直接基于已有信息回答,或提示用户 |
多轮交互上下文优化:
问题:用户进行20轮对话后,Context 溢出风险
解决方案:
1. 摘要压缩:每隔5轮用 small LLM 压缩历史
2. 滑动窗口:只保留最近 N 轮对话
3. 重要性筛选:删除与当前任务无关的历史
微调实战配比:
| 数据类型 | 推荐比例 | 作用 |
|---|---|---|
| 工具调用轨迹数据 | 30% | 专精工具调用能力 |
| 自然对话数据 | 70% | 保持通用对话能力 |
冷热方案选型:
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 快速上线验证 | 少样本Prompt | 无需训练,快速迭代 |
| 深度业务适配 | SFT微调 | 专属工具调用范式,效果更优 |
| 复杂多工具场景 | ReAct + DAG规划 | 推理+行动+环境观察闭环 |
延伸阅读:
- Schick et al., "Toolformer: Language Models Can Teach Themselves to Use Tools" (ICLR 2023)
- Yao et al., "ReAct: Synergizing Reasoning and Acting in Language Models" (ICLR 2023)
- Liu et al., "Tool Learning with Foundation Models" (2023)
- Zhou et al., "InternLM: A Multilingual Language Model with Progressively Enhanced Capabilities" (2023)
- Patil et al., "Gorilla: Large Language Models Connect with Tools" (2023)