上周接了个私活,甲方要做一个能查天气、查航班、还能下单的智能客服。一开始寻思用 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 的工作流程
先看一张图,理解整个调用链路:
关键点:模型本身不执行任何函数,它只是告诉你"我觉得现在该调这个工具,参数是这些"。真正执行的是你的代码。这个设计很安全,但也意味着你得自己写执行逻辑和循环。
环境准备
# 安装 Anthropic 官方 SDKpipinstallanthropic>=0.39.0# 或者你用 OpenAI 兼容格式也行(后面会讲)pipinstallopenai>=1.50.0API Key 的话,可以去 Anthropic 官方申请,也可以用聚合平台的 Key。我现在用的是 ofox.ai,一个 API Key 能调 Claude、GPT-5、Gemini 3 等 50+ 模型,不用每家单独注册,改个 base_url 就行,省事。
方案一:Anthropic 原生 SDK 实现 Tool Use
这是最标准的写法,直接用 Anthropic 的 Messages API。
第一步:定义工具
importanthropicimportjson# 定义工具列表(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 写得好不好直接影响调用准确率,这是我踩的第一个坑——一开始写得太简略,模型经常选错工具。
第二步:模拟工具执行函数
defexecute_tool(tool_name:str,tool_input:dict)->str:"""模拟执行工具,实际项目中这里对接真实 API"""iftool_name=="get_weather":# 实际项目中调用天气 APIcity=tool_input.get("city","未知")returnjson.dumps({"city":city,"temperature":"28℃","humidity":"65%","condition":"多云转晴","wind":"东南风3级"},ensure_ascii=False)eliftool_name=="search_flights":departure=tool_input.get("departure")arrival=tool_input.get("arrival")date=tool_input.get("date")returnjson.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)returnjson.dumps({"error":f"未知工具:{tool_name}"})第三步:完整的 Tool Use 循环
这是核心代码,处理模型可能多轮调用工具的情况:
defchat_with_tools(user_message:str):client=anthropic.Anthropic(api_key="your-api-key")messages=[{"role":"user","content":user_message}]# 循环处理,因为模型可能连续调用多个工具whileTrue:response=client.messages.create(model="claude-sonnet-4-20250514",max_tokens=4096,tools=tools,messages=messages)print(f"[Stop Reason]:{response.stop_reason}")# 如果模型认为不需要调工具,直接返回文本ifresponse.stop_reason=="end_turn":# 提取文本内容forblockinresponse.content:ifhasattr(block,"text"):print(f"[最终回答]:{block.text}")return# 如果模型要调工具ifresponse.stop_reason=="tool_use":# 把 assistant 的响应加到消息列表messages.append({"role":"assistant","content":response.content})# 收集所有 tool_resulttool_results=[]forblockinresponse.content:ifblock.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_useblock,说明它能并行判断需要调用多个工具。
方案二:用 OpenAI 兼容格式调用
如果你的项目已经在用 OpenAI SDK,不想换,也能调 Claude 的 Tool Use。很多聚合平台都做了协议转换,比如 ofox.ai 就兼容 OpenAI 的 Function Calling 格式来调 Claude。
fromopenaiimportOpenAI 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"]}}}]defchat_openai_format(user_message:str):messages=[{"role":"user","content":user_message}]whileTrue:response=client.chat.completions.create(model="claude-sonnet-4-20250514",# 通过聚合接口调 Claudemessages=messages,tools=openai_tools,tool_choice="auto")msg=response.choices[0].message# 没有工具调用,直接输出ifnotmsg.tool_calls:print(f"[最终回答]:{msg.content}")return# 处理工具调用messages.append(msg)# 先把 assistant 消息加上fortool_callinmsg.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_useblock 都有唯一的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_weathermessages=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 就能用。有问题评论区聊。