登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

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,并放在同一条结果消息的内容数组中。

时间线:真正的触发条件在消息边界

把一次失败请求按时间拆开,顺序通常是这样的:

  1. 客户端在 tools 参数中声明工具名、描述和 input_schema。
  2. Claude 返回完整的 assistant 内容,其中可能同时有文本 block 和 tool_use block,停止原因是 tool_use。
  3. 客户端执行与工具名匹配的受控函数,得到成功结果或可公开的错误结果。
  4. 客户端先追加完整 assistant 消息,再紧接着追加一条 role 为 user 的消息;这条消息的内容先放 tool_result,再放可选文本。
  5. Claude 根据回填结果继续生成;没有新的工具调用时,才把最终内容交给用户。

所以触发条件不是“模型没有继续生成”,而是客户端在 assistant 的工具调用消息和 user 的工具结果消息之间插入了错误结构,或者让两者的 ID 失配。

根因:tool_use 是请求对象,不是普通文本

工具契约要在请求阶段写清楚。Anthropic 当前文档把用户自定义客户端工具拆成 name、详细的 description 和 JSON Schema 形式的 input_schema;描述应说明用途、参数含义和限制,但它不能代替客户端的权限判断。

tools 工具定义、参数 schema、assistant content 与 tool_use_id 的静态关系说明图
图1:静态结构说明图,展示工具定义、assistant tool_use 和关联 ID 的边界。

我会把模型可见的契约和真正的执行器分开:模型只能提出结构化调用意图,执行器只从白名单中选择函数,并再次检查参数类型、格式和访问权限。这样即使模型给出未知工具名或额外字段,也不会被动态分发成任意函数调用。

修复动作:完整保存 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_result、原 tool_use_id 与消息审计边界的静态关系图
图2:静态关系说明图,展示 tool_result 回填、错误分支和审计字段的边界。

上线前可以固定四项复查:每个 tool_use 是否都有唯一结果;assistant 消息是否完整保留;user 结果消息是否紧随其后且结果 block 在前;执行器是否做了工具名、参数和资源权限检查。涉及删除、转账、发信或权限修改时,还要增加人工确认、幂等键和回滚设计。

相关问题

为什么不能把工具结果当普通文本发送? 普通文本没有与原 tool_use_id 的结构化配对关系,模型无法可靠判断结果属于哪一次调用。

多个工具调用要拆成多条 user 消息吗? 同一轮的多个结果通常放在同一条 user 消息的内容数组中,并分别保留各自的 tool_use_id。

工具报错后还能继续请求吗? 可以,但应把错误作为原调用的 tool_result 返回,限制重试次数,并过滤堆栈、凭据和隐私数据。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>