上周接了个需求,做一个能查天气、查数据库、还能发邮件的 AI 助手。一开始想着用 LangChain 套一层,后来发现 Claude 原生的 Tool Use(也叫 Function Calling)已经很成熟了,根本不需要额外框架。但官方文档写得有点绕,我踩了不少坑才把整条链路跑通。把摸索出来的东西全写下来,让你少走弯路。
Claude Tool Use 是 Anthropic 提供的原生函数调用能力,允许你在对话中定义工具(函数),Claude 会自动判断何时调用哪个工具、提取参数,你执行函数后把结果返回给它,它再生成最终回答。目前 Claude Sonnet 4.6 和 Opus 4.6 都支持,Sonnet 性价比最高,Opus 复杂推理更强。
先说结论
| 维度 | 说明 |
|---|---|
| 支持模型 | Claude Opus 4.6、Sonnet 4.6、Haiku 4.6 |
| 协议格式 | Anthropic 原生格式 / OpenAI 兼容格式均可 |
| 核心流程 | 定义工具 → 发送请求 → Claude 返回工具调用 → 你执行函数 → 结果回传 → Claude 生成回答 |
| 并行调用 | 支持,一次可调用多个工具 |
| 嵌套调用 | 支持,工具结果可触发新一轮工具调用 |
| 踩坑重灾区 | tool_result 的 content 必须是字符串、工具描述写得烂会导致调用率暴跌 |
核心流程一图看懂
sequenceDiagram
participant U as 你的代码
participant A as Claude API
participant T as 本地工具函数
U->>A: 发送消息 + 工具定义
A->>U: 返回 tool_use(要调用哪个工具、参数是什么)
U->>T: 执行本地函数
T->>U: 返回执行结果
U->>A: 把 tool_result 回传
A->>U: Claude 生成最终回答
这个流程是整个 Tool Use 的骨架,后面的代码全是围绕这个转的。
环境准备
pip install openai httpx
这里用的是 OpenAI SDK。因为很多聚合平台都兼容 OpenAI 协议,Claude 的 Tool Use 在 OpenAI 兼容模式下也能正常工作,切换模型零成本。
方案一:基础 Tool Use(单工具调用)
先来最简单的场景——让 Claude 调用一个查天气的函数。
import json
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://api.ofox.ai/v1" # 聚合接口,一个 Key 调 Claude/GPT/Gemini
)
# 1. 定义工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气信息,包括温度、湿度、天气状况",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海、深圳"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认摄氏度"
}
},
"required": ["city"]
}
}
}
]
# 2. 你的本地函数(实际项目中这里调真实 API)
def get_weather(city: str, unit: str = "celsius") -> dict:
# 模拟数据,实际替换成真实天气 API
fake_data = {
"北京": {"temp": 22, "humidity": 45, "condition": "晴"},
"上海": {"temp": 26, "humidity": 72, "condition": "多云"},
"深圳": {"temp": 31, "humidity": 80, "condition": "雷阵雨"},
}
data = fake_data.get(city, {"temp": 20, "humidity": 50, "condition": "未知"})
if unit == "fahrenheit":
data["temp"] = data["temp"] * 9 / 5 + 32
data["city"] = city
data["unit"] = unit
return data
# 3. 发送请求
messages = [
{"role": "user", "content": "深圳今天天气怎么样?适合跑步吗?"}
]
response = client.chat.completions.create(
model="claude-sonnet-4.6",
messages=messages,
tools=tools,
tool_choice="auto" # 让 Claude 自己决定要不要调工具
)
# 4. 处理工具调用
assistant_message = response.choices[0].message
if assistant_message.tool_calls:
# Claude 决定调用工具
messages.append(assistant_message) # 先把 assistant 消息加进去
for tool_call in assistant_message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"Claude 要调用: {func_name}({func_args})")
# 执行本地函数
if func_name == "get_weather":
result = get_weather(**func_args)
# 把结果回传(注意:content 必须是字符串!)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
# 5. 让 Claude 根据工具结果生成最终回答
final_response = client.chat.completions.create(
model="claude-sonnet-4.6",
messages=messages,
tools=tools
)
print(final_response.choices[0].message.content)
else:
# Claude 觉得不需要调工具,直接回答了
print(assistant_message.content)
实测输出:
Claude 要调用: get_weather({'city': '深圳'})
深圳今天31°C,湿度80%,有雷阵雨。不太建议户外跑步哦,
湿度太高容易中暑,雷阵雨也不安全。建议等晚上凉快点再去,
或者去室内跑步机上练练。
Claude 不是机械地把数据复述一遍,它会结合天气数据给出建议。这就是 Tool Use 比硬编码模板强的地方。
方案二:多工具 + 自动循环调用
真实项目很少只有一个工具。下面这个例子定义了三个工具,并且实现了自动循环——Claude 可能调完一个工具还想调另一个,代码自动处理。
import json
from openai import OpenAI
client = OpenAI(
api_key="your-api-key",
base_url="https://api.ofox.ai/v1"
)
# 定义多个工具
tools = [
{
"type": "function",
"function": {
"name": "search_products",
"description": "根据关键词搜索商品,返回商品列表(名称、价格、库存)",
"parameters": {
"type": "object",
"properties": {
"keyword": {"type": "string", "description": "搜索关键词"},
"max_results": {"type": "integer", "description": "最多返回几条,默认5"}
},
"required": ["keyword"]
}
}
},
{
"type": "function",
"function": {
"name": "get_product_reviews",
"description": "获取指定商品ID的用户评价摘要",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string", "description": "商品ID"}
},
"required": ["product_id"]
}
}
},
{
"type": "function",
"function": {
"name": "create_order",
"description": "创建订单。注意:仅在用户明确表示要购买时才调用",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"quantity": {"type": "integer", "description": "购买数量,默认1"}
},
"required": ["product_id"]
}
}
}
]
# 模拟业务函数
def search_products(keyword, max_results=5):
return [
{"id": "P001", "name": f"机械键盘-{keyword}款", "price": 359, "stock": 42},
{"id": "P002", "name": f"薄膜键盘-{keyword}入门", "price": 89, "stock": 156},
]
def get_product_reviews(product_id):
reviews = {
"P001": {"score": 4.7, "count": 2341, "summary": "手感好,Cherry轴,就是有点吵"},
"P002": {"score": 4.1, "count": 892, "summary": "便宜够用,适合办公"},
}
return reviews.get(product_id, {"score": 0, "summary": "暂无评价"})
def create_order(product_id, quantity=1):
return {"order_id": "ORD20260329001", "status": "created", "product_id": product_id, "quantity": quantity}
# 函数路由
FUNC_MAP = {
"search_products": search_products,
"get_product_reviews": get_product_reviews,
"create_order": create_order,
}
def chat_with_tools(user_input: str):
messages = [
{"role": "system", "content": "你是一个电商购物助手,帮用户搜索商品、查看评价、下单购买。"},
{"role": "user", "content": user_input}
]
max_rounds = 5 # 防止死循环
for round_num in range(max_rounds):
response = client.chat.completions.create(
model="claude-sonnet-4.6",
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
# 没有工具调用,说明 Claude 准备好回答了
if not msg.tool_calls:
return msg.content
# 有工具调用,执行并回传
messages.append(msg)
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f" [Round {round_num + 1}] 调用 {func_name}({func_args})")
result = FUNC_MAP[func_name](**func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
return "工具调用轮次超限,请简化问题"
# 测试
answer = chat_with_tools("我想买个键盘打代码用,帮我看看有啥推荐的,评价好的那种")
print(answer)
跑起来后,Claude 会先调 search_products,看到结果后自动调 get_product_reviews 查评价,再综合两次结果给出推荐。两轮工具调用,全自动。
踩坑记录
这部分是我花时间最多的地方。
坑 1:tool_result 的 content 不是字符串直接报错
content 字段必须是字符串,不能直接传 dict。
# ❌ 错误写法
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": {"temp": 22} # 这会报 422 错误
})
# ✅ 正确写法
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps({"temp": 22}, ensure_ascii=False)
})
坑 2:工具描述写太简单,Claude 不调或者乱调
这个坑比较隐蔽。我一开始给 get_weather 的 description 写的是 "获取天气",三个字。结果 Claude 经常不调这个工具,直接瞎编一个天气回答。
description 要写清楚三件事:这个函数干什么、输入什么、返回什么。参数的 description 也一样,别偷懒。
# ❌ 太简单
"description": "获取天气"
# ✅ 写清楚
"description": "获取指定城市的当前天气信息,返回温度(数字)、湿度(百分比)、天气状况(文字描述)"
坑 3:忘记把 assistant message 加回 messages
这个 bug 特别坑,报错信息还不明显。Claude 返回工具调用后,必须先把那条 assistant 消息原封不动加回 messages,再加 tool result,顺序不能乱。
# ❌ 漏掉了 assistant message
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result_str
})
# ✅ 先加 assistant,再加 tool result
messages.append(assistant_message) # 这一步不能少!
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result_str
})
坑 4:并行工具调用时 tool_call_id 必须一一对应
Claude 有时候一次返回多个 tool_calls(比如同时查两个城市的天气)。每个 tool_result 的 tool_call_id 必须和对应的 tool_call 匹配,搞混了就 400 错误。用上面 for tool_call in msg.tool_calls 的写法就不会出问题。
tool_choice 参数详解
这个参数控制 Claude 调不调工具、怎么调:
| 值 | 行为 | 适用场景 |
|---|---|---|
"auto" | Claude 自己决定(推荐) | 大多数场景 |
"none" | 禁止调工具 | 纯聊天轮次 |
"required" | 强制必须调工具 | 你确定这轮一定要调工具 |
{"type": "function", "function": {"name": "xxx"}} | 强制调指定工具 | 流程编排、测试 |
90% 的时候用 "auto" 就够了。只有在做严格的多步骤流程编排时才需要强制指定。
小结
Claude Tool Use 的核心就是那个请求-调用-回传的循环,搞懂这个剩下的都是细节。重点记四条:description 要写详细、content 必须是字符串、assistant message 不能漏、循环要设上限防死循环。
我现在用的方案是通过 ofox.ai 的聚合接口调 Claude,它是一个 AI 模型聚合平台,一个 API Key 可以调 Claude Opus 4.6、GPT-5、Gemini 3 等 50+ 模型,低延迟直连,支持支付宝付款。换模型不用改代码,Claude 的 Tool Use 和 GPT-5 的 Function Calling 用同一套代码就能跑,方便对比效果。
代码我放 Gist 上了,直接 copy 就能跑。有问题评论区见。