Claude Tool Use 完全教程:从零实现 Function Calling,附完整代码(2026)

12 阅读1分钟

上周接了个私活,甲方要做一个能查天气、查航班、还能下单的智能客服。一开始寻思用 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 原生 SDKOpenAI 兼容格式
SDKanthropicopenai
工具定义字段input_schemaparameters(包在 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_resulttool_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 装上了"手",让它能操作外部世界。

几个建议:

  1. description 认真写,这是影响准确率的第一因素
  2. while 循环处理多轮工具调用,别只做一轮
  3. tool_result 做好数据裁剪,别把原始大数据甩给模型
  4. 要频繁切换 Claude/GPT-5/Gemini 3 对比效果的话,用 OpenAI 兼容格式更方便,改 model 参数就行

代码都是实际项目里跑过的,复制过去改改 API Key 就能用。有问题评论区聊。