第01章 工具调用的本质:LLM如何与外部世界交互
第01章 工具调用的本质:LLM如何与外部世界交互
“语言模型是大脑,工具是手脚。没有手脚的大脑,只能思考,无法行动。” —— AI系统设计师
在深入工具Schema设计、并行调用、安全防护之前,我们需要彻底理解一件事:工具调用在底层究竟是如何工作的?
很多开发者把工具调用当成"黑魔法"——传入一些函数定义,LLM自动就知道要调用哪个函数。理解背后的机制,是写好工具的前提。
1.1 LLM的根本限制:文本进,文本出
# LLM的本质:一个预测下一个token的函数
# 不使用工具时,LLM只能生成文本
conversation = [
{"role": "user", "content": "今天北京的天气怎么样?"}
]
# LLM的回答:
# "我无法访问实时数据,所以我不知道今天北京的具体天气。
# 北京四月份通常...(一堆猜测)"
# 问题:LLM被训练于过去的数据,没有实时访问能力
# 解决:给LLM提供"工具",让它能调用外部系统
LLM有三个内置限制:
- 训练数据截止:不知道训练后发生的事
- 无法执行操作:不能发邮件、不能写文件
- 无状态:每次对话独立,不记得上次的结果
工具调用解决了所有三个限制。
1.2 工具调用的完整流程
# 工具调用的完整生命周期
import json
# ===== 第1步:开发者定义工具Schema =====
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的实时天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,使用中文,例如:北京、上海"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认 celsius(摄氏度)"
}
},
"required": ["city"],
"additionalProperties": False, # 禁止未定义的额外参数
}
}
}
]
# ===== 第2步:用户消息 + 工具定义发送给LLM =====
messages = [
{"role": "user", "content": "北京今天天气如何?用摄氏度告诉我"}
]
# 发送给 OpenAI API:
# response = client.chat.completions.create(
# model="gpt-4o-mini",
# messages=messages,
# tools=tools,
# tool_choice="auto", # 让LLM自己决定是否用工具
# )
# ===== 第3步:LLM返回工具调用请求(不是回答!)=====
# LLM 返回的内容(模拟):
mock_llm_response = {
"role": "assistant",
"content": None, # 没有文字内容,只有工具调用
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": '{"city": "北京", "unit": "celsius"}'
# 注意:arguments 是字符串!需要 json.loads() 解析
}
}
]
}
# ===== 第4步:开发者执行工具函数 =====
def get_weather(city: str, unit: str = "celsius") -> dict:
"""真实实现:调用天气API"""
# 这里调用真实的天气API(如 OpenWeatherMap)
return {
"city": city,
"temperature": 22,
"unit": unit,
"condition": "晴天",
"humidity": "45%",
"wind": "东南风 2级",
}
tool_call = mock_llm_response["tool_calls"][0]
fn_name = tool_call["function"]["name"]
fn_args = json.loads(tool_call["function"]["arguments"])
result = get_weather(**fn_args)
# ===== 第5步:工具结果发回给LLM =====
messages.append(mock_llm_response) # 添加LLM的工具调用请求
messages.append({
"role": "tool",
"tool_call_id": "call_abc123", # 必须匹配工具调用的ID
"content": json.dumps(result, ensure_ascii=False),
})
# ===== 第6步:LLM生成最终回答 =====
# LLM 现在知道天气数据,会生成自然语言回答:
# "北京今天天气晴朗,气温22°C,相对湿度45%,东南风2级。
# 今天非常适合户外活动!"
关键认知:LLM本身从不执行工具。工具调用是LLM告诉你"我想调用这个函数,参数是这些"——实际执行完全由你的代码负责。
1.3 Tool Choice:控制LLM的工具使用策略
# tool_choice 参数的三种模式
from openai import AsyncOpenAI
async def demo_tool_choice():
client = AsyncOpenAI()
# 模式1:auto(默认)
# LLM自己决定是否用工具
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "你好"}],
tools=tools,
tool_choice="auto", # LLM判断"你好"不需要天气工具,直接文字回答
)
# 模式2:none
# 强制不使用工具(即使工具可用)
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "北京天气怎样"}],
tools=tools,
tool_choice="none", # 强制文字回答,LLM只能说"我不知道实时数据"
)
# 模式3:required
# 强制使用工具(LLM必须调用至少一个工具)
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "随便告诉我什么城市的天气"}],
tools=tools,
tool_choice="required", # LLM必须调用工具
)
# 模式4:指定工具
# 强制调用特定工具
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "上海天气"}],
tools=tools,
tool_choice={"type": "function", "function": {"name": "get_weather"}},
)
# 实用建议:
# - 对话型Agent → auto(让LLM决定)
# - 工作流型Agent → required 或指定工具(确保执行路径)
# - 调试时 → required(强制LLM说明它要做什么)
1.4 LLM如何选择工具:内部机制
# 理解LLM的工具选择逻辑
# LLM通过以下信号决定调用哪个工具:
TOOL_SELECTION_SIGNALS = {
"工具名称(最重要)": {
"好名称": "get_real_time_weather", # 清楚说明是实时的
"差名称": "tool_1", # 无意义
"差名称2": "fetch", # 太通用
},
"工具描述(非常重要)": {
"好描述": "获取指定城市的实时天气数据,包括温度、湿度、风速。适用于'今天/现在/当前'天气查询",
"差描述": "天气工具",
"关键点": "描述中包含触发场景(什么时候该用这个工具)",
},
"参数描述(重要)": {
"好描述": "城市名称,使用完整中文城市名,如'北京'而非'BJ'",
"差描述": "城市",
"关键点": "告诉LLM参数的格式、范围、示例",
},
"参数的required设置": {
"必须": "如果没有某个参数工具就无法工作,标记为required",
"可选": "有合理默认值的参数标记为可选,并在描述中说明默认行为",
},
}
# 实验:同一任务,不同描述,LLM的选择差异巨大
SAME_TOOLS_DIFFERENT_DESCRIPTIONS = {
"差版本": {
"name": "tool1",
"description": "获取数据",
"parameters": {"query": {"type": "string"}}
},
"好版本": {
"name": "search_web_for_current_info",
"description": "在互联网上搜索最新信息。适用于:最新新闻、实时数据、最新技术进展、当前事件。不适用于:数学计算、历史知识(使用其他工具)",
"parameters": {
"query": {
"type": "string",
"description": "搜索关键词。使用简洁精确的关键词,不要用完整句子"
}
}
}
}
1.5 与Anthropic Claude的工具调用对比
# OpenAI vs Claude 的工具调用API差异
# OpenAI 格式(本书主要使用)
openai_request = {
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "北京天气"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取天气",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"tool_choice": "auto",
}
# Claude 格式(本质相同,语法略有不同)
claude_request = {
"model": "claude-3-5-sonnet-20241022",
"messages": [{"role": "user", "content": "北京天气"}],
"tools": [{
"name": "get_weather", # 没有 "type": "function" 包装
"description": "获取天气",
"input_schema": { # 叫 input_schema,不叫 parameters
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}],
}
# Claude的工具调用结果:
# response.content[0].type == "tool_use"
# response.content[0].name == "get_weather"
# response.content[0].input == {"city": "北京"}
# 建议:使用统一封装层,支持多模型
def create_tool_schema(name: str, description: str, parameters: dict, provider: str = "openai") -> dict:
"""创建跨模型兼容的工具Schema"""
if provider == "openai":
return {
"type": "function",
"function": {"name": name, "description": description, "parameters": parameters}
}
elif provider == "anthropic":
return {
"name": name,
"description": description,
"input_schema": parameters
}
raise ValueError(f"未知的provider: {provider}")
本章小结
五个核心认知:
-
工具调用是LLM与外部世界的唯一接口:LLM本身是纯文本系统,工具调用是在文本协议上建立的"外部能力接入层"
-
LLM不执行工具,只描述调用意图:
tool_calls是LLM的"指令",执行由你的代码完成——这个分离很重要,意味着你完全控制工具的执行逻辑 -
工具选择质量取决于Schema设计:工具名称、描述、参数说明是LLM选择工具的唯一依据;Schema写得好,工具调用准确率从70%提升到90%以上
-
tool_call_id是工具调用和结果的绑定键:多工具并行调用时,LLM通过这个ID知道哪个结果对应哪个调用——一定不能搞错 -
OpenAI和Claude的工具调用语法不同,但概念相同:学好一种,理解原理后切换到另一种只是API语法问题;构建封装层让代码跨模型复用
核心行动:
# 动手验证:用Python实现一个完整的工具调用循环
# 要求:
# 1. 定义两个工具(天气 + 计算器)
# 2. 打印出LLM返回的原始 tool_calls 内容
# 3. 手动解析 arguments(json.loads)
# 4. 执行工具函数
# 5. 把结果加回 messages 然后得到最终回答
#
# 不用框架,就用原始 OpenAI Python SDK
# 这是理解工具调用最好的方式
本章提示词模板
模板一:理解工具调用流程
我是一个Python开发者,了解基本的API调用,但是第一次接触LLM工具调用。
请用最简单的方式解释:
1. 为什么需要工具调用?直接让LLM回答不行吗?
2. LLM工具调用的完整数据流是什么?(用伪代码+流程说明)
3. "LLM不执行工具,只描述调用意图"这句话是什么意思?
4. tool_call_id 的作用是什么?
5. 给我一个最小的、能跑通的工具调用示例(Python,不超过50行)
模板二:从零设计第一个工具
我想给我的AI助手添加一个工具,用途是:[描述你要做的事]
请帮我:
1. 设计这个工具的JSON Schema(符合OpenAI格式)
2. 写出对应的Python函数
3. 分析这个工具的Schema有哪些可以改进的地方(让LLM更容易理解)
4. 这个工具可能被LLM错误调用的场景是什么?如何在描述中预防?
5. 如果这个工具可能失败(网络超时/参数错误),应该返回什么格式的错误信息?