Claude Tool Use 怎么用?从零实现 Function Calling 完整教程(2026)

5 阅读1分钟

上周接了个活,给一个旅游小程序做智能客服,需要 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 的价格是 3/Minput3/M input、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 写得还不够好。

先跑起来再说,有问题评论区聊。