Tool Use 实战:让 LLM 调用外部工具的 3 种模式
阅读时间:约 15 分钟
前置知识:了解 LLM 是什么;能跑通一个 Python 脚本;读过 P01(或理解 Agent 的闭环概念)
上一篇我们搞清楚了"什么不是 Agent"。这一篇,我们让 LLM 真正调用一个外部工具。
第一部分:Tool Use 的底层原理:没有魔法,只是 Token 预测
很多人以为 Function Calling(工具调用)是 LLM 的"新能力"。其实不是。
Function Calling 没有引入任何新的推理机制。 它的本质是:模型通过训练,学会了在特定情况下切换输出模式:从"生成自然语言"切换到"生成结构化 JSON"。
这个训练分两个阶段:
- 监督微调(SFT):给模型看大量"用户问了什么 → 模型该调什么工具 → 参数怎么填"的样本
- 强化学习(RL):让模型在真实场景中尝试调用工具,调对了给奖励,调错了惩罚
所以当你传入工具定义时,模型做的其实是:
- 阅读工具的
description,判断自己能不能处理 - 理解
parameters的 schema,确认参数类型和必填项 - 输出一个 JSON 对象作为工具调用请求
工具描述的质量,直接决定了模型调用的准确率。 这是很多人忽略的一点。2026 年 6 月的一篇 arXiv 论文甚至直接以"MCP Tool Descriptions Are SmellyMCP !"(MCP 工具描述有异味)为题,论证了糟糕的工具描述如何拖垮 Agent 效率。
🤔 思考一下:理解这个原理后,你能解释为什么有时候模型会"调错工具"吗?(提示:工具描述重叠、不清晰)
📌 本章要点:Function Calling 本质是 Token 预测的模式切换。工具描述的质量 > 模型本身的能力。
刚才我们理解了原理。接下来用 3 种方式,让 LLM 调用一个真实的外部工具。
💡 运行环境:以下代码全部使用
openai>=1.0.0Python SDK(pip install openai)。你需要一个支持 Function Calling 的模型 API:
- 安装了
openai>=1.0.0:pip install openai- 配置了支持 Function Calling 的模型 API(见下方)
- 推荐 Python 3.10+
🔑 模型选择(任选其一):
- 如果你有 OpenAI 账号:设置
OPENAI_API_KEY,模型用gpt-4.1或gpt-4.1-mini- 国内方案 DeepSeek:设置
DEEPSEEK_API_KEY,base_url='https://api.deepseek.com',模型用deepseek-v4-pro- 国内方案通义千问:设置
DASHSCOPE_API_KEY,base_url='https://dashscope.aliyuncs.com/compatible-mode/v1',模型用qwen3-max代码中已用占位
YOUR_MODEL_NAME和YOUR_BASE_URL,根据你选择的方案替换即可。
第二部分:3 种 Tool Use 模式:从最简到最实用
模式一:手动拼装(理解原理用)
最原始的方式:自己拼 JSON,手动管理整个流程。这个模式的目的是让你看清每一步,而不是用在生产环境。
📋 运行说明:复制代码保存为
mode1.py,修改YOUR_MODEL_NAME和YOUR_BASE_URL,运行python mode1.py。
import openai # 导入 OpenAI SDK
import json # 导入 JSON 模块(Python 标准库,无需安装)
# ═══════════════════════════════════════════
# 第 1 步:定义工具
# 告诉模型"你手里有哪些工具可以用"
# ═══════════════════════════════════════════
tools = [{ # 定义一个工具列表(可以包含多个工具)
"type": "function", # 固定值:声明这是一个函数型工具
"function": { # 工具的详细信息
"name": "get_weather", # 工具名称(模型通过这个名字匹配意图)
"description": "查询指定城市的当前天气", # 工具描述(模型读这个来决定是否调用)
"parameters": { # 参数 schema(限制模型传哪些参数、什么类型)
"type": "object", # 参数整体是对象类型
"properties": { # 对象的各个字段定义
"city": { # city 字段的定义
"type": "string", # 字段类型:字符串
"description": "城市名称,如'北京'" # 字段描述
}
},
"required": ["city"] # 必填字段列表
}
}
}]
# ═══════════════════════════════════════════
# 第 2 步:调用模型
# 把用户问题和工具定义一起发给模型
# ═══════════════════════════════════════════
client = openai.OpenAI( # 创建 OpenAI 客户端
base_url="YOUR_BASE_URL" # 替换为你的 API 地址
)
response = client.chat.completions.create( # 调用聊天补全 API
model="YOUR_MODEL_NAME", # 替换为你的模型名
messages=[ # 消息历史(只有一个用户消息)
{"role": "user", # 消息角色:用户
"content": "北京今天天气怎么样?"} # 用户问题
],
tools=tools, # 传入我们定义的工具列表
tool_choice="auto" # 让模型自己决定要不要调用工具
)
# ═══════════════════════════════════════════
# 第 3 步:解析 Tool Call
# 检查模型是否决定调用工具
# ═══════════════════════════════════════════
msg = response.choices[0].message # 获取第一个回复消息
if msg.tool_calls: # 检查消息中是否包含工具调用
tool_call = msg.tool_calls[0] # 获取第一个工具调用
args = json.loads(tool_call.function.arguments) # 解析参数(JSON 字符串 → 字典)
print(f"模型想调用: {tool_call.function.name}") # 打印工具名
print(f"参数: {args}") # 打印参数
# 输出: 模型想调用: get_weather
# 输出: 参数: {'city': '北京'}
运行后你会看到:
模型想调用: get_weather
参数: {'city': '北京'}
这个代码只"问"了模型想调用什么,还没真正执行工具。下一步要调用工具函数并拿到结果。
📌 本章要点:手动模式让你看清 Tool Use 的 4 个步骤:定义工具 → 调用模型 → 解析 Tool Call → 执行函数。
刚才我们看到了最原始的调用方式。接下来用 OpenAI 的 Responses API,很多步骤被封装好了。
模式二:Responses API(快速上手,仅 OpenAI 官方支持)
OpenAI 的 Responses API 把"调用模型 → 解析 Tool Call → 执行函数 → 提交结果 → 返回最终回答"这些步骤全部封装好。
⚠️ 重要:Responses API 目前仅 OpenAI 官方支持,国内模型(DeepSeek、通义千问)暂不兼容。如果你用的是国内模型,直接跳到模式三。
📋 运行说明:复制保存为
mode2.py。需要openai>=1.60.0(pip install openai --upgrade)和 OpenAI API Key。
import openai # 导入 OpenAI SDK
import json # 导入 JSON 模块
# ═══════════════════════════════════════════
# 第 1 步:定义工具
# 工具定义是通用的,不管哪种模式都需要
# ═══════════════════════════════════════════
tools = [{ # 定义一个工具列表
"type": "function", # 固定值:函数型工具
"function": { # 工具详细信息
"name": "get_weather", # 工具名称
"description": "查询指定城市的当前天气", # 工具描述
"parameters": { # 参数 schema
"type": "object", # 类型:对象
"properties": { # 字段定义
"city": { # city 字段
"type": "string", # 字符串类型
"description": "城市名称" # 字段描述
}
},
"required": ["city"] # 必填字段
}
}
}]
# ═══════════════════════════════════════════
# 第 2 步:定义工具执行函数
# 这是你自己写的、真正执行业务逻辑的代码
# ═══════════════════════════════════════════
def get_weather(city: str) -> str:
# 参数:city = 城市名称(字符串)
# 返回值:天气信息(JSON 字符串)
# 实际项目中,这里会调用真实天气 API(如和风天气、高德)
# 这里用模拟返回值代替
return json.dumps({"city": city, "temp": "22°C", "weather": "晴"})
# ═══════════════════════════════════════════
# 第 3 步:调用 Responses API
# 以下步骤全部自动完成:
# 1. 把工具定义和消息发给模型
# 2. 模型决定调用工具 → API 自动执行你的函数
# 3. 把执行结果提交回模型
# 4. 返回模型最终生成的回答
# ═══════════════════════════════════════════
client = openai.OpenAI() # 创建 OpenAI 客户端
response = client.responses.create( # 调用 Responses API(不是 chat.completions)
model="gpt-4.1", # 模型名(⚠️ 仅 OpenAI 官方支持此 API)
input="北京今天天气怎么样?", # 用户输入
tools=tools # 传入工具列表,自动处理调用循环
)
# ═══════════════════════════════════════════
# 第 4 步:拿到最终回答
# response.output_text 已经是模型最终生成的回答
# ═══════════════════════════════════════════
print(response.output_text) # 打印最终回答
# 输出: 北京今天天气晴朗,气温22°C。
运行后你会看到:
北京今天天气晴朗,气温22°C。
模式二和模式一的区别:模式二把"解析 Tool Call → 执行函数 → 提交结果 → 让模型再回答"这 4 步全封装了,你只需要一行 responses.create。
📌 本章要点:Responses API 适合快速原型。但国内模型暂不支持,且生产环境中需要更细粒度的控制。
前两种模式都是一次调用一个工具。真实场景中,经常需要连续调用多个工具,在循环中反复判断。
模式三:Agent 循环(生产环境用,所有模型通用)
这是最接近"真正的 Agent"的模式:模型在一个循环中反复"思考 → 调用工具 → 观察结果 → 再思考"。这是生产环境的标准写法。
📋 运行说明:复制保存为
mode3.py。需要openai>=1.0.0(pip install openai)。修改YOUR_MODEL_NAME和YOUR_BASE_URL。
import openai # 导入 OpenAI SDK
import json # 导入 JSON 模块
# ═══════════════════════════════════════════
# 初始化:创建客户端
# ═══════════════════════════════════════════
client = openai.OpenAI( # 创建 OpenAI 客户端
base_url="YOUR_BASE_URL" # 替换为你的 API 地址
)
# ═══════════════════════════════════════════
# 第 1 步:定义多个工具
# 这里定义 2 个工具:查询天气 + 数学计算
# 实际项目中,工具列表可能包含几十个甚至上百个
# ═══════════════════════════════════════════
tools = [ # 定义多个工具(数组,可包含任意数量)
{ # 工具 1:查询天气
"type": "function", # 固定值:函数型工具
"function": { # 工具 1 的详细信息
"name": "get_weather", # 工具名称
"description": "查询城市天气", # 工具描述
"parameters": { # 参数 schema
"type": "object",
"properties": {
"city": { # city 参数:城市名称
"type": "string",
"description": "城市名"
}
},
"required": ["city"]
}
}
},
{ # 工具 2:数学计算
"type": "function", # 固定值
"function": { # 工具 2 的详细信息
"name": "calculate", # 工具名称
"description": "执行数学计算", # 工具描述
"parameters": { # 参数 schema
"type": "object",
"properties": {
"expression": { # expression 参数:数学表达式
"type": "string",
"description": "数学表达式,如'2+3*4'"
}
},
"required": ["expression"]
}
}
}
]
# ═══════════════════════════════════════════
# 第 2 步:定义工具执行函数
# 根据工具名执行不同逻辑
# ═══════════════════════════════════════════
def execute_tool(name, args):
"""
根据工具名和执行参数,返回执行结果
name: 工具名称(字符串)
args: 工具参数(字典)
返回: 执行结果(JSON 字符串)
"""
if name == "get_weather": # 如果调用的是 get_weather
# 用城市名查询天气(此处模拟返回)
return json.dumps({"city": args["city"], "temp": "22°C"})
elif name == "calculate": # 如果调用的是 calculate
# 执行数学表达式
# ⚠️ 实际项目中不要直接用 eval(),用安全表达式计算器
return json.dumps({"result": eval(args["expression"])})
return json.dumps({"error": "unknown tool"}) # 工具名不存在
# ═══════════════════════════════════════════
# 第 3 步:启动 Agent 循环
# 核心逻辑:模型反复"思考 → 调用工具 → 观察结果 → 再思考"
# ═══════════════════════════════════════════
messages = [ # 初始化消息历史
{"role": "user", # 消息角色:用户
"content": "北京和上海的温差是多少度?北京22度,上海28度"} # 用户问题
]
for i in range(5): # 最多循环 5 次(防止模型陷入死循环)
response = client.chat.completions.create( # 调用模型
model="YOUR_MODEL_NAME", # 替换为你的模型名
messages=messages, # 传入当前消息历史
tools=tools, # 传入工具列表
tool_choice="auto" # 让模型自己决定是否调用工具
)
msg = response.choices[0].message # 获取模型回复消息
messages.append(msg) # 把模型回复加入消息历史
# ──── 判断:模型是否调用了工具? ────
if msg.tool_calls: # 检查是否有工具调用
for tc in msg.tool_calls: # 遍历所有工具调用(可能同时多个)
args = json.loads(tc.function.arguments) # 解析参数
result = execute_tool(tc.function.name, args) # 执行工具
messages.append({ # 把工具结果加入消息历史
"role": "tool", # 角色必须是 "tool"
"tool_call_id": tc.id,# 关联调用 ID(模型靠它匹配结果)
"content": result # 工具执行的结果
})
continue # 继续循环,让模型根据工具结果再推理
# ──── 判断:模型决定最终回答(不再调用工具) ────
print(msg.content) # 打印最终回答
break # 退出循环
运行后你会看到:
北京气温22°C,上海气温28°C,温差是6°C。
关键设计点:
for i in range(5):最大循环 5 次,防止模型不断调用工具陷入死循环tool_choice="auto":让模型自己决定是否需要调用工具role: "tool":角色必须是"tool",否则模型不知道这是工具返回的结果tool_call_id:每个工具调用有唯一 ID,模型用它来关联结果和调用
这其实就是 Cursor、Claude Code 等编码 Agent 的底层工作原理。Cursor 在 2026 年 6 月刚被 SpaceX 以 600 亿美元收购,很大一部分价值就来自这套工具调用的可靠性。
🤔 思考一下:对比 P01 里的决策循环图:这个代码里的"感知"(messages)、“决策”(chat.completions.create)、“执行”(execute_tool)、“判断”(msg.tool_calls)分别对应哪几行?
📌 本章要点:Agent 循环 = 模型反复"思考 → 调用 → 观察 → 再思考"。核心是循环控制(最大次数)和结果反馈(tool message)。
第三部分:踩坑实录:你一定会遇到的 4 个问题
坑 1:工具描述写得像 API 文档
这是 2026 年 6 月 arXiv 论文"MCP Tool Descriptions Are Smelly!"重点讨论的问题。工具描述的质量直接影响 Agent 效率。
错误示例:
{
"name": "query_db",
"description": "Query database with SQL statement",
"parameters": {
"properties": {
"sql": {"type": "string"}
}
}
}
问题:描述太抽象:模型不知道什么时候该用、什么时候不该用、参数怎么填。
正确示例:
{
"name": "query_db",
"description": "查询用户数据库。只用于读取数据(SELECT),不能用于修改数据(INSERT/UPDATE/DELETE)。如果用户要求修改数据,回复'我只能查询数据,修改请联系管理员'。",
"parameters": {
"properties": {
"sql": {
"type": "string",
"description": "SQL 查询语句,只支持 SELECT。示例:SELECT * FROM users WHERE city='北京'"
}
}
}
}
工具描述不是 API 文档,是给模型的指令。 你要告诉模型:什么时候用、什么时候不用、参数怎么填、边界在哪里。
📌 本章要点:工具描述 = 给模型的指令,不是 API 文档。写清触发条件、参数示例、边界约束。
坑 2:模型调错了工具 / 填错了参数
Function Calling 的失败模式有 4 种:
| 失败类型 | 表现 | 原因 |
|---|---|---|
| 调错工具 | 该用 A 工具,用了 B | 工具描述重叠、不清晰 |
| 参数填错 | 工具用对了,参数格式/值错误 | schema 不够严格 |
| 该调没调 | 需要工具时模型直接回答了 | 工具描述没写清触发条件 |
| 不该调时调了 | 不需要工具时模型硬调了 | 工具描述太宽泛 |
一个真实 bug:截至 2026 年 6 月,DeepSeek V4 在使用 tool_choice="required" 时,实际强制调用成功率约 60%(GitHub Issue #1376)。这意味着在复杂 Agent 场景下,你不能完全依赖"强制调用",需要在代码层做兜底。
解决方案:把失败模式映射到工具描述的改进上,同时在代码中做好重试和降级逻辑。Patronus AI(2026 年 6 月获 5000 万美元融资)做的就是把这类失败模式变成可量化的评测指标。
🤔 思考一下:你遇到过哪种失败?当时是怎么处理的?如果重来,你会改工具描述还是改代码?
坑 3:没有幂等性保护
真实场景:Agent 调用"创建订单"工具,第一次调用成功了,但网络超时没收到返回。Agent 以为失败了,自动重试:结果创建了两个订单。
解决方案:给写操作加幂等性键。
📋 运行说明:复制保存为
idempotency.py,运行python idempotency.py
import json
# 模拟数据库(实际项目替换为 Redis/PostgreSQL)
db = {}
def create_order(item_id: str, idempotency_key: str) -> dict:
"""
创建订单,带幂等性保护
item_id: 商品 ID(字符串)
idempotency_key: 幂等性键(每次调用传入唯一请求 ID)
返回: 订单结果(字典)
幂等性保护原理(三步):
1. 先检查 idempotency_key 是否已存在缓存
2. 如果存在 → 直接返回缓存结果(不重复执行)
3. 如果不存在 → 执行操作 → 缓存结果 → 返回
"""
# 第 1 步:检查是否已处理过
cache_key = f"idempotency:{idempotency_key}" # 构建缓存键
if db.exists(cache_key): # 检查缓存中是否有这个请求的结果
return db.get(cache_key) # 返回缓存结果(不重复执行)
# 第 2 步:执行操作(只有第一次到达这里)
order_result = db.create_order(item_id) # 调用数据库创建订单
# 第 3 步:缓存结果(用于后续相同请求)
db.set(cache_key, order_result) # 缓存结果
return order_result # 返回结果
规则:读操作可以重试,写操作必须有 request_id、事务状态或去重键。
📌 本章要点:幂等性是 Tool Use 的安全底线:读操作可重试,写操作必须去重。
🤔 思考一下:回顾你的项目:有哪些工具调用是写操作?加过幂等性键吗?
坑 4:错误信息对模型没有帮助
错误示例:返回 {"error": "failed"}
模型看到这个,除了"再试一次"什么都做不了。
正确示例:返回结构化错误信息
{
"error_type": "parameter_invalid",
"retryable": false,
"message": "城市名'北精'不存在,你是否想输入'北京'?",
"suggested_action": "ask_user_to_confirm"
}
结构化错误是工具层和 Agent 之间的协议。error_type、retryable、suggested_action 这些字段能让模型做出正确的下一步决策。
📌 本章要点:工具返回的错误信息要对模型有帮助:告诉它发生了什么、能不能重试、下一步该怎么做。
总结:读完这篇,你应该带走这几件事
- Function Calling 不是新能力,是 Token 预测的模式切换。工具描述的质量 > 模型的能力。
- 3 种模式逐步进阶:手动拼装(理解原理)→ Responses API(快速上手)→ Agent 循环(生产环境)。
- 工具描述 = 给模型的指令,不是 API 文档。写清触发条件、参数示例、边界约束。
- 幂等性是安全底线:读操作可重试,写操作必须去重。
- 结构化错误:让模型知道发生了什么、能不能重试、下一步该怎么做。
- DeepSeek V4 用户特别注意:幻觉率 94-96%,必须加校验层;
tool_choice="required"不稳定,代码层兜底。 - 这不仅仅是一个技术点:Forbes 6 月 25 日发文称"未来 AI 取决于 Agent 基础设施"。Tool Use 正在从技巧变成基础设施。
🤔 思考一下:你现在的项目里,有没有一个工具调用经常出问题?用今天学到的方法,你会怎么改工具描述?如果用的是 DeepSeek V4,你打算怎么加校验层?
思维导图
- Tool Use 实战:3 种模式
- 底层原理
- Function Calling = Token 预测的模式切换
- SFT + RL 两阶段训练
- 工具描述质量 > 模型能力
- 3 种调用模式
- 手动拼装:理解原理,4 步骤
- Responses API:一键封装(仅 OpenAI)
- Agent 循环:生产标准写法,通用兼容
- 4 个常见坑
- 工具描述像 API 文档 → 改成给模型的指令
- 调错工具/填错参数 → 严格 schema + 清晰描述
- 没有幂等性保护 → 写操作加去重键
- 错误信息没帮助 → 结构化错误协议
- 核心 Takeaways
- 底层原理
下一篇预告
P03. 上下文工程:不是堆 token,是精准投喂
工具能调用了,但模型的"记忆"怎么管?上下文窗口不是越大越好:塞太多会淹没关键信息,塞太少会丢失上下文。
这一篇讲:上下文的 4 层结构、压缩策略、按需加载。
🤔 思考一下:你的 Agent 在处理长任务时,有没有遇到过"前面的上下文被冲掉了"的问题?下一篇会给你答案。
更多推荐

所有评论(0)