系列文章导航:AI系列文章导航目录-持续更新中
第13课:工具使用(Tool Use)范式
📝 本文摘要:本文阐述Tool Use的完整概念层级(Tool Use→Function Calling/MCP/Skill等实现方式),详解工具描述规范(name/description/parameters JSON Schema)、工具选择策略(模型自动选择/强制调用/禁用)、工具结果处理(结构化返回→模型理解→下一步决策),对比不同模型工具调用能力差异,以及从Function Calling到MCP协议的标准化演进方向。
Function Calling是一种实现方式,Tool Use是更广泛的概念。理解Tool Use的完整范式,你才能理解MCP、Skill等上层技术的价值。
一、Tool Use到底是什么
1.1 从一个问题说起
大模型(LLM)本质上只能做一件事:根据输入的文字,生成输出的文字。
这意味着它有很多事情做不了:
LLM做不到的事情:
❌ 查询今天的天气(它不能上网)
❌ 查数据库里的订单(它没有数据库连接)
❌ 发一封邮件(它没有邮箱账号)
❌ 读取你电脑上的文件(它没有文件系统访问权限)
❌ 执行一段代码(它只能生成代码文本,不能运行)
❌ 调用任何外部API(它只是一个文本生成器)
LLM能做到的事情:
✅ 理解你的意图("我想查北京天气")
✅ 生成文本回复("北京今天可能是晴天"------但这是瞎猜的)
✅ 生成结构化数据(输出JSON格式的内容)
Tool Use(工具使用)就是解决这个问题的方案:让LLM能够"使用工具"来完成它本身做不到的事情。
1.2 一句话定义
Tool Use = 让LLM能够调用外部工具(API、数据库、代码执行器等)来完成任务
类比:
LLM本身 = 一个很聪明但没有手的大脑
Tool Use = 给这个大脑装上手,让它能操作外部世界
人类也是这样:
大脑想知道天气 → 手拿起手机打开天气App → 眼睛看到结果 → 大脑理解
LLM想知道天气 → 调用天气API → 获取返回数据 → LLM理解并回答用户
1.3 Tool Use的工作原理(核心!)
关键理解:LLM自己不执行工具,它只是"说"它想用什么工具。真正执行工具的是你的代码。
完整流程(以查天气为例):
┌─────────────────────────────────────────────────────────────┐
│ │
│ 第1步: 你告诉LLM "你有一个查天气的工具可以用" │
│ (通过API的tools参数传入工具描述) │
│ │
│ 第2步: 用户问 "北京今天天气怎么样?" │
│ │
│ 第3步: LLM思考后决定 → "我需要用查天气工具" │
│ LLM输出: {name: "get_weather", args: {city: "北京"}} │
│ 注意: LLM只是输出了这段JSON,它并没有真的去查天气! │
│ │
│ 第4步: 你的代码收到这个JSON │
│ → 解析出: 要调get_weather,参数是city="北京" │
│ → 你的代码真正去调用天气API │
│ → 得到结果: {temp: 28, condition: "晴"} │
│ │
│ 第5步: 你把工具执行结果返回给LLM │
│ "天气API返回: 28度,晴" │
│ │
│ 第6步: LLM基于这个真实数据,生成最终回答 │
│ "北京今天天气晴朗,气温28°C,适合出行。" │
│ │
└─────────────────────────────────────────────────────────────┘
整个过程中的角色分工:
LLM的职责: 理解意图 → 决定用哪个工具 → 生成参数 → 理解结果 → 生成回答
你的代码的职责: 提供工具列表 → 执行工具 → 返回结果
LLM = 大脑(决策者)
你的代码 = 手(执行者)
1.4 Tool Use vs Function Calling:到底什么关系?
你会在各种文章中看到这两个词混用,这里彻底讲清楚:
Tool Use(工具使用)= 一个广义的概念/范式
含义: "让LLM能使用外部工具"
不限定具体怎么实现
Function Calling = Tool Use的一种具体实现方式
含义: "通过API的tools参数定义工具,LLM输出结构化的函数调用JSON"
是OpenAI首先推出的具体API实现
关系:
Tool Use 是"做什么"(让LLM用工具)
Function Calling 是"怎么做"(通过API的tools参数+结构化JSON输出)
类比:
"出行" = Tool Use(广义概念)
"开车" = Function Calling(具体方式之一)
"骑车"、"坐地铁" = 其他实现方式
关于 "type": "function" 的常见误解
很多初学者看到OpenAI的工具定义格式后会产生一个误解:
python
# OpenAI的工具定义
tools = [
{
"type": "function", # ← 看到这个type,你可能会想:
"function": { ... } # "是不是还有type为mcp、code_interpreter等的?"
}
]
# 答案:不是!
# "type": "function" 目前是唯一的值!
# 不存在 "type": "mcp" 或 "type": "code_interpreter"
为什么只有 "function" 这一个type?
因为对LLM来说,所有工具最终都是以"函数"的形式呈现的:
不管工具来自哪里:
├── 你手动写的工具定义 → "type": "function"
├── MCP Server提供的工具 → 也被转换为 "type": "function"
├── Skill系统注册的工具 → 也被转换为 "type": "function"
└── 框架自动生成的工具 → 也被转换为 "type": "function"
LLM看到的永远是同一种格式!
它根本不知道也不关心工具是从哪来的。
类比:
你去餐厅点菜,菜单上写着"宫保鸡丁"。
你不关心这道菜的食材是从超市买的还是从农场直送的。
对你来说,它就是菜单上的一道菜。
LLM也一样,它只看到"这里有个get_weather工具可以用",
不关心这个工具是开发者手写的还是MCP Server提供的。
图示:工具来源不同,但LLM看到的格式完全一样
┌─────────────────────────────────────────────────────────┐
│ LLM (大模型) │
│ │
│ 它只认一种格式: │
│ tools = [{"type": "function", "function": {...}}] │
│ │
│ 不管工具从哪来,对我来说都一样 │
└────────────────────────┬────────────────────────────────┘
│
传入的都是同一种格式
│
┌─────────────┼─────────────┐
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌───▼────┐
│ 手动定义 │ │ MCP Server │ │ Skill │
│ 的工具 │ │ 提供的工具 │ │ 的工具 │
└─────────┘ └───────────┘ └────────┘
来源不同,但最终格式完全一样!
MCP解决的不是"LLM怎么调工具"的问题,
而是"工具怎么被发现和管理"的问题。
为什么要区分这两个概念? 因为Tool Use不只有Function Calling这一种实现:
Tool Use(工具使用)------ 广义概念
│
├── Function Calling / Tool Calling(最主流的实现方式)
│ 各家LLM厂商的API都支持
│ OpenAI叫 Function Calling
│ Anthropic叫 Tool Use(注意:Anthropic用了同一个词!容易混淆)
│ 本质一样: LLM输出结构化的工具调用JSON
│
├── Code Interpreter(代码解释器)
│ LLM自己写代码并执行
│ 不是调用预定义的函数,而是动态生成代码
│ 例: ChatGPT的Code Interpreter、Claude的Analysis Tool
│
├── Computer Use(电脑操作)
│ LLM直接操作电脑屏幕
│ 点击、打字、截图、滚动
│ 例: Claude的Computer Use、OpenAI的Operator
│
├── Web Browsing(网页浏览)
│ LLM自己上网搜索和浏览
│ 例: ChatGPT的Browse with Bing
│
└── MCP(标准化协议)
统一了工具暴露和调用的标准
让任何工具都能被任何Agent使用
层级关系:
Tool Use (概念层 - "LLM能用工具")
└─ Function Calling (实现层 - 各家API的具体实现)
└─ MCP (标准化层 - 统一协议,让工具可复用)
└─ Skill (能力层 - 工具+知识+提示的组合包)
1.5 为什么Tool Use是Agent的核心能力
没有Tool Use的LLM:
用户: "帮我查一下订单ORD001的状态"
LLM: "抱歉,我无法访问订单系统,请您自行查询。"
→ 只能聊天,不能做事
有Tool Use的LLM(= Agent):
用户: "帮我查一下订单ORD001的状态"
LLM: [调用query_order工具] → 获取结果
LLM: "您的订单ORD001已发货,预计明天送达。"
→ 能理解意图 + 能执行操作 + 能返回结果 = 真正的智能助手
所以:
LLM + Tool Use = Agent(能做事的智能体)
Tool Use是从"聊天机器人"进化为"智能助手"的关键一步
二、不同平台的Tool Use实现
不同的LLM厂商对Tool Use的API实现略有不同,但核心思路完全一致。下面分别讲解。
2.1 OpenAI: Function Calling
OpenAI是最早推出Function Calling的厂商(2023年6月),也是目前最成熟的实现。
2.1.1 工具定义格式
python
from openai import OpenAI
import json
client = OpenAI() # 需要设置OPENAI_API_KEY环境变量
# 工具定义:告诉LLM "你有哪些工具可以用"
# 每个工具需要: name(名称)、description(描述)、parameters(参数规范)
tools = [
{
"type": "function", # 目前只有function这一种type
"function": {
"name": "get_weather", # 工具名称(LLM用这个名字来调用)
"description": "获取指定城市的当前天气信息,包括温度、天气状况和湿度", # 描述越清晰,LLM选择越准确
"parameters": { # 参数定义,使用JSON Schema格式
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'、'广州'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"], # 枚举值,限定可选项
"description": "温度单位,celsius=摄氏度,fahrenheit=华氏度,默认摄氏度"
}
},
"required": ["city"], # 必填参数
"additionalProperties": False # 不允许额外参数
}
}
},
{
"type": "function",
"function": {
"name": "query_order",
"description": "根据订单编号查询订单详情,包括订单状态、商品列表和金额",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单编号,格式如ORD001、ORD002"
}
},
"required": ["order_id"],
"additionalProperties": False
}
}
}
]
2.1.2 完整的调用流程代码
python
# 实际的工具实现(这些是你自己写的真实函数)
def get_weather(city: str, unit: str = "celsius") -> dict:
"""模拟天气API(实际项目中这里会调用真实的天气API)"""
weather_data = {
"北京": {"temp": 28, "condition": "晴", "humidity": 45},
"上海": {"temp": 32, "condition": "多云", "humidity": 78},
"深圳": {"temp": 35, "condition": "雷阵雨", "humidity": 85},
}
data = weather_data.get(city, {"temp": 25, "condition": "未知", "humidity": 50})
if unit == "fahrenheit":
data["temp"] = data["temp"] * 9/5 + 32
return data
def query_order(order_id: str) -> dict:
"""模拟订单查询API"""
orders = {
"ORD001": {"status": "已发货", "items": ["手机壳", "充电器"], "total": 128.5},
"ORD002": {"status": "待发货", "items": ["耳机"], "total": 299.0},
}
return orders.get(order_id, {"status": "未找到", "items": [], "total": 0})
# 工具名称 → 函数的映射(方便根据名称找到对应函数)
tool_map = {
"get_weather": get_weather,
"query_order": query_order,
}
# 完整的Agent循环
def agent_chat(user_message: str) -> str:
messages = [
{"role": "system", "content": "你是一个智能助手,可以查询天气和订单信息。"},
{"role": "user", "content": user_message}
]
max_rounds = 5 # 最多循环5次,防止死循环
for _ in range(max_rounds):
# 第1步: 调用LLM,传入消息历史 + 工具列表
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto" # auto=让模型自己决定是否调工具
)
msg = response.choices[0].message
# 第2步: 判断LLM的响应类型
# 情况A: LLM直接回复文本(不需要调工具)
if msg.content and not msg.tool_calls:
return msg.content
# 情况B: LLM要求调用工具
if msg.tool_calls:
# 把LLM的tool_call消息加入历史
messages.append(msg)
# 逐个执行LLM要求的工具调用
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f" → LLM决定调用: {func_name}({func_args})")
# 执行工具(你的代码负责真正执行)
result = tool_map[func_name](**func_args)
print(f" ← 工具返回: {result}")
# 把工具结果加入messages,告诉LLM执行结果
messages.append({
"role": "tool", # 角色是"tool"
"tool_call_id": tool_call.id, # 对应哪个tool_call
"content": json.dumps(result, ensure_ascii=False)
})
# 继续循环,让LLM基于工具结果生成最终回答
return "抱歉,处理过程中遇到了问题。"
# 测试
print(agent_chat("北京今天天气怎么样?"))
# 输出:
# → LLM决定调用: get_weather({'city': '北京'})
# ← 工具返回: {'temp': 28, 'condition': '晴', 'humidity': 45}
# "北京今天天气晴朗,温度28°C,湿度45%。适合户外活动!"
print(agent_chat("我的订单ORD001到哪了?"))
# 输出:
# → LLM决定调用: query_order({'order_id': 'ORD001'})
# ← 工具返回: {'status': '已发货', 'items': ['手机壳', '充电器'], 'total': 128.5}
# "您的订单ORD001已发货,包含手机壳和充电器,总金额128.5元。"
print(agent_chat("你好"))
# 输出:
# (不调用任何工具,直接回复)
# "你好!有什么我可以帮你的吗?"
2.1.3 tool_choice参数详解
python
# tool_choice 控制LLM是否/如何使用工具
# "auto"(默认): LLM自己判断要不要用工具
# 用户问"你好" → 不调工具,直接回复
# 用户问"北京天气" → 调get_weather工具
tool_choice="auto"
# "none": 禁止使用工具,强制纯文本回复
# 即使用户问"北京天气",也不会调工具
tool_choice="none"
# "required": 强制必须调用工具(至少调一个)
# 即使用户只是说"你好",也会被迫选一个工具调用
tool_choice="required"
# 指定工具: 强制调用某个特定工具
# 不管用户说什么,都会调get_weather
tool_choice={"type": "function", "function": {"name": "get_weather"}}
2.1.4 并行工具调用
python
# 当用户的问题需要多个工具时,LLM可以一次性输出多个tool_calls
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "北京和上海今天天气怎么样?"}],
tools=tools
)
msg = response.choices[0].message
# msg.tool_calls 会包含两个调用:
# tool_calls[0]: get_weather({"city": "北京"})
# tool_calls[1]: get_weather({"city": "上海"})
# 你需要都执行,然后把两个结果都返回给LLM
for tool_call in msg.tool_calls:
# 执行每个工具调用...
pass
2.2 Anthropic: Tool Use
Anthropic(Claude的开发商)对Tool Use的实现和OpenAI思路一样,但API格式有明显差异。下面给出完整的独立案例。
2.2.1 工具定义格式
python
import anthropic
import json
client = anthropic.Anthropic() # 需要设置ANTHROPIC_API_KEY环境变量
# Anthropic的工具定义格式
# 注意和OpenAI的区别:
# 1. 没有外层的 "type": "function" 包装
# 2. 参数定义的key叫 input_schema(不是parameters)
tools = [
{
"name": "get_weather", # 工具名称
"description": "获取指定城市的当前天气信息,包括温度、天气状况和湿度", # 描述
"input_schema": { # ← 注意!叫input_schema,不是parameters
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'、'广州'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认摄氏度"
}
},
"required": ["city"]
}
},
{
"name": "query_order",
"description": "根据订单编号查询订单详情,包括订单状态、商品列表和金额",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单编号,格式如ORD001、ORD002"
}
},
"required": ["order_id"]
}
}
]
2.2.2 完整的调用流程代码
python
# 实际的工具实现(和OpenAI的例子一样)
def get_weather(city: str, unit: str = "celsius") -> dict:
"""模拟天气API"""
weather_data = {
"北京": {"temp": 28, "condition": "晴", "humidity": 45},
"上海": {"temp": 32, "condition": "多云", "humidity": 78},
}
data = weather_data.get(city, {"temp": 25, "condition": "未知", "humidity": 50})
if unit == "fahrenheit":
data["temp"] = data["temp"] * 9/5 + 32
return data
def query_order(order_id: str) -> dict:
"""模拟订单查询API"""
orders = {
"ORD001": {"status": "已发货", "items": ["手机壳", "充电器"], "total": 128.5},
"ORD002": {"status": "待发货", "items": ["耳机"], "total": 299.0},
}
return orders.get(order_id, {"status": "未找到", "items": [], "total": 0})
tool_map = {"get_weather": get_weather, "query_order": query_order}
# 完整的Agent循环
def agent_chat(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
max_rounds = 5
for _ in range(max_rounds):
# 第1步: 调用Claude API
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=messages,
tools=tools,
tool_choice={"type": "auto"} # auto=模型自己决定
)
# 第2步: 判断响应类型
# Anthropic的响应是一个content数组,可能包含多种block
# 情况A: stop_reason为"end_turn",表示模型直接回复(不调工具)
if response.stop_reason == "end_turn":
# 从content中提取文本
text_parts = [block.text for block in response.content if block.type == "text"]
return "\n".join(text_parts)
# 情况B: stop_reason为"tool_use",表示模型要调工具
if response.stop_reason == "tool_use":
# 把模型的响应加入消息历史(role为assistant)
messages.append({"role": "assistant", "content": response.content})
# 收集所有工具调用的结果
tool_results = []
for block in response.content:
if block.type == "tool_use":
func_name = block.name
func_args = block.input # 注意:Anthropic直接给dict,不需要json.loads
print(f" → Claude决定调用: {func_name}({func_args})")
# 执行工具
result = tool_map[func_name](**func_args)
print(f" ← 工具返回: {result}")
# 构造tool_result
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # 必须对应tool_use的id
"content": json.dumps(result, ensure_ascii=False)
})
# 把工具结果以user消息的形式返回
# 注意!这是和OpenAI最大的区别:
# OpenAI: 工具结果用 role="tool" 的独立消息
# Anthropic: 工具结果放在 role="user" 消息的content数组中
messages.append({"role": "user", "content": tool_results})
# 继续循环,让Claude基于工具结果生成最终回答
return "处理超时"
# 测试
print(agent_chat("北京今天天气怎么样?"))
# 输出:
# → Claude决定调用: get_weather({'city': '北京'})
# ← 工具返回: {'temp': 28, 'condition': '晴', 'humidity': 45}
# "北京今天天气晴朗,气温28°C,湿度45%。"
print(agent_chat("你好"))
# 输出:
# (不调用任何工具,直接回复)
# "你好!有什么我可以帮你的吗?"
2.2.3 tool_choice参数
python
# Anthropic的tool_choice格式和OpenAI不同
# 自动模式:模型自己决定(默认)
tool_choice={"type": "auto"}
# 强制使用工具(必须调至少一个,类似OpenAI的"required")
tool_choice={"type": "any"}
# 强制调用指定工具
tool_choice={"type": "tool", "name": "get_weather"}
# 注意:Anthropic没有"none"选项
# 如果不想让模型调工具,直接不传tools参数即可
2.3 OpenAI vs Anthropic 完整对比
下面用同一个任务对比两家的实现差异,帮你一目了然:
2.3.1 工具定义对比
python
# ═══════════════════════════════════════════════════
# OpenAI 的工具定义
# ═══════════════════════════════════════════════════
openai_tools = [
{
"type": "function", # ← 有外层type包装
"function": { # ← 嵌套在function key下
"name": "get_weather",
"description": "获取城市天气",
"parameters": { # ← 叫parameters
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"]
}
}
}
]
# ═══════════════════════════════════════════════════
# Anthropic 的工具定义
# ═══════════════════════════════════════════════════
anthropic_tools = [
{
# 没有外层type包装,直接就是工具定义
"name": "get_weather",
"description": "获取城市天气",
"input_schema": { # ← 叫input_schema
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"]
}
}
]
2.3.2 LLM响应格式对比
python
# ═══════════════════════════════════════════════════
# OpenAI 的响应(当LLM决定调工具时)
# ═══════════════════════════════════════════════════
# response.choices[0].message 的结构:
{
"role": "assistant",
"content": None, # 调工具时content为空
"tool_calls": [ # ← 工具调用在tool_calls数组中
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"北京\"}" # ← 是JSON字符串!需要json.loads
}
}
]
}
# ═══════════════════════════════════════════════════
# Anthropic 的响应(当LLM决定调工具时)
# ═══════════════════════════════════════════════════
# response.content 的结构:
[
{
"type": "text", # 可能先有一段思考文本
"text": "让我查一下北京的天气。"
},
{
"type": "tool_use", # ← 工具调用是content中的一个block
"id": "toolu_abc123",
"name": "get_weather",
"input": {"city": "北京"} # ← 直接是dict!不需要json.loads
}
]
2.3.3 返回工具结果的方式对比
python
# ═══════════════════════════════════════════════════
# OpenAI: 用独立的 role="tool" 消息返回结果
# ═══════════════════════════════════════════════════
messages.append({
"role": "tool", # ← 专门的tool角色
"tool_call_id": "call_abc123", # 对应哪个tool_call
"content": "{\"temp\": 28, \"condition\": \"晴\"}" # 结果
})
# ═══════════════════════════════════════════════════
# Anthropic: 放在 role="user" 消息的content数组中
# ═══════════════════════════════════════════════════
messages.append({
"role": "user", # ← 注意!是user角色
"content": [ # content是数组
{
"type": "tool_result", # 类型是tool_result
"tool_use_id": "toolu_abc123", # 对应哪个tool_use
"content": "{\"temp\": 28, \"condition\": \"晴\"}" # 结果
}
]
})
2.3.4 总结对比表
┌──────────────────┬─────────────────────────┬─────────────────────────────┐
│ 维度 │ OpenAI │ Anthropic │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 工具定义外层 │ {"type":"function", │ 直接定义,无外层包装 │
│ │ "function":{...}} │ {"name":..., ...} │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 参数定义key │ parameters │ input_schema │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 工具调用输出位置 │ message.tool_calls数组 │ content中的tool_use block │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 参数格式 │ JSON字符串 │ 直接是dict对象 │
│ │ (需要json.loads解析) │ (不需要额外解析) │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 工具结果角色 │ role: "tool" │ role: "user" │
│ │ (专门的tool角色) │ (放在user消息的content中) │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ tool_choice选项 │ auto / none / │ auto / any / │
│ │ required / 指定工具 │ 指定工具(无none) │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 并行调用 │ 多个tool_calls │ 多个tool_use blocks │
├──────────────────┼─────────────────────────┼─────────────────────────────┤
│ 判断是否调工具 │ msg.tool_calls存在 │ stop_reason=="tool_use" │
└──────────────────┴─────────────────────────┴─────────────────────────────┘
2.3.5 核心结论
虽然格式不同,但本质完全一样:
1. 你定义工具列表 → 传给LLM
2. LLM决定调哪个工具 → 输出结构化的调用信息
3. 你执行工具 → 把结果返回给LLM
4. LLM基于结果 → 生成最终回答
这个流程不管是OpenAI还是Anthropic都一样。
差异只在API格式的细节上,不影响你理解Tool Use的核心思想。
如果你用框架(如LangChain、LlamaIndex),
框架会帮你屏蔽这些格式差异,你写一份代码就能同时支持两家。
三、高级Tool Use模式
3.1 动态工具注册
python
class ToolRegistry:
"""动态工具注册器------运行时增减可用工具"""
def __init__(self):
self.tools = {}
self.schemas = []
def register(self, func, schema):
"""注册工具"""
self.tools[schema["function"]["name"]] = func
self.schemas.append(schema)
def unregister(self, name):
"""注销工具"""
self.tools.pop(name, None)
self.schemas = [s for s in self.schemas if s["function"]["name"] != name]
def get_schemas(self):
return self.schemas
def execute(self, name, args):
return self.tools[name](**args)
# 使用
registry = ToolRegistry()
registry.register(
func=get_weather,
schema={
"type": "function",
"function": {
"name": "get_weather",
"description": "获取城市天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"}
},
"required": ["city"],
"additionalProperties": False
}
}
}
)
# 根据用户权限动态加载工具
if user.has_permission("order_query"):
registry.register(query_order_func, query_order_schema)
if user.has_permission("refund"):
registry.register(create_refund_func, refund_schema)
3.2 工具组合(Tool Composition)
python
# 一个工具的输出作为另一个工具的输入
# 让Agent自己决定调用顺序
# 例: 查询订单 → 根据结果决定是否退款
tools = [
{
"type": "function",
"function": {
"name": "query_order",
"description": "查询订单详情,包括状态、金额、商品等",
"parameters": {...}
}
},
{
"type": "function",
"function": {
"name": "create_refund",
"description": "为指定订单创建退款申请。只有状态为'已签收'的订单才能退款",
"parameters": {...}
}
}
]
# Agent会自动: 先query_order → 检查状态 → 再create_refund(如果可以)
3.3 人工确认(Human-in-the-loop,人工介入循环)
python
# 危险操作需要人工确认
DANGEROUS_TOOLS = {"create_refund", "delete_account", "send_email"}
def agent_with_confirmation(user_message: str) -> str:
messages = [{"role": "user", "content": user_message}]
for _ in range(5):
response = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=tools
)
msg = response.choices[0].message
if not msg.tool_calls:
return msg.content
messages.append(msg)
for tc in msg.tool_calls:
if tc.function.name in DANGEROUS_TOOLS:
# 请求人工确认
args = json.loads(tc.function.arguments)
confirm = input(f"⚠️ 即将执行 {tc.function.name}({args}),确认?(y/n): ")
if confirm.lower() != 'y':
result = {"status": "cancelled", "message": "用户取消操作"}
else:
result = tool_map[tc.function.name](**args)
else:
result = tool_map[tc.function.name](**args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "处理超时"
四、Code Interpreter(代码解释器)------特殊的Tool Use
4.1 什么是Code Interpreter
让Agent能执行代码,是最强大的工具之一
普通工具: 预定义的API,模型只能选择调用
Code Interpreter: 模型自己写代码来解决问题
适用场景:
- 数学计算(避免模型算错)
- 数据分析(处理CSV/Excel)
- 图表生成
- 文件格式转换
4.2 实现一个简易Code Interpreter
python
import subprocess
import tempfile
import os
def execute_python(code: str) -> dict:
"""安全地执行Python代码"""
try:
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
f.flush()
temp_path = f.name
result = subprocess.run(
["python3", temp_path],
capture_output=True,
text=True,
timeout=30, # 30秒超时
)
os.unlink(temp_path)
return {
"stdout": result.stdout[:5000], # 限制输出长度
"stderr": result.stderr[:5000],
"returncode": result.returncode
}
except subprocess.TimeoutExpired:
return {"error": "执行超时(30秒)"}
except Exception as e:
return {"error": str(e)}
# 作为工具注册
code_interpreter_tool = {
"type": "function",
"function": {
"name": "execute_python",
"description": "执行Python代码并返回结果。用于数学计算、数据分析等需要精确计算的场景。",
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "要执行的Python代码"
}
},
"required": ["code"],
"additionalProperties": False
}
}
}
五、Tool Use的设计哲学
5.1 给模型合适的工具
❌ 给模型一个"do_everything"工具
✅ 给模型多个专一的工具
原因: 工具粒度越细,模型选择越准确
类比: 给员工一个"万能遥控器"不如给他每个设备的遥控器
5.2 工具描述即API文档
工具的description就是给模型看的API文档
写好工具描述 = 写好API文档
好的工具描述要素:
1. 功能说明: 这个工具做什么
2. 使用时机: 什么时候应该用
3. 参数说明: 每个参数的含义和格式
4. 返回说明: 返回什么格式的数据
5. 注意事项: 使用限制和边界条件
5.3 工具数量与质量的权衡
工具数量:
< 5个: 模型选择很准
5-20个: 偶尔选错
> 20个: 经常选错,需要分组或检索
优化策略:
1. 按场景分组: 只加载当前场景需要的工具
2. 两步选择: 先选类别,再选具体工具
3. 工具检索: 用嵌入相似度选择最相关的工具
📝 作业
作业1:实现一个带人工确认的订单管理Agent
要求:
- 有3个工具:查询订单、创建退款、发送通知
- 退款操作需要人工确认
- 完整的Agent循环
参考答案:
python
from openai import OpenAI
import json
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
# 模拟数据
ORDERS = {
"ORD001": {"status": "已签收", "amount": 299.0, "item": "蓝牙耳机"},
"ORD002": {"status": "配送中", "amount": 89.5, "item": "手机壳"},
"ORD003": {"status": "已签收", "amount": 1299.0, "item": "机械键盘"},
}
def query_order(order_id: str) -> dict:
return ORDERS.get(order_id, {"status": "未找到", "amount": 0, "item": ""})
def create_refund(order_id: str, reason: str) -> dict:
order = ORDERS.get(order_id)
if not order:
return {"success": False, "message": "订单不存在"}
if order["status"] != "已签收":
return {"success": False, "message": f"订单状态为{order['status']},无法退款"}
return {"success": True, "message": f"退款申请已创建,金额{order['amount']}元"}
def send_notification(order_id: str, message: str) -> dict:
return {"success": True, "message": f"已发送通知到订单{order_id}关联的用户"}
tool_map = {"query_order": query_order, "create_refund": create_refund, "send_notification": send_notification}
DANGEROUS = {"create_refund", "send_notification"}
tools = [
{
"type": "function",
"function": {
"name": "query_order",
"description": "查询订单详情",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string", "description": "订单编号"}},
"required": ["order_id"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "create_refund",
"description": "为已签收的订单创建退款申请",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单编号"},
"reason": {"type": "string", "description": "退款原因"}
},
"required": ["order_id", "reason"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "send_notification",
"description": "向用户发送通知",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "关联订单编号"},
"message": {"type": "string", "description": "通知内容"}
},
"required": ["order_id", "message"],
"additionalProperties": False
}
}
}
]
def order_agent(user_message: str) -> str:
messages = [
{"role": "system", "content": "你是订单管理助手。查询订单不需要确认,但退款和发通知需要用户确认。"},
{"role": "user", "content": user_message}
]
for _ in range(5):
response = client.chat.completions.create(
model="qwen2.5:7b", messages=messages, tools=tools, tool_choice="auto"
)
msg = response.choices[0].message
if msg.content and not msg.tool_calls:
return msg.content
if msg.tool_calls:
messages.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
if tc.function.name in DANGEROUS:
print(f"⚠️ Agent请求执行: {tc.function.name}({args})")
confirm = input("确认执行?(y/n): ")
if confirm.lower() != 'y':
result = {"status": "cancelled", "message": "用户取消了操作"}
else:
result = tool_map[tc.function.name](**args)
else:
result = tool_map[tc.function.name](**args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "处理超时"
下一篇文章见:AI系列文章导航目录-持续更新中