上周接了个活,给一个旅游小程序做智能客服,需要 AI 能查航班、查天气、下订单。说白了就是让大模型不只是聊天,还能真正"动手干活"。Claude 的 Tool Use(也就是 Function Calling)我之前一直没正经用过,这次从头啃文档踩了不少坑,干脆整理出来。
Claude Tool Use 是 Anthropic 提供的函数调用能力,让 Claude Opus 4.7 / Sonnet 4.6 等模型在对话中识别用户意图后,主动调用你预定义的外部函数(工具),拿到真实数据再生成回答。和 OpenAI 的 Function Calling 思路一样,但 Claude 的 JSON Schema 定义方式和多轮交互流程有些不同,下面一步步带你跑通。
先说结论
| 维度 | 说明 |
|---|---|
| 支持模型 | Claude Opus 4.7、Sonnet 4.6、Haiku 4.5 均支持 |
| 协议 | Anthropic 原生 Messages API + OpenAI 兼容格式均可 |
| 核心流程 | 定义 tools → 发请求 → 模型返回 tool_use → 你执行函数 → 把结果喂回去 → 模型生成最终回答 |
| 最大工具数 | 单次请求最多传 128 个 tool 定义(实测超过 20 个延迟会明显上升) |
| 踩坑重点 | tool_use 的 id 必须原样回传,不然直接 400 |
环境准备
Python 3.10+,装两个包就行:
pip install anthropic openai
我这边用的是 OpenAI 兼容格式调 Claude,因为项目里已经有一堆 OpenAI SDK 的代码,不想改太多。当然你用 Anthropic 原生 SDK 也完全没问题,后面两种都会写。
整体调用流程
先看一眼全流程,不然后面代码容易看晕:
sequenceDiagram
participant U as 用户
participant A as 你的后端
participant C as Claude API
participant T as 外部工具/API
U->>A: "北京明天天气怎么样?"
A->>C: messages.create(tools=[get_weather], messages=[...])
C->>A: stop_reason: "tool_use", tool_use: {name: "get_weather", input: {city: "北京"}}
A->>T: 调用真实天气 API
T->>A: {"temp": 26, "condition": "晴"}
A->>C: messages.create(messages=[...tool_result...])
C->>A: "北京明天晴天,气温 26°C,适合出行"
A->>U: 返回最终回答
关键点:Claude 不会自己调 API,它只是告诉你"我想调哪个函数、参数是什么",你自己执行完把结果喂回去。很多人刚上手会以为模型能直接发 HTTP 请求,不是的。
方案一:用 Anthropic 原生 SDK
这是最正统的写法,文档示例基本都是这个格式。
第一步:定义工具
tools = [
{
"name": "get_weather",
"description": "获取指定城市的当前天气信息,包括温度、天气状况、湿度",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,比如 北京、Tokyo、New York"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认摄氏度"
}
},
"required": ["city"]
}
}
]
注意 description 写得越清楚,模型判断"该不该调这个工具"就越准。我一开始偷懒只写了个 "get weather",结果模型经常该调的时候不调。
第二步:发起请求 + 处理工具调用
import anthropic
import json
client = anthropic.Anthropic(api_key="your-key")
def get_weather_real(city: str, unit: str = "celsius") -> dict:
"""这里替换成你真实的天气 API 调用"""
# 模拟返回
return {"city": city, "temp": 26, "condition": "晴", "humidity": "45%"}
def chat_with_tools(user_message: str):
messages = [{"role": "user", "content": user_message}]
# 第一轮:发给 Claude,带上工具定义
response = client.messages.create(
model="claude-sonnet-4-6-20260414",
max_tokens=1024,
tools=tools,
messages=messages
)
# 检查是否要调用工具
if response.stop_reason == "tool_use":
# 找到 tool_use block
tool_use_block = next(
b for b in response.content if b.type == "tool_use"
)
tool_name = tool_use_block.name
tool_input = tool_use_block.input
tool_use_id = tool_use_block.id # 这个 id 必须原样回传!
print(f"Claude 想调用: {tool_name}({tool_input})")
# 执行真实函数
if tool_name == "get_weather":
result = get_weather_real(**tool_input)
else:
result = {"error": f"未知工具: {tool_name}"}
# 第二轮:把工具结果喂回去
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": json.dumps(result, ensure_ascii=False)
}
]
})
final_response = client.messages.create(
model="claude-sonnet-4-6-20260414",
max_tokens=1024,
tools=tools,
messages=messages
)
return final_response.content[0].text
# 不需要工具,直接返回
return response.content[0].text
# 测试
print(chat_with_tools("北京明天天气怎么样?"))
跑出来大概这样:
Claude 想调用: get_weather({'city': '北京', 'unit': 'celsius'})
北京当前天气晴朗,气温 26°C,湿度 45%,挺适合出门的。
方案二:用 OpenAI 兼容格式(推荐老项目迁移)
如果你项目里已经用了 OpenAI SDK,不想大改代码,可以通过 OpenAI 兼容接口调 Claude。改个 base_url 和 model 名就行。
from openai import OpenAI
import json
client = OpenAI(
api_key="your-key",
base_url="https://api.ofox.ai/v1"
)
tools_openai_format = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的当前天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["city"]
}
}
},
{
"type": "function",
"function": {
"name": "search_flights",
"description": "搜索两个城市之间的航班信息",
"parameters": {
"type": "object",
"properties": {
"from_city": {"type": "string"},
"to_city": {"type": "string"},
"date": {"type": "string", "description": "日期,格式 YYYY-MM-DD"}
},
"required": ["from_city", "to_city", "date"]
}
}
}
]
def chat_with_tools_openai(user_message: str):
messages = [{"role": "user", "content": user_message}]
response = client.chat.completions.create(
model="claude-sonnet-4-6-20260414",
messages=messages,
tools=tools_openai_format,
tool_choice="auto"
)
msg = response.choices[0].message
if msg.tool_calls:
messages.append(msg)
for tool_call in msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f"调用工具: {fn_name}({fn_args})")
# 路由到真实函数
if fn_name == "get_weather":
result = get_weather_real(**fn_args)
elif fn_name == "search_flights":
result = {"flights": [
{"airline": "CA1234", "price": 1280, "time": "08:30"},
{"airline": "MU5678", "price": 960, "time": "14:15"}
]}
else:
result = {"error": "unknown tool"}
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result, ensure_ascii=False)
})
final = client.chat.completions.create(
model="claude-sonnet-4-6-20260414",
messages=messages,
tools=tools_openai_format
)
return final.choices[0].message.content
return msg.content
# 测试多工具并行调用
print(chat_with_tools_openai("我想从上海飞北京,4月28号,顺便告诉我北京天气"))
这种写法的好处是,你之前调 GPT-5.5 的代码几乎不用改,换个 model 参数就能跑 Claude。我那个旅游小程序项目就是这么迁移的,改了不到 20 行代码。
OpenRouter 和 ofox.ai 这类聚合平台都支持 OpenAI 兼容协议转发 Claude 请求,ofox.ai 是 0% 加价对齐 Anthropic 官方价格,OpenRouter 收 5.5% 手续费,看你自己取舍。
踩坑记录
折腾这两天踩的坑,全记下来了。
坑一:tool_use_id 没回传,直接 400
这是最蠢的一个。Claude 返回 tool_use 时会带一个 id 字段,类似 toolu_01XFDUDYJgAACzvnptvVer6u,你回传 tool_result 的时候必须把这个 id 原样带上。我一开始以为可以省略,结果:
anthropic.BadRequestError: 400 - {"type":"error","error":{"type":"invalid_request_error","message":"tool_use_id is required for tool_result blocks"}}
坑二:description 写太短,模型不调工具
我有个工具叫 create_order,一开始 description 就写了 "create order"。结果用户说"帮我订一张机票",Claude 愣是自己编了个回答说"好的我帮你查一下",根本不触发工具调用。
把 description 改成 "创建订单,当用户明确要求预订、购买、下单时调用此工具" 之后就正常了。description 不是给人看的,是给模型看的,写得像 prompt 一样详细。
坑三:并行工具调用的 token 消耗比想象中大
用户问"上海飞北京的航班和北京天气",Claude 会在一次响应里同时返回两个 tool_use。这本身没问题,但我发现并行调用时 input token 比单次调用多了大概 30-40%,因为模型要在一个 response 里规划多个工具的参数。
Sonnet 4.6 的价格是 15/M output,并行调用一次大概多花 ¥0.02-0.03,量大的话要注意。
坑四:tool_choice 设成 "required" 会强制调用
有时候用户就是闲聊,比如"你好啊",如果你把 tool_choice 设成 {"type": "any"}(Anthropic 格式)或 "required"(OpenAI 格式),模型会硬凑一个工具调用出来,参数瞎填。
正常情况用 "auto" 就行,让模型自己判断要不要调。
进阶:循环处理多轮工具调用
实际业务里,一个用户请求可能触发多轮工具调用。比如用户说"帮我查北京天气,如果晴天就订后天的机票",Claude 会先调天气工具,看到结果是晴天后再调订票工具。写个循环处理:
def chat_loop(user_message: str, max_rounds: int = 5):
messages = [{"role": "user", "content": user_message}]
for i in range(max_rounds):
response = client.chat.completions.create(
model="claude-sonnet-4-6-20260414",
messages=messages,
tools=tools_openai_format,
tool_choice="auto"
)
msg = response.choices[0].message
if not msg.tool_calls:
return msg.content # 没有工具调用,结束
messages.append(msg)
for tc in msg.tool_calls:
result = dispatch_tool(tc.function.name,
json.loads(tc.function.arguments))
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
print(f"第 {i+1} 轮工具调用完成")
return "达到最大轮次限制"
max_rounds 一定要设,不然遇到模型反复调用的情况会死循环。我设的 5 轮,实际业务里 3 轮基本够了。
小结
Tool Use 的核心就三步:定义工具 schema → 处理 tool_use 响应 → 回传 tool_result。概念不复杂,但 description 写不好、id 没回传这种细节坑确实烦人。
我目前的体感是 Sonnet 4.6 在工具调用的准确率上已经很够用了,Opus 4.7 更强但贵三倍多,除非你的工具定义特别复杂(超过 15 个工具互相有依赖关系),否则 Sonnet 就够。Haiku 4.5 便宜但偶尔会漏掉该调用的工具,不确定是不是我 description 写得还不够好。
先跑起来再说,有问题评论区聊。