上周接了个私活,甲方要做一个能查天气、查航班、还能下单的智能客服。一开始寻思用 Claude 纯文本对接就行,结果发现 LLM 不能直接调外部 API——它只会"说话",不会"动手"。折腾了两天 Tool Use(也就是 Anthropic 版的 Function Calling),总算把整条链路跑通了,坑踩了不少,写篇完整教程分享一下。
Claude Tool Use 是 Anthropic 提供的函数调用能力,允许你在对话中定义工具(函数),模型判断何时调用、传什么参数,你的代码执行完工具后把结果喂回去,模型再生成最终回答。目前 Claude Opus 4.6 和 Sonnet 4.6 都支持,Sonnet 4.6 性价比最高,我个人项目基本都用它。
先说结论
| 维度 | 说明 |
|---|---|
| 核心原理 | 你定义工具 schema → 模型返回 tool_use 决策 → 你执行函数 → 结果喂回模型 |
| 支持模型 | Claude Opus 4.6、Sonnet 4.6、Haiku 4.6 |
| 协议格式 | Anthropic 原生 Messages API(也兼容 OpenAI 格式的 Function Calling) |
| 难度 | 比 OpenAI 的 function calling 稍复杂,但更灵活 |
| 适用场景 | 智能客服、数据查询、自动化工作流、Agent 编排 |
Tool Use 的工作流程
先看一张图,理解整个调用链路:
sequenceDiagram
participant User as 用户
participant App as 你的代码
participant LLM as Claude API
participant Tool as 外部工具/API
User->>App: "北京明天天气怎么样?"
App->>LLM: 发送消息 + 工具定义
LLM->>App: 返回 tool_use(调用 get_weather)
App->>Tool: 执行 get_weather("北京")
Tool->>App: 返回天气数据
App->>LLM: 发送 tool_result
LLM->>App: "北京明天晴,最高32℃..."
App->>User: 展示最终回答
关键点:模型本身不执行任何函数,它只是告诉你"我觉得现在该调这个工具,参数是这些"。真正执行的是你的代码。这个设计很安全,但也意味着你得自己写执行逻辑和循环。
环境准备
# 安装 Anthropic 官方 SDK
pip install anthropic>=0.39.0
# 或者你用 OpenAI 兼容格式也行(后面会讲)
pip install openai>=1.50.0
API Key 的话,可以去 Anthropic 官方申请,也可以用聚合平台的 Key。我现在用的是 ofox.ai,一个 API Key 能调 Claude、GPT-5、Gemini 3 等 50+ 模型,不用每家单独注册,改个 base_url 就行,省事。
方案一:Anthropic 原生 SDK 实现 Tool Use
这是最标准的写法,直接用 Anthropic 的 Messages API。
第一步:定义工具
import anthropic
import json
# 定义工具列表(JSON Schema 格式)
tools = [
{
"name": "get_weather",
"description": "获取指定城市的天气信息,包括温度、湿度、天气状况",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如:北京、上海、深圳"
},
"date": {
"type": "string",
"description": "日期,格式 YYYY-MM-DD,不传则默认今天"
}
},
"required": ["city"]
}
},
{
"name": "search_flights",
"description": "查询两个城市之间的航班信息",
"input_schema": {
"type": "object",
"properties": {
"departure": {
"type": "string",
"description": "出发城市"
},
"arrival": {
"type": "string",
"description": "到达城市"
},
"date": {
"type": "string",
"description": "出发日期,格式 YYYY-MM-DD"
}
},
"required": ["departure", "arrival", "date"]
}
}
]
input_schema 就是标准的 JSON Schema,模型会根据 description 判断什么时候该调哪个工具。description 写得好不好直接影响调用准确率,这是我踩的第一个坑——一开始写得太简略,模型经常选错工具。
第二步:模拟工具执行函数
def execute_tool(tool_name: str, tool_input: dict) -> str:
"""模拟执行工具,实际项目中这里对接真实 API"""
if tool_name == "get_weather":
# 实际项目中调用天气 API
city = tool_input.get("city", "未知")
return json.dumps({
"city": city,
"temperature": "28℃",
"humidity": "65%",
"condition": "多云转晴",
"wind": "东南风3级"
}, ensure_ascii=False)
elif tool_name == "search_flights":
departure = tool_input.get("departure")
arrival = tool_input.get("arrival")
date = tool_input.get("date")
return json.dumps({
"flights": [
{"flight_no": "CA1234", "time": "08:30-11:00", "price": 980},
{"flight_no": "MU5678", "time": "14:15-16:45", "price": 1120},
],
"date": date,
"route": f"{departure} → {arrival}"
}, ensure_ascii=False)
return json.dumps({"error": f"未知工具: {tool_name}"})
第三步:完整的 Tool Use 循环
这是核心代码,处理模型可能多轮调用工具的情况:
def chat_with_tools(user_message: str):
client = anthropic.Anthropic(api_key="your-api-key")
messages = [{"role": "user", "content": user_message}]
# 循环处理,因为模型可能连续调用多个工具
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=tools,
messages=messages
)
print(f"[Stop Reason]: {response.stop_reason}")
# 如果模型认为不需要调工具,直接返回文本
if response.stop_reason == "end_turn":
# 提取文本内容
for block in response.content:
if hasattr(block, "text"):
print(f"[最终回答]: {block.text}")
return
# 如果模型要调工具
if response.stop_reason == "tool_use":
# 把 assistant 的响应加到消息列表
messages.append({"role": "assistant", "content": response.content})
# 收集所有 tool_result
tool_results = []
for block in response.content:
if block.type == "tool_use":
print(f"[调用工具]: {block.name}, 参数: {block.input}")
# 执行工具
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # 必须对应!
"content": result
})
# 把工具结果喂回去
messages.append({"role": "user", "content": tool_results})
# 测试
chat_with_tools("帮我查一下北京明天的天气,顺便看看北京到上海后天的航班")
运行结果大概长这样:
[Stop Reason]: tool_use
[调用工具]: get_weather, 参数: {'city': '北京', 'date': '2026-07-16'}
[调用工具]: search_flights, 参数: {'departure': '北京', 'arrival': '上海', 'date': '2026-07-17'}
[Stop Reason]: end_turn
[最终回答]: 帮你查到了:
1. 北京明天天气:多云转晴,气温28℃,湿度65%,东南风3级,适合出行。
2. 北京到上海后天的航班:
- CA1234,08:30-11:00,980元
- MU5678,14:15-16:45,1120元
注意模型一次返回了两个 tool_use block,说明它能并行判断需要调用多个工具。
方案二:用 OpenAI 兼容格式调用
如果你的项目已经在用 OpenAI SDK,不想换,也能调 Claude 的 Tool Use。很多聚合平台都做了协议转换,比如 ofox.ai 就兼容 OpenAI 的 Function Calling 格式来调 Claude。
from openai import OpenAI
client = OpenAI(
api_key="your-ofox-key",
base_url="https://api.ofox.ai/v1" # 聚合接口,一个 Key 调所有模型
)
# OpenAI 格式的 tools 定义
openai_tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"date": {"type": "string", "description": "日期 YYYY-MM-DD"}
},
"required": ["city"]
}
}
}
]
def chat_openai_format(user_message: str):
messages = [{"role": "user", "content": user_message}]
while True:
response = client.chat.completions.create(
model="claude-sonnet-4-20250514", # 通过聚合接口调 Claude
messages=messages,
tools=openai_tools,
tool_choice="auto"
)
msg = response.choices[0].message
# 没有工具调用,直接输出
if not msg.tool_calls:
print(f"[最终回答]: {msg.content}")
return
# 处理工具调用
messages.append(msg) # 先把 assistant 消息加上
for tool_call in msg.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
print(f"[调用工具]: {func_name}, 参数: {func_args}")
result = execute_tool(func_name, func_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result
})
chat_openai_format("深圳今天天气怎么样?")
两种方案的核心差异:
| 对比项 | Anthropic 原生 SDK | OpenAI 兼容格式 |
|---|---|---|
| SDK | anthropic | openai |
| 工具定义字段 | input_schema | parameters(包在 function 里) |
| 停止原因 | stop_reason == "tool_use" | msg.tool_calls 是否为空 |
| 结果回传角色 | role: "user" + type: "tool_result" | role: "tool" |
| 切换模型 | 只能用 Claude | 改 model 参数就能换 GPT-5/Gemini 3 |
我个人更推荐方案二,代码通用性更好。万一哪天要换模型,改一行 model= 就完事。
踩坑记录
坑 1:tool_use_id 没对上
最常见的报错。模型返回的每个 tool_use block 都有唯一的 id,回传 tool_result 时 tool_use_id 必须一一对应。漏了或者对错了直接 400。
# ❌ 错误:硬编码 id
{"type": "tool_result", "tool_use_id": "some-random-id", "content": "..."}
# ✅ 正确:从 block.id 拿
{"type": "tool_result", "tool_use_id": block.id, "content": result}
坑 2:工具 description 写得太烂
一开始把 get_weather 的 description 写成 "查天气",两个字。结果模型经常在不该查天气的时候也调这个工具。后来改成详细描述——"获取指定城市的实时天气信息,包括温度、湿度、天气状况、风力等级",准确率直接上来了。
description 要写清楚这个工具能干什么、返回什么、什么时候该用。把它当成给模型看的文档来写。
坑 3:没处理多轮工具调用
有些复杂场景,模型会先调工具 A 拿到结果,再根据结果调工具 B。如果代码只处理一轮就退出了,就会漏掉后续调用。一定要用 while True 循环,直到 stop_reason == "end_turn" 才退出。
坑 4:工具返回的内容太长
有一次把整个数据库查询结果(几百条)直接扔给模型当 tool_result,直接超 token 了。解决方案是在 execute_tool 里做好数据裁剪,只返回模型需要的关键字段。
坑 5:Streaming 模式下解析 tool_use
用 stream=True 的话,tool_use 的内容会分成多个 chunk 到达,需要自己拼接 JSON。这块比较烦,建议先用非 streaming 模式把逻辑跑通,再改 streaming。
进阶:强制调用指定工具
有时候你希望模型必须调某个工具,不要自作主张直接回答。用 tool_choice 参数:
# 强制调用指定工具
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=tools,
tool_choice={"type": "tool", "name": "get_weather"}, # 强制调 get_weather
messages=messages
)
# 或者让模型自己决定(默认行为)
tool_choice={"type": "auto"}
# 或者禁止调用任何工具
tool_choice={"type": "none"}
做确定性流程的时候很有用,比如用户明确说了"查天气",就不需要让模型再判断一次。
小结
Claude Tool Use 的核心就三步:定义工具 → 处理 tool_use 响应 → 回传 tool_result,循环往复直到模型给出最终回答。给 LLM 装上了"手",让它能操作外部世界。
几个建议:
- description 认真写,这是影响准确率的第一因素
- 用
while循环处理多轮工具调用,别只做一轮 - tool_result 做好数据裁剪,别把原始大数据甩给模型
- 要频繁切换 Claude/GPT-5/Gemini 3 对比效果的话,用 OpenAI 兼容格式更方便,改 model 参数就行
代码都是实际项目里跑过的,复制过去改改 API Key 就能用。有问题评论区聊。