Anthropic Claude tool use怎样把tool_use和tool_result配成一轮可继续的对话
来源:17golang原创
时间:2026-11-10 02:58:21 158浏览 收藏
我第一次把 Claude 接到内部查询工具时,用户看到的症状不是模型完全没有回答,而是对话停在一段 tool_use 上,或者下一次请求返回消息顺序错误。复盘后发现,问题集中在同一轮的三个动作没有被当成一份契约:客户端没有保存完整的 assistant 内容、重新生成了关联 ID,或把工具结果放到了错误的消息里。
官方地址:https://platform.claude.com/docs/en/agents-and-tools/tool-use/handle-tool-calls
客户端工具调用的最小闭环是:Claude 返回一个或多个tool_use,客户端执行受控函数,再把每个结果作为下一条user消息中的tool_result回填,并原样保留对应的tool_use_id。这样下一轮才有明确的配对关系。
影响面:为什么对话会停在 tool_use
这类故障通常发生在第一次接入工具时。普通文本响应可以直接展示,但带工具调用的响应包含结构化 block,且 stop_reason 会是 tool_use。如果代码只取文本、看到非空响应就结束,用户会看到半截回答;如果客户端只发送一段普通字符串,Claude 也无法判断这段结果对应哪一次工具调用。
另一个容易被忽略的影响是批量调用:同一轮可能出现多个 tool_use。它们不能共用一个新 ID,也不应拆成夹杂其他消息的多次回填;每个调用都需要自己的 tool_result,并放在同一条结果消息的内容数组中。
时间线:真正的触发条件在消息边界
把一次失败请求按时间拆开,顺序通常是这样的:
- 客户端在
tools参数中声明工具名、描述和input_schema。 - Claude 返回完整的 assistant 内容,其中可能同时有文本 block 和
tool_useblock,停止原因是tool_use。 - 客户端执行与工具名匹配的受控函数,得到成功结果或可公开的错误结果。
- 客户端先追加完整 assistant 消息,再紧接着追加一条 role 为
user的消息;这条消息的内容先放tool_result,再放可选文本。 - Claude 根据回填结果继续生成;没有新的工具调用时,才把最终内容交给用户。
所以触发条件不是“模型没有继续生成”,而是客户端在 assistant 的工具调用消息和 user 的工具结果消息之间插入了错误结构,或者让两者的 ID 失配。
根因:tool_use 是请求对象,不是普通文本
工具契约要在请求阶段写清楚。Anthropic 当前文档把用户自定义客户端工具拆成 name、详细的 description 和 JSON Schema 形式的 input_schema;描述应说明用途、参数含义和限制,但它不能代替客户端的权限判断。

我会把模型可见的契约和真正的执行器分开:模型只能提出结构化调用意图,执行器只从白名单中选择函数,并再次检查参数类型、格式和访问权限。这样即使模型给出未知工具名或额外字段,也不会被动态分发成任意函数调用。
修复动作:完整保存 assistant,再按原 ID 回填
下面的最小示例只演示客户端工具调用的消息循环。它没有启动真实工具,也不代表本地已经执行过查询;关键是保留消息结构和错误边界。
import json
import os
from typing import Any
from anthropic import Anthropic
# SDK 从环境变量读取凭据,模型名也由部署配置决定。
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
MODEL = os.environ["CLAUDE_MODEL"]
# 工具描述帮助模型形成参数,真正权限仍由客户端执行器控制。
TOOLS = [
{
"name": "lookup_order",
"description": "按订单号查询当前用户可见的订单状态,不执行修改操作。",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "形如 ORD-1001 的订单号",
}
},
"required": ["order_id"],
},
}
]
def execute_tool(name: str, payload: dict[str, Any]) -> dict[str, Any]:
# 只允许显式登记的工具,避免模型拼出任意函数名。
if name != "lookup_order":
raise ValueError("unsupported tool")
order_id = payload.get("order_id")
# 参数校验放在执行器中,不能只依赖模型遵守 schema。
if not isinstance(order_id, str) or not order_id.startswith("ORD-"):
raise ValueError("order_id format is invalid")
# 示例只返回脱敏字段,生产代码还要做调用者与资源权限校验。
return {"order_id": order_id, "status": "processing"}
messages = [{"role": "user", "content": "帮我查询订单 ORD-1001 的状态"}]
while True:
response = client.messages.create(
model=MODEL,
max_tokens=800,
tools=TOOLS,
messages=messages,
)
# 必须保存完整 assistant content,不能只留下文本 block。
messages.append({"role": "assistant", "content": response.content})
tool_results = []
# 只有 stop_reason 为 tool_use 时,才按工具调用分支处理。
if response.stop_reason == "tool_use":
for block in response.content:
if block.type != "tool_use":
continue
try:
# block.id 是配对 tool_result 的关键,不能重新生成。
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result, ensure_ascii=False),
})
except Exception as exc:
# 失败也回填原 ID,并明确告诉模型本次执行出错。
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"is_error": True,
"content": str(exc),
})
if not tool_results:
# 没有待回填的工具结果时,当前响应可作为最终回答处理。
print(response.content)
break
# tool_result 紧跟 assistant tool_use,且结果 block 放在 user 内容最前面。
messages.append({"role": "user", "content": tool_results})
防复发:把执行器和消息审计一起固定下来
修复后最值得保留的不是一条提示词,而是几项可复查的记录:每次请求的 stop_reason、工具名、原始 tool_use_id、参数校验结果、结果类型和耗时。日志只记录足够定位问题的摘要,不要无条件写入令牌、授权头、完整订单内容或未脱敏的工具响应。
工具失败时不要吞掉异常,也不要伪造成功数据。把稳定、低敏感的错误信息作为同一个 ID 的 tool_result 返回,并设置 is_error;这样模型可以解释“查询失败”,客户端也能限制重试次数,避免工具故障形成无限循环。

上线前可以固定四项复查:每个 tool_use 是否都有唯一结果;assistant 消息是否完整保留;user 结果消息是否紧随其后且结果 block 在前;执行器是否做了工具名、参数和资源权限检查。涉及删除、转账、发信或权限修改时,还要增加人工确认、幂等键和回滚设计。
相关问题
为什么不能把工具结果当普通文本发送? 普通文本没有与原 tool_use_id 的结构化配对关系,模型无法可靠判断结果属于哪一次调用。
多个工具调用要拆成多条 user 消息吗? 同一轮的多个结果通常放在同一条 user 消息的内容数组中,并分别保留各自的 tool_use_id。
工具报错后还能继续请求吗? 可以,但应把错误作为原调用的 tool_result 返回,限制重试次数,并过滤堆栈、凭据和隐私数据。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
240 收藏
-
354 收藏
-
106 收藏
-
113 收藏
-
306 收藏
-
139 收藏
-
242 收藏
-
374 收藏
-
442 收藏
-
383 收藏
-
289 收藏
-
158 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习