Claude API 工具调用入门:定义工具、处理调用、自己写循环
让 Claude 调你自己的函数:怎么定义工具、怎么把结果送回去、一次调多个工具怎么处理、出错怎么告诉它,以及 strict、Tool Runner 和流式参数。例子都在服务器上通过中转站跑过,并列出和官方文档不一样的地方。
本站已实测,测试日期:2026-10-13。
本页目录(15 节)
这篇教程解决一个问题:自己写的程序想让 Claude 查数据库、调内部接口、改文件,该怎么接?
答案是工具调用(tool use,也叫 function calling)。Claude Code 每一次读文件、跑命令,底下用的都是这套机制。这篇讲自己直接调接口时怎么用它。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-13 |
| 方式 | Python:anthropic SDK 1.13.0,部分实验直接发 HTTP 请求 |
| 模型 | Sonnet 5.5 为主,Opus 5.5 和 Opus 4.7 做对比 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
先说实测的结论:
- 定义工具、送回结果、并行调用、
strict、Tool Runner、流式参数,通过这家中转站都能用。 - Sonnet 5.5 和 Opus 5.5 不接受强制调用工具,这和官方文档一致。
- 消息格式写错的几种情况,官方文档说会报 400,通过中转站没有报错。照官方的规则写,不要依赖这一点。详见第十二节。
一、工具调用是一份约定
你告诉 Claude 有哪些操作可用、每个操作的参数长什么样;Claude 决定什么时候调、用什么参数调。模型自己不执行任何东西,它只输出一个结构化的请求,由你的代码(或 Anthropic 的服务器)去执行,再把结果放回对话。
工具按“代码在哪里跑”分三类:
| 类别 | 谁写定义 | 谁执行 | 例子 |
|---|---|---|---|
| 自己定义的工具 | 你 | 你 | 查订单、调内部接口 |
| Anthropic 定义、你来执行的工具 | Anthropic | 你 | bash、文本编辑器、记忆、电脑和浏览器操作 |
| 服务器端工具 | Anthropic | Anthropic | 联网搜索、抓取网页、代码执行 |
这一篇只讲第一类。后两类各有一篇,见文末。
什么时候该用工具,官方给了一个判断:如果你在写正则,想从模型的回答里抠出一个决定,那这个决定本来就该是一次工具调用。 反过来,模型凭已有知识就能答的(总结、翻译、常识问答),不需要工具,多一个来回只会更慢。
二、一个来回:定义、调用、送回结果
下面这段代码在服务器上原样跑过:
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "查询一个城市当前的天气。用户问到某个地方现在的天气、气温、"
"要不要带伞时使用。只返回当前实况,不返回预报。",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "城市名,例如“杭州”"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认 celsius"},
},
"required": ["location"],
},
}]
messages = [{"role": "user", "content": "杭州现在天气怎么样?"}]
# 第一次请求:Claude 决定要不要调工具
first = client.messages.create(model="claude-sonnet-5-5", max_tokens=1024,
tools=tools, messages=messages)
print(first.stop_reason) # tool_use
call = next(b for b in first.content if b.type == "tool_use")
print(call.name, call.input) # get_weather {'location': '杭州'}
# 你的代码执行工具(这里用一个写死的结果代替)
result = "多云,18°C,东北风 3 级,湿度 71%"
# 第二次请求:把 Claude 的整条回复和工具结果都放回去
messages.append({"role": "assistant", "content": first.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": call.id, "content": result},
]})
second = client.messages.create(model="claude-sonnet-5-5", max_tokens=1024,
tools=tools, messages=messages)
print(second.stop_reason) # end_turn
print(second.content[-1].text)
输出:
tool_use
get_weather {'location': '杭州'}
end_turn
杭州现在是多云,气温 18°C,东北风 3 级,湿度 71%。
目前没有降雨,不过湿度偏高,体感可能有点潮凉。出门可以带一件薄外套,一般不用带伞。
第一次响应里的工具调用块,原样是这样的(id 截短了):
{"type": "tool_use", "id": "toolu_01NQfC6i…", "name": "get_weather",
"input": {"location": "杭州"}, "caller": {"type": "direct"}}
几件要记住的事:
stop_reason是tool_use,表示它在等你执行工具。id用来对号。你回的tool_result里,tool_use_id要填它。- 把助手的整条
content原样放回去,不要只取文字,里面可能还有思考块。 - 没有
tool这种角色。 工具结果放在user消息里,这一点和 OpenAI 的接口不一样。 caller是direct,表示这是模型直接调的。另一种取值在讲服务器端工具的那篇里。tool_result的content可以是字符串,也可以是内容块的数组(文字、图片、文档)。字符串和文字块数组这两种写法实测过,结果一样。
带上工具要多花多少输入 token? 同一个问题,带这一个工具比不带多了 471 个输入 token。官方文档的说法是:只要请求里有工具,接口就会自动加一段让模型会用工具的系统提示词,Opus 5.5、Sonnet 5.5 和 Haiku 5.5 上是 286 个 token,其余是你的工具定义本身。老模型这段更长,Opus 4.7 是 675 个。
三、把工具定义写好
工具定义有三个必填的部分:
| 字段 | 要求 |
|---|---|
name |
只能用字母、数字、下划线和连字符,最长 128 个字符 |
description |
一段话,说清它做什么、什么时候用 |
input_schema |
一份 JSON Schema,描述参数 |
官方文档说,说明写得够不够详细,是影响工具表现最大的一个因素。它给的做法:
- 每个工具至少写三四句。 讲清楚它做什么、什么时候该用和不该用、每个参数的含义、它不返回什么。
- 把相关的操作合并成一个工具。 与其做
create_pr、review_pr、merge_pr三个,不如做一个,带一个action参数。工具少了,模型更不容易选错。 - 工具多了就加前缀。 例如
github_list_prs、slack_send_message。 - 返回的内容只留有用的。 给稳定、有意义的标识符,只带模型下一步推理用得着的字段。
- 不要设一个让模型写“推理过程”的参数。 想知道它为什么这么调,就要一句简短的解释。要它一步一步的思考过程,可能会被拒答。
工具名出现重复时,请求会被拒绝。实测到的报错是 tools: Tool names must be unique.。
给几个输入的例子
参数嵌套比较深、或者格式有讲究的工具,可以在定义里加 input_examples,放几组合法的输入:
{
"name": "search_flights",
"description": "按目的地、日期、人数和舱位搜索航班。",
"input_schema": { "…": "…" },
"input_examples": [
{"destination": "成都", "date": "2026-11-03", "passengers": 2, "cabin": "economy"},
{"destination": "东京", "date": "2026-12-24", "passengers": 1, "cabin": "business"}
]
}
每个例子都必须符合 input_schema,否则请求报 400。实测故意放了一个不合法的例子,拿到的报错直接指出了哪里不对:
tools.0.custom: Example at index 0 is invalid: "超级经济舱" is not one of
"economy", "business" or "first". Each example must match the tool's `input_schema`.
例子是要占输入 token 的。上面这两组例子,实测多了 299 个输入 token。
用户没给全参数时
官方文档说,缺了必填参数时,Opus 更可能反过来问你,Sonnet 可能会自己猜一个。本站用“现在天气怎么样?”(没说城市)各试了三次,Sonnet 5.5 和 Opus 5.5 这六次都是反问“你想查哪个城市”,没有猜。次数很少,只能说明这一代模型在这个简单例子上没有乱猜。
四、tool_choice:让不让它调、调哪个
tool_choice |
意思 |
|---|---|
{"type": "auto"} |
由 Claude 决定调不调。带了工具时的默认值 |
{"type": "none"} |
这一次不许调工具 |
{"type": "any"} |
必须调一个工具,调哪个它自己选 |
{"type": "tool", "name": "…"} |
必须调指定的这个工具 |
后两种叫强制调用。Opus 5.5、Sonnet 5.5、Fable 5.1 和 Mythos 5.1 不支持强制调用,会返回 400。 实测结果:
| 模型 | auto |
none |
any |
指定工具 |
|---|---|---|---|---|
| Sonnet 5.5 | 200 | 200 | 400 | 400 |
| Opus 5.5 | 200 | 200 | 400 | 400 |
| Opus 4.7 | 200 | 200 | 200,调了工具 | 200,调了工具 |
测试用的问题是“用一句话说说杭州这座城市”,和天气工具没有关系。auto 下三个模型都没有调工具,Opus 4.7 在强制下照样调了 get_weather。
在这几个新模型上,官方建议的替代办法:
- 想让它一定调工具:用
auto,在提示词里说明。例如在用户消息末尾加一句“用 get_weather 工具回答”。 - 想让参数一定合法:用
auto加strict,见第九节。 - 想要固定结构的 JSON 回答:用结构化输出,不用拿工具来凑。
另外两点:
- 在支持强制调用的模型上,强制调用会跳过思考,回复直接以工具调用开头,前面也不会有解释的话。
- 用了提示词缓存时,改
tool_choice会让消息部分的缓存失效,工具定义和系统提示词的缓存不受影响。
auto 下调得太少或太多,可以用系统提示词调。官方给的几句话,从轻到重:“先用工具查清楚再回答”“回答之前一定先调一次工具”;反方向是“自己判断该调工具还是直接回答”。
五、一次回复里调好几个工具
Claude 4 及之后的模型,默认会在一次回复里同时发出多个工具调用。实测问“杭州、东京、伦敦现在分别是什么天气、当地几点”,给了查天气和查时间两个工具,Sonnet 5.5 一次回复里发了 6 个 tool_use 块。
处理的规则有三条:
- 每个
tool_use块都要回一个tool_result,用tool_use_id对上。 - 所有结果放在同一条
user消息里。 - 结果放在最前面,文字只能跟在后面。
这些调用按什么顺序执行、并不并发,由你决定,接口不管。互不相干的只读操作可以并发;有副作用或有先后依赖的,按顺序跑。某一个没有执行(比如前一个失败了,后面的你决定不跑),也要给它回一个带 is_error: true 的结果,说明原因。
想关掉并行,在 tool_choice 里加一个字段(它不是顶层参数):
"tool_choice": {"type": "auto", "disable_parallel_tool_use": true}
同一个问题,开和关的对比:
| 请求次数 | 输入 token 合计 | 输出 token 合计 | |
|---|---|---|---|
| 默认(并行) | 2 | 1,595 | 485 |
| 关掉并行 | 7 | 5,075 | 474 |
关掉以后每次只调一个,要来回七次,每次都把越来越长的历史重发一遍,所以输入是并行时的三倍多。除非你的执行环境一次只能跑一个操作,否则不要关。
如果它该并行的时候没有并行,官方文档说最常见的原因是历史里的结果格式不对:把每个结果各放了一条 user 消息。这样写不会报错,但等于在教它以后一个一个调。其次是提示词不够强,可以在系统提示词里明确要求“互不依赖的工具调用要同时发出”。
六、工具出错了怎么告诉它
执行失败时,照样回一个 tool_result,把错误信息放进 content,再加上 is_error: true:
{"type": "tool_result", "tool_use_id": "toolu_…", "is_error": true,
"content": "天气服务超时(上游 504)。稍后可以重试一次。"}
实测在六个结果里把查伦敦时间的那个标成这样的错误,Sonnet 5.5 的下一条回复是一句“伦敦的时间查询超时了,我重试一次。”,后面跟着对同一个工具的再次调用。
错误信息怎么写,直接决定它接下来怎么做:
- 写清楚发生了什么、下一步可以做什么。 只写一个
failed,它只能猜。 - 有副作用的工具,要写明这次操作到底有没有生效。 下一节的退款例子里,报错写的是“这次退款没有执行,没有产生任何扣款或退款。可以原样重试一次”,模型才敢原样重试。如果只写“超时”,它不知道钱退了没有。
- 不要在工具结果里放给模型的指令。 Claude 被训练成把工具结果里的指令当作可能不可信的第三方内容,会拒绝照做或者回头向用户确认。你自己的指令放在后面的
user消息里,工具结果里只放数据。
反过来也一样:工具结果常常来自你控制不了的地方(网页、邮件、用户上传的文件),别人可以在里面埋指令。把这类内容留在 tool_result 里,不要拼进系统提示词。
模型调用工具时参数不对(比如缺了必填项),官方文档说把错误回给它,它一般会改正后重试两三次。想从根上避免,用 strict。
七、完整的循环:查订单、退款
真实的任务很少一个来回就结束。标准的写法是一个循环:只要 stop_reason 是 tool_use,就执行工具、送回结果、再发请求。
下面的例子用了一个内存里的小订单系统(shop.py,三个函数),其中 refund 第一次被调用时故意抛一个超时错误:
import json
import anthropic
import shop # 一个内存里的小订单系统:list_orders、get_order、refund 三个函数
client = anthropic.Anthropic()
def tool(name, description, properties):
return {"name": name, "description": description, "strict": True,
"input_schema": {"type": "object", "properties": properties,
"required": list(properties), "additionalProperties": False}}
TOOLS = [
tool("list_orders", "按客户邮箱列出这个客户的全部订单,返回订单号、日期、商品、金额和状态。"
"不返回扣款明细,要看扣款明细用 get_order。",
{"email": {"type": "string", "description": "客户邮箱"}}),
tool("get_order", "按订单号查一笔订单的详情,包括每一笔扣款(charges)、已退金额"
"和还能退多少(refundable)。",
{"order_id": {"type": "string", "description": "订单号,形如 A1001"}}),
tool("refund", "给一笔订单退款。会真的把钱退给客户,调用前先用 get_order 确认金额。"
"金额单位是元,不能超过这一单还能退的金额。",
{"order_id": {"type": "string"},
"amount": {"type": "number", "description": "退款金额,单位元"},
"reason": {"type": "string", "description": "退款原因,一句话,会写进工单"}}),
]
FUNCS = {"list_orders": shop.list_orders, "get_order": shop.get_order, "refund": shop.refund}
messages = [{"role": "user", "content": "客户 lin@example.com 说国庆后买的那一单被扣了两次钱。"
"查清楚,多扣的退掉,最后告诉我你做了什么。"}]
for step in range(10): # 给循环设一个上限
reply = client.messages.create(
model="claude-sonnet-5-5", max_tokens=2048, tools=TOOLS, messages=messages,
system="你是电商客服后台的助手,用工具查订单、处理退款。只在核实过扣款明细之后才退款。")
if reply.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": reply.content}) # 整条原样放回去
results = []
for block in reply.content:
if block.type != "tool_use":
continue
try:
output = FUNCS[block.name](**block.input)
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": json.dumps(output, ensure_ascii=False)})
except Exception as e: # 出错也要回一个结果
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": f"{type(e).__name__}: {e}", "is_error": True})
print(block.name, block.input, "→", results[-1]["content"][:60])
messages.append({"role": "user", "content": results}) # 全部结果放同一条消息
print(reply.stop_reason)
print(reply.content[-1].text)
实际的输出(最后的回答有删节):
list_orders {'email': 'lin@example.com'} → [{"order_id": "A1001", "date": "2026-09-21", "item": "机械键盘",
get_order {'order_id': 'A1002'} → {"order_id": "A1002", "date": "2026-10-06", "item": "显示器支架",
get_order {'order_id': 'A1003'} → {"order_id": "A1003", "date": "2026-10-09", "item": "USB-C 线
refund {'amount': 199, 'order_id': 'A1002', 'reason': '重复扣款(ch_03 与 ch_02 同时扣 199 元),退还多扣的一笔'} → TimeoutError: 支付网关超时,这次退款没有执行,没有产生任何扣款或退款。可以原样重试一次。
refund {'amount': 199, 'order_id': 'A1002', 'reason': '重复扣款(ch_03 与 ch_02 同时扣 199 元),退还多扣的一笔'} → {"refund_id": "rf_9001", "order_id": "A1002", "amount": 199,
end_turn
已经查清并退掉了多扣的钱。
- **查证过程:** 客户国庆后有两单,A1002 和 A1003。A1003 只扣了一次 39 元,没有问题。A1002 在 10 月 6 日 20:15 同一分钟里扣了两次 199 元,一共扣了 398 元,所以是重复扣款。
- **退款:** 我给 A1002 退了 199 元,退款单号是 rf_9001。第一次调用时支付网关超时,系统确认那次没有执行,我重试了一次。这笔退款只成功了一次,没有重复退。
一共五次请求:列订单,并行查两单的详情,退款失败,重试成功,最后总结。这个循环有几处是有意这样写的:
- 循环有上限。 没有上限的循环,遇到模型反复调同一个工具时会一直烧钱。
stop_reason不是tool_use就退出。 退出以后还要再看一眼是哪种:max_tokens是被截断了,refusal是拒答,pause_turn是服务器端工具还没跑完。各自怎么处理见《报错、限流和停止原因》。- 有副作用的工具(这里的
refund)在真实系统里要加一道人工确认。 自己写循环的好处就在这里:执行之前你可以拦下来。
八、不想自己写循环:SDK 的 Tool Runner
官方七种语言的 SDK 都带一个 Tool Runner(目前是 beta),替你做循环、执行工具、把结果送回去。Python 里用 @beta_tool 装饰一个普通函数,SDK 从类型标注和文档字符串生成工具定义:
import json
import anthropic
from anthropic import beta_tool
import shop
client = anthropic.Anthropic()
@beta_tool
def refund(order_id: str, amount: float, reason: str) -> str:
"""给一笔订单退款。会真的把钱退给客户,调用前先用 get_order 确认金额。金额单位是元,不能超过这一单还能退的金额。
Args:
order_id: 订单号
amount: 退款金额,单位元
reason: 退款原因,一句话,会写进工单
"""
return json.dumps(shop.refund(order_id, amount, reason), ensure_ascii=False)
# list_orders 和 get_order 的写法一样,这里略去
runner = client.beta.messages.tool_runner(
model="claude-sonnet-5-5",
max_tokens=2048,
max_iterations=10,
system="你是电商客服后台的助手,用工具查订单、处理退款。只在核实过扣款明细之后才退款。",
tools=[list_orders, get_order, refund],
messages=[{"role": "user", "content": "客户 lin@example.com 说国庆后买的那一单被扣了两次钱。"
"查清楚,多扣的退掉,最后告诉我你做了什么。"}],
)
for message in runner: # 每轮给出 Claude 的一条回复
for block in message.content:
if block.type == "tool_use":
print("调用", block.name, block.input)
print(message.content[-1].text) # 循环结束时,最后一条就是最终回答
实测它走了和手写循环一样的路:列订单,查两单,退款失败,重试成功,总结。几个看得见的细节:
- 函数抛异常时,Runner 自动回一个
is_error的结果。 送给模型的内容是异常的类型和信息,实测是TimeoutError('支付网关超时,…'),不带调用栈。 - Python SDK 会把完整的调用栈打到日志里(标准的
logging),所以终端里能看到。别的语言的 SDK 不记这个,要自己在工具函数里记。 - 工具函数返回字符串(或内容块)。要返回结构化的数据,先转成 JSON 字符串。
- SDK 生成的定义里带
additionalProperties: false,但没有strict。
常用的几个控制点:
| 想做的事 | Python 里怎么做 |
|---|---|
| 限制最多循环几轮 | max_iterations=10 |
| 不关心中间过程,只要最后的回答 | runner.until_done() |
| 送回去之前看一眼工具结果 | 循环体里调 runner.generate_tool_call_response() |
| 改掉工具结果,或者插一条消息 | 再调 runner.append_messages(…),这一轮的历史由你接管 |
中途改请求参数(如 max_tokens) |
runner.set_messages_params(…) |
| 流式 | 创建时传 stream=True,每轮拿到一个流 |
接管历史时要小心:一旦调了 append_messages,这一轮 Runner 就不再自动追加助手消息和工具结果,你得自己把它们放进去,否则对话往下走不动。
什么时候还是自己写循环:执行之前要人批准、要自己的日志和审计、要按条件决定执行不执行。官方文档也是这么分的。
其他语言的入口:TypeScript 用 betaZodTool()(配 Zod)或 betaTool()(配 JSON Schema);Go 用带 jsonschema 标签的结构体;Java 用带注解的类,并且默认给工具加 strict。本站只测了 Python。
九、strict:保证参数符合 schema
在工具定义里加 "strict": true,接口会在生成时就把模型的输出限制在你的 schema 之内。它保证两件事:input 一定符合 input_schema,name 一定是你给过的工具名。
代价是 schema 只能用 JSON Schema 的一个子集。下面是实测到的几条规则和对应的报错:
| 写法 | 结果 |
|---|---|
对象上没写 additionalProperties: false |
400:'additionalProperties' must be explicitly set to false |
整数加 minimum、maximum |
400:properties maximum, minimum are not supported |
pattern 里用了先行断言 (?=…) |
400:Invalid regex in pattern field |
pattern 用普通写法,如 ^\d{4}-\d{2}-\d{2}$ |
通过 |
字符串加 format: date |
通过 |
有不在 required 里的可选字段 |
通过 |
文档列的不支持项还有:字符串的 minLength 和 maxLength,数组除 minItems 为 0 或 1 以外的约束,递归的 schema,外部的 $ref,正则里的反向引用和 \b。
还有几个数量上的限制,一个请求里所有 strict 工具合起来算:
| 限制 | 上限 |
|---|---|
| strict 工具的个数 | 20 |
| 可选参数的总数 | 24 |
用了 anyOf 或多类型的参数总数 |
16 |
超了会报 400。文档的建议是只给真正要紧的工具加 strict,并且尽量把参数都设成必填。
第一次用一份新的 schema 会慢一点。 接口要先把 schema 编译成语法,编译结果缓存 24 小时。实测不加 strict 的请求约 3 秒,同一个工具加了 strict 后,每换一种 schema 的第一次请求用了 5 到 8 秒。第七节那个循环第一次跑的时候,头一个请求用了 10 秒,后面几个是 3 到 5 秒。
它在这次测试里有没有带来差别?没有测出来。 本站用一句故意写得别扭的话(“数量‘十二’件,单价 ¥9.90,加急:是”)让模型填一个有整数、小数、布尔值和数组的订单,加和不加 strict 各跑五次,十次的类型全对。strict 的意义在于“保证”:不加的时候大多数时候也对,加了以后你可以不写校验和重试的代码。
十、流式输出时的工具参数
流式输出时,工具的参数是一段一段到的:
content_block_start事件里,tool_use块的input是一个空对象{},只是占位。- 之后一连串
input_json_delta事件,每个带一小段partial_json字符串。 content_block_stop之后,把这些片段拼起来,再解析成 JSON。
默认情况下,接口会把每个参数的值攒齐、校验过再发给你。参数很长时(比如让它写一个文件的内容),中间会有一段时间什么都收不到。给工具加上 eager_input_streaming: true,接口就不攒了,模型写多少发多少。
实测让 Sonnet 5.5 用一个 make_file 工具写一份 600 字的文档,各一次:
| 默认 | eager_input_streaming: true |
|
|---|---|---|
| 第一个片段到达 | 2.5 秒 | 2.2 秒 |
| 一半的片段到达 | 12.1 秒 | 6.2 秒 |
| 最后一个片段到达 | 12.3 秒 | 10.5 秒 |
| 片段的样子 | 每段固定 7 个字符 | 长短不一,最长 25 个字符 |
默认方式下,2.5 秒时到的只是文件名那个短参数,文件内容要等到 12 秒左右才一口气全到。开了以后内容是均匀到的。要在界面上实时显示它正在写的东西,就得开。
代价是:接口不再替你校验,拼出来的字符串可能不是合法的 JSON。 最常见的情况是被 max_tokens 截断。实测把 max_tokens 设成 200:
| 拼出来的内容 | 能否解析 | |
|---|---|---|
| 默认 | 只有 {"filename": "notes.md",23 个字符 |
不能 |
| 开了 eager | 226 个字符,停在文件内容的半中间 | 不能 |
两种方式都解析不了,所以不管开没开,解析都要包在 try 里,并且先看 stop_reason。下面这段跑过的代码演示了完整的写法:
import json
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "make_file",
"description": "把内容写进一个文件。",
"eager_input_streaming": True, # 去掉这一行就是默认的缓冲方式
"input_schema": {"type": "object",
"properties": {"filename": {"type": "string"},
"content": {"type": "string", "description": "文件的完整内容"}},
"required": ["filename", "content"]},
}]
buffers = {} # 每个 tool_use 块一个字符串
with client.messages.stream(
model="claude-sonnet-5-5", max_tokens=4096, tools=tools,
messages=[{"role": "user", "content": "用 make_file 工具写一个 notes.md:一份 300 字左右的"
"《Git 分支管理约定》。直接调用工具,不要先解释。"}],
) as stream:
for event in stream:
if event.type == "content_block_start" and event.content_block.type == "tool_use":
buffers[event.index] = "" # 这时 input 是 {},只是占位
elif event.type == "content_block_delta" and event.delta.type == "input_json_delta":
buffers[event.index] += event.delta.partial_json
print(event.delta.partial_json, end="", flush=True) # 片段一到就能显示
final = stream.get_final_message()
print("\nstop_reason:", final.stop_reason)
for raw in buffers.values():
try:
args = json.loads(raw) # 拼完再解析,而且要防着解析失败
print("解析成功:", args["filename"], len(args["content"]), "字符")
except json.JSONDecodeError:
print("解析失败,按出错把原文送回去:",
json.dumps({"INVALID_JSON": raw}, ensure_ascii=False)[:80])
解析失败时,官方建议把收到的原文包在一个 {"INVALID_JSON": "…"} 的对象里,转成字符串,作为带 is_error: true 的工具结果送回去,让模型知道它上一次的参数坏了。包这一层要用 JSON 库,不要自己拼字符串。
eager_input_streaming 只能用在自己定义的工具上。
十一、排错速查
下面是官方排错页的要点,按现象查:
| 现象 | 多半的原因 | 办法 |
|---|---|---|
| 调了 A 工具,你想要的是 B | 两个工具的说明分不清 | 说明里写清“什么时候用”,不只写“做什么” |
| 从来不调你的工具 | 工具重名,或 schema 太笼统 | 查重名;加 input_examples |
| 参数类型不对,或出现 schema 里没有的参数 | 没开 strict | 加 strict: true,或加 input_examples |
| 该并行的时候一个一个调 | 历史里每个结果各占一条消息 | 所有结果放进同一条 user 消息 |
disable_parallel_tool_use 好像没生效 |
设得太晚 | 它要设在返回 tool_use 的那次请求上 |
| 每次请求都没命中缓存 | tool_choice、思考设置或 effort 每次不一样 |
这几项在一段对话里保持不变 |
| 拒绝照工具结果里的话去做,或回头找用户确认 | 你把自己的指令放进了工具结果 | 指令放到后面的 user 消息里 |
| 对工具参数做字符串比较,换了模型就对不上 | 不同模型对 Unicode 和斜杠的转义不一样 | 先用 JSON 库解析,再比较 |
常见的 400 报错:
| 报错里的话 | 原因 |
|---|---|
tool_use ids were found without tool_result blocks immediately after |
有的调用没有回结果,或者结果前面放了别的内容 |
thinking … blocks in the latest assistant message cannot be modified |
把助手消息放回去之前动了里面的思考块。原样放回 |
All tools have defer_loading: true |
所有工具都设成了延迟加载,模型一个也看不到 |
十二、通过中转站实测到的出入
| 项目 | 官方文档 | 通过中转站实测 |
|---|---|---|
| Sonnet 5.5、Opus 5.5 强制调用工具 | 400 | 400,一致。报错文字是中转站自己写的中文 |
| 工具结果前面放一段文字 | 400 | 200,正常回答 |
| 六个调用只回了五个结果 | 400 | 200,模型把缺的那个重新调了一次 |
tool_use_id 写错一个 |
文档没有单独说明 | 200,同上 |
| 工具名带点号、用中文 | 不符合命名规则 | 被接受,调用时返回的也是原名 |
| 工具名 129 个字符 | 超过 128 的上限 | 被接受,但返回的工具名和请求里的不一样,参数里的一个整数成了字符串 |
strict、input_examples、工具重名的校验 |
400 | 400,一致,报错文字是英文的官方格式 |
中间几条对你的影响是:在这家中转站上能跑通的代码,换到官方接口可能直接报 400。 所以写代码的时候按官方的规则来:每个调用都回结果、结果放在最前面、工具名只用字母数字下划线和连字符且不超过 128 个字符。
少回一个结果的那种情况还有一个副作用:模型会把缺的工具再调一次。如果那是个有副作用的工具,就等于执行了两遍。
和 Claude Code、Agent SDK 的关系
- Claude Code 的读文件、改文件、跑命令,都是它内置的工具,走的就是这套
tool_use和tool_result。工具怎么设计才好用,可以看《从 Claude Code 学工具设计》。 - Agent SDK 已经替你写好了循环、权限和上下文管理,自定义工具的写法见《给智能体加自定义工具》。只想要一个现成的智能体,用它比自己写循环省事。
- 自己直接调接口(这一篇)适合循环和执行环境都要自己掌握的场合,比如把工具调用嵌进已有的后端服务。
这个系列接下来两篇讲另外两类工具:Anthropic 定义、由你执行的 bash、文本编辑器、记忆等工具;以及联网搜索、代码执行这些在 Anthropic 服务器上执行的工具。
花费
这一篇一共发了约 100 次请求,大多是几百到一两千 token 的小请求,最长的是四次订单循环(每次合计七八千输入 token)。按官方标价粗算在 0.5 美元上下。
这次实测没有覆盖到的
- 官方接口没有测。 第十二节里“官方文档”一栏是文档的说法,不是本站测到的。
- 只测了 Python。 其他六种语言的 Tool Runner 是按文档整理的。
- Tool Runner 只测了基本用法和工具抛异常的情况。接管历史、中途改参数、流式这几项没有试。
- 强制调用只在 Opus 4.7 上各试了一次。 Fable 5.1、Mythos 5.1 没有测;这家中转站没有 Haiku 5.5。
- strict 的数量上限没有试(20 个工具、24 个可选参数、16 个多类型参数)。
input_examples对调用质量有没有帮助,没有量过,只确认了它被接受和校验。- 各项对比都只跑了一次到五次,时间和 token 数会有波动。
- 只测了一家中转站、一天。
来源
- Tool use with Claude(Claude 开发平台文档)
- How tool use works(Claude 开发平台文档)
- Tutorial: Build a tool-using agent(Claude 开发平台文档)
- Define tools(Claude 开发平台文档)
- Handle tool calls(Claude 开发平台文档)
- Parallel tool use(Claude 开发平台文档)
- Tool runner (SDK)(Claude 开发平台文档)
- Strict tool use(Claude 开发平台文档)
- Fine-grained tool streaming(Claude 开发平台文档)
- Troubleshooting tool use(Claude 开发平台文档)
- Tool reference(Claude 开发平台文档)
- Structured outputs:JSON Schema limitations(Claude 开发平台文档)