Anthropic SDK 工具调用

QuibblerAgentQuibblerAgent 2026-11-01 约 22 分钟 6 次阅读

第 3 篇把工具的"定义"讲完了:tools 参数声明工具名、描述与参数模式,tool_choice 控制模型用不用它。但那一步结束时,模型只是"开口要了工具"——真正把工具跑起来、把结果送回去、让对话继续往下走的部分,一步都还没发生。工具调用的完整形态是一个循环:模型请求调用,你的代码负责执行,执行结果回传给模型,模型再决定是继续要工具,还是直接作答。这个来回滚起来,就是 Agent 的最小骨架。

本文是《Anthropic SDK 从入门到实战》第 4 篇,把循环的每一环拆开讲:先看五步全景与停止条件,再解析响应里的 tool_use 块、回传 tool_result 的正确层级,然后落地成 while 循环骨架,处理多工具并行,最后组装一个文件查询助手。全文按"全景 → 解析 → 回传 → 循环 → 并行 → 实战 → 总结"组织。

1、工具调用循环全景

先立一个前提:模型没有任何执行能力,它能做的只是发出调用请求。工具的执行永远发生在你的代码里——模型决定"调什么、传什么参数",你的程序负责真正去查天气、读文件、跑脚本,再把结果递回去。这个协作关系固定成五步:

  1. 发起带 tools 定义的请求
  2. 检查 stop_reason 是否为 tool_use
  3. 在本地执行模型点名的工具
  4. 把结果作为 tool_result 回传
  5. 重复,直到 stop_reason 变为 end_turn

这个来回用流程图看更直观:

flowchart TD A[发起带工具的请求] --> B{stop_reason 是什么} B -->|tool_use| C[提取 tool_use 块] C --> D[本地执行工具] D --> E[组装 tool_result 回传] E --> A B -->|end_turn| F[取文本块作答]

解读:图里唯一的选择点是 stop_reason——等于 tool_use 就沿着左侧走一圈回到起点,等于 end_turn 就从右侧出循环。循环期间真正要盯的就是这一个字段,它的全部取值与应对方式如下:

取值 含义 循环应对
end_turn 自然说完,回合结束 取文本作答,正常出口
tool_use 模型请求调用工具 执行并回传,继续循环
max_tokens 输出被上限截断 提高上限重试,不算正常出口
stop_sequence 命中自定义停止序列 同 end_turn,回合结束
pause_turn 长回合被服务端暂停 续传暂停消息后继续
refusal 安全原因拒答 终止并检查 stop_details

六种取值里需要动手处理的只有 tool_use 一种,其余都是"收尾"或"异常处理"分支——这也是官方工具调用文档用一个条件判断就能驱动整个循环的原因,取值语义详见停止原因处理。另外记住一个贯穿全文的事实:Messages API 无状态,每轮请求都携带全量历史,循环能一环扣一环,靠的是 messages 数组不断"滚雪球"。

2、解析响应中的 tool_use 块

第 1 篇的内容块家族里,tool_use 只是露了个面,现在它是主角。模型决定调工具时,response.content 里会出现 tool_use 块,前面可能还跟着一句"我查一下"的 text 块。提取写法与第 1 篇取文本如出一辙——判型遍历:

tool_calls = [b for b in response.content if b.type == "tool_use"]

call = tool_calls[0]
print(call.name)   # 工具名,如 get_weather
print(call.input)  # 参数,SDK 已解析成字典
print(call.id)     # 本次调用的唯一编号

把这个块序列化成 JSON,线上传输的原始形状如下:

{
  "type": "tool_use",
  "id": "toolu_01XFDUDYJgACzvnptvVoYEL",
  "name": "get_weather",
  "input": { "location": "旧金山", "unit": "celsius" }
}

解读:三个字段各司其职。name 与工具定义里的名字对应,告诉你要执行哪个函数;input 是模型按 input_schema 填好的参数——注意它已经是 Python 字典,直接取键使用,不需要再做一次 JSON 解析;id 是这次调用的唯一编号,回传结果时必须原样带回,它是模型把"请求"与"结果"配对的唯一凭据。字段完整定义见Messages API 参考。另外两个不要假设:tool_use 不一定排在第 0 个位置,也不一定只有一个——一次回复携带多个 tool_use 块是并行调用的常态,第 5 节展开。

3、执行工具与回传 tool_result

执行环节没有黑魔法:按 name 分发到本地函数、把 input 当参数传进去——查天气就调天气 API,读文件就 open,模型到此已经交完接力棒。真正决定循环能不能续上的,是把结果送回去的姿势。回传代码如下:

output = execute_tool(block.name, block.input)  # 本地执行

messages.append({"role": "assistant", "content": response.content})
messages.append({
    "role": "user",                             # role 必须是 user
    "content": [
        {
            "type": "tool_result",
            "tool_use_id": block.id,            # 与 tool_use 块的 id 对应
            "content": str(output),             # 结果以文本回传
        }
    ],
})

解读:两处 append 缺一不可。第一处把模型上一轮的完整回复(含 tool_use 块)落进历史——不带它,模型不知道自己刚才要过什么;第二处才是结果本身:tool_result 块必须作为 user 消息 content 数组的元素出现,role 是 user,不是 assistant,更不是一条独立的顶层消息。官方排错指南还要求 tool_result 块排在那条 user 消息 content 的最前面,附加的普通文本块只能跟在后面。

注意:新手最常见的 400 都出在层级上——把 tool_result 放进 assistant 消息、当成顶层消息、或 tool_use_id 对不上。规矩只有一条:每个 tool_use 块恰好对应一个 tool_result,id 一一对应。

tool_result 块的字段一览如下:

字段 必填 说明
type 是 固定值 tool_result
tool_use_id 是 对应 tool_use 块的 id
content 否 结果文本,或内容块数组
is_error 否 置 true 标记执行失败

content 最常用字符串,要回传图片等富内容时改用内容块数组。is_error 值得单独强调:工具执行失败也要生成 tool_result——带上可读的错误描述、把 is_error 置 true,模型会据此换参数重试或改走别的路;把异常直接吞掉不回传,下一轮请求立刻因为缺少结果而 400。让模型知道工具失败了,比假装没发生好得多。

4、while 循环骨架

一个来回手写两段代码足够,但真实任务里模型往往连续调工具——先列目录、再开文件、再查引用,每个动作一个来回。把来回套进 while,就是官方工具使用总览给出的循环骨架。完整实现如下:

client = anthropic.Anthropic()
tools = [...]                              # 第 3 篇的工具定义
messages = [{"role": "user", "content": "旧金山现在多少度?"}]

while True:
    response = client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=1024,
        tools=tools,                        # 每轮必带,不能只在第一轮带
        messages=messages,                  # 滚雪球的全量历史
    )
    if response.stop_reason != "tool_use":  # 出口:不再要工具就停
        break
    messages.append({"role": "assistant", "content": response.content})
    results = []                            # 收集本轮全部工具结果
    for block in response.content:
        if block.type == "tool_use":
            output = execute_tool(block.name, block.input)
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,    # 与 tool_use 一一对应
                "content": str(output),
            })
    messages.append({"role": "user", "content": results})

for block in response.content:              # 循环外的最终回答
    if block.type == "text":
        print(block.text)

解读:四个要点。出口条件写成"不是 tool_use 就退",比"等于 end_turn 才退"更稳——max_tokens 截断、refusal 拒答同样不该继续跑循环;tools 与 messages 每一轮都要带全,API 无状态,哪轮少了 tools,模型哪轮就"没有工具可用";append 进历史的是整个 content 而不是只挑 tool_use 块,text 块同样是上下文;采样参数 temperature、top_p、top_k 已在 2026-10-21 API 版本移除,工具调用请求同样一个都不要带。这个骨架配合第 3 节的层级规则,就是一切"自动工具调用"的内核——后面无论换什么框架,背后转的都是这一个 while。

5、多工具与并行调用

"多工具"有两个维度。定义侧:tools 列表本来就能装任意多个工具,模型按 name 自行挑选。响应侧:一次回复的 content 里可能同时出现多个 tool_use 块——并行调用默认开启,比如一句话问三个城市的天气,模型一口气发出三个 get_weather。处理规矩只有一条:逐个执行(可以并发),全部结果装进同一条 user 消息回传。代码如下:

results = []
for call in [b for b in response.content if b.type == "tool_use"]:
    try:
        output = execute_tool(call.name, call.input)
        results.append({"type": "tool_result",
                        "tool_use_id": call.id, "content": str(output)})
    except Exception as e:                  # 失败也要回传,不能跳过
        results.append({"type": "tool_result", "tool_use_id": call.id,
                        "content": f"执行失败:{e}", "is_error": True})

messages.append({"role": "user", "content": results})  # 一条消息装下全部

解读:失败的调用照样生成 tool_result——跳过它就破坏了一一对应,下一轮直接 400;带上 is_error 与可读的错误信息,模型还有机会自己修正。多个结果也绝不能拆成多条 user 消息分两次发送:官方文档明确指出,拆开发送会让模型逐渐放弃并行调用,行为悄悄退化而性能毫无收益。真想提速,是在本地用线程池并发跑 execute_tool,回传这一步永远合并成一条消息。第 4 节骨架里的 for 循环天然满足这条规矩——它本来就是收集完再一次性 append。

6、实战:文件查询助手

零件齐了,组装一个能回答"我的笔记里写了什么"的助手:两个工具 list_files 与 read_file,加上第 4 节的循环。工具定义如下:

tools = [
    {
        "name": "list_files",
        "description": "列出指定目录下的文件与子目录名。",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "相对笔记目录的路径"}
            },
            "required": ["path"],
        },
    },
    {
        "name": "read_file",
        "description": "读取一个文本文件的内容。",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {"type": "string", "description": "相对笔记目录的路径"}
            },
            "required": ["path"],
        },
    },
]

解读:两个工具共用一个 path 参数,粒度刻意做细——工具越小、参数越少,模型选对、填对的概率越高;description 写清"什么时候该用我",是模型挑工具的主要依据。执行侧是一个分发函数加安全底线:

import os

ROOT = os.path.abspath("notes")            # 只允许访问 notes 目录

def execute_tool(name, args):
    path = os.path.abspath(os.path.join(ROOT, args["path"]))
    if not path.startswith(ROOT):           # 防路径穿越,越界一律拒绝
        return "错误:路径越界"
    if name == "list_files":
        return "\n".join(os.listdir(path)) or "(空目录)"
    if name == "read_file":
        with open(path, encoding="utf-8") as f:
            return f.read()
    return f"错误:未知工具 {name}"

解读:工具一旦声明,模型就能要求读任何路径。所以第一步先用 abspath 把参数规整成绝对路径,再检查它必须落在白名单目录 ROOT 之内,越界返回错误文本而不抛异常——这段防护与模型能力无关,是把文件系统交给模型的程序必须守住的底线。主循环直接复用第 4 节骨架,加上 system 提示:

messages = [{"role": "user",
             "content": "notes 目录里有哪些文件?挑最长的一篇总结成三句话。"}]

while True:
    response = client.messages.create(
        model="claude-sonnet-5-5", max_tokens=1024,
        tools=tools, messages=messages,
        system="你是文件助理,先看清目录再读文件,不要臆测内容。",
    )
    if response.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": response.content})
    results = [{"type": "tool_result", "tool_use_id": b.id,
                "content": execute_tool(b.name, b.input)}
               for b in response.content if b.type == "tool_use"]
    messages.append({"role": "user", "content": results})

print(next(b.text for b in response.content if b.type == "text"))

解读:system 里那句"先看清目录再读文件",能明显减少模型跳过 list_files 直接猜文件名的行为。运行起来循环一般转两到三轮:第一轮 stop_reason 为 tool_use,调 list_files 拿到目录;第二轮挑一篇笔记调 read_file;第三轮 stop_reason 变 end_turn,content 里只剩 text 块——那就是最终回答。给循环加一行日志、把每轮的工具名打出来,完整的调用轨迹一目了然,也是日后排查"模型为什么不调工具"的第一手段。

7、总结

本篇把工具调用从"定义"推进到"循环":五步循环由 stop_reason 驱动,tool_use 块给出 id、name、input 三要素,执行结果以 tool_result 块回到 user 消息的 content 数组、与 tool_use_id 一一对应,while 骨架把来回滚到 end_turn 为止;并行调用的全部结果合并进同一条消息,失败用 is_error 如实回传。这套"请求——执行——回传——循环"的机制就是 Agent 一词的具体内核,任何框架的自动工具调用都是它的封装。

留两个练习:其一,给第 6 节的助手加一个 write_file 工具——schema 增加 content 参数、execute_tool 增加分支并沿用路径防护,然后让它"把这篇笔记的总结存成 summary.md",观察模型何时决定动笔;其二,故意把回传的 role 改成 assistant、或删掉一个 tool_use_id,跑一次亲眼看 400 报错长什么样,再改回来——层级关系亲手修对一次,就再也不会包错。对于想让 Claude 从"会说话"变成"能动手"的开发者,建议今天就把这个循环跑起来——工具调用的体感,跑一次胜过读十篇文档。

相关推荐

获取系统SDK版本、判断手机ROM
精选Code

获取系统SDK版本、判断手机ROM

Build获取系统SDK版本Android中部分API的使用,需要在特定的SDK版本之后才能使用,因此在兼容老版本SDK的时候,经常需要判断API的版本。各种Android版本的对应关系参考《Android各版本对应的SDK版本》判断手机ROM有时候需要判断手机系统的ROM,检测ROM是MIUI、EMUI还是Flyme,可以使用getprop命令,去系统build.prop文件(关于build.p

4.2k
AndroidStudio中各种中文乱码问题
精选Code

AndroidStudio中各种中文乱码问题

AndroidStudio中各种中文乱码问题1、编译Java错误信息乱码在出现这个Annotation processors must be explicitly declared now...问题的时候,正好也发现AndroidStudio Build Output错误信息都乱码。经常遇到各种问题,习惯了,现在遇到的问题,都是以后的答案。 1.1、修改项目build.gradle(无效)在整个p

2.8k
PydanticAI 详解:类型安全的 Python AI SDK
精选AI

PydanticAI 详解:类型安全的 Python AI SDK

PydanticAI 详解:类型安全的 Python AI SDK写 LLM 应用最疼的两件事:模型返回的 JSON 不合预期、换一家模型商要改一串代码。pydantic/pydantic-ai(PydanticAI)用 Pydantic 团队的老本行回答了第一件——输出用 BaseModel 定义、运行前先校验,错误从运行时提前到编码时;第二件则用"模型即字符串"回答:换个厂商只改一个参数。如今

57