Claude Agent SDK 实战指南 — 用Tool Use让AI代理真正执行任务

Claude Agent SDK 实战指南 — 用Tool Use让AI代理真正执行任务

亲自安装anthropic 0.101.0 SDK并完整实现了tool_use代理循环全流程。本文系统介绍了从JSON Schema工具定义、多工具并发调用、错误处理策略到流式响应与成本优化,通过Python实战代码逐步详细讲解区分聊天机器人与真正AI代理的核心设计模式,附可运行代码示例与步骤说明。

在使用FastAPI构建Claude API流式传输后端时,我第一次真正用上了Tool Use。起因很简单:用户问”今年还剩多少天?“,Claude给出了错误的答案。不是一般的错,而是充满自信地错了。看到这一幕,我心里想:“纯聊天机器人确实不够用。”

Tool Use从结构上解决了这个问题。模型不再直接计算,而是调用计算函数并使用返回结果来回答。这个区别,正是聊天机器人与代理的核心分水岭。

下面整理的,是我通过直接安装并运行anthropic SDK 0.101.0验证过的Tool Use模式。基础工具定义、代理循环、错误处理、成本。每一段都以我实际跑过的代码为依据。

Tool Use与聊天机器人的根本区别:结构性差异

大语言模型从概率分布中采样token。日期计算、精确数值运算、外部API查询这类任务在结构上是不可靠的。模型只是在重现训练数据中的模式,而非真正的计算。

Tool Use在另一个层面解决这个问题。模型决定”该做什么”,实际执行委托给外部代码。模型不再直接计算,而是输出类似calculate("365 - today.day_of_year")这样的调用,由Python代码执行并返回结果。

# 聊天机器人:模型直接回答
# "不知道今天是几月几日,还得直接计算 -> 可能出错"
response = client.messages.create(
    model="claude-opus-4-7",
    messages=[{"role": "user", "content": "今年还剩多少天?"}]
)

# 代理:委托给工具
# "模型选择工具,Python精确计算"
response = client.messages.create(
    model="claude-opus-4-7",
    tools=tools,  # 包含日期计算工具定义
    messages=[{"role": "user", "content": "今年还剩多少天?"}]
)

决定性的区别在于可靠性。Python的datetime模块不会算错日期。

安装anthropic 0.101.0并初始化客户端

python3 -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install anthropic

在临时目录中直接安装的结果:

anthropic version: 0.101.0
Client instantiated: ✓
Client type: Anthropic

0.101.0是截至2026-05-13的最新版本。这是Anthropic官方SDK,与2025年之前使用的pyautogen等包完全不同。

import anthropic
import json
from typing import Any

client = anthropic.Anthropic(api_key="your-api-key")  # 也可使用ANTHROPIC_API_KEY环境变量

SDK会自动从ANTHROPIC_API_KEY环境变量读取API密钥。不要在代码中硬编码密钥。

定义第一个工具:JSON Schema就是全部

Tool Use使用与OpenAI Function Calling类似的结构。每个工具由三部分组成:

  • name:工具标识符(类似函数名)
  • description:模型判断何时使用此工具的依据
  • input_schema:输入参数的JSON Schema
tools = [
    {
        "name": "get_current_date_info",
        "description": "返回当前日期和时间信息。用于涉及'今天'、'现在'或需要当前日期知识的问题。",
        "input_schema": {
            "type": "object",
            "properties": {
                "timezone": {
                    "type": "string",
                    "description": "IANA时区(如Asia/Shanghai、UTC)。默认值:UTC"
                }
            },
            "required": []
        }
    },
    {
        "name": "calculate",
        "description": "执行数学运算。处理加法、减法、乘法、除法、乘方和取模等基本运算。",
        "input_schema": {
            "type": "object",
            "properties": {
                "operation": {
                    "type": "string",
                    "enum": ["add", "subtract", "multiply", "divide", "power", "modulo"],
                    "description": "要执行的运算类型"
                },
                "a": {"type": "number", "description": "第一个操作数"},
                "b": {"type": "number", "description": "第二个操作数"}
            },
            "required": ["operation", "a", "b"]
        }
    }
]

description字段比看起来更重要。模型只读描述来决定是否使用这个工具。我测试时发现,描述模糊的话模型会选错工具或根本不使用工具。

沙盒实际验证的工具定义结构:

Tool: get_current_date_info
  Description: 返回当前日期信息
  Required params: []

Tool: calculate
  Description: 执行数学运算
  Required params: ['operation', 'a', 'b']

实现代理循环:调用与响应反复交替的循环

代理循环图 — 从用户消息到工具执行、结果返回的循环流程

这是核心所在。Tool Use不会在单次API调用后结束。模型调用工具后 → 我们执行 → 将结果反馈回去。这个循环在模型返回end_turn之前持续重复。

def run_agent(user_message: str, tools: list, max_iterations: int = 10) -> str:
    messages = [{"role": "user", "content": user_message}]
    
    for i in range(max_iterations):
        response = client.messages.create(
            model="claude-opus-4-7",
            max_tokens=4096,
            tools=tools,
            messages=messages,
        )
        
        # 无工具调用即结束 -> 返回最终回答
        if response.stop_reason == "end_turn":
            for block in response.content:
                if hasattr(block, "text"):
                    return block.text
        
        # 有工具调用时处理
        if response.stop_reason == "tool_use":
            # 将完整的助手响应添加到messages(包含工具调用)
            messages.append({"role": "assistant", "content": response.content})
            
            # 收集工具结果并一次性添加
            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    result = process_tool_call(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })
            
            # 以user角色添加工具结果(API要求)
            messages.append({"role": "user", "content": tool_results})
    
    return "超出最大迭代次数"

这里有两个容易忽视的细节。

第一,必须将response.content整体添加到messages中,不能只提取block.text。模型需要记住自己调用了哪个工具,才能正确生成下一个响应。

第二,工具结果必须以user角色添加。直觉上可能认为是assistant,但API设计上将工具执行结果视为用户(环境)返回的内容。

实战工具实现:计算器、日期、文件读取

from datetime import datetime
import pytz
import json
import operator
from typing import Any

# 安全的数学运算 — 使用运算符映射,避免执行字符串表达式
SAFE_OPERATIONS = {
    "add": operator.add,
    "subtract": operator.sub,
    "multiply": operator.mul,
    "divide": operator.truediv,
    "power": operator.pow,
    "modulo": operator.mod,
}

def process_tool_call(tool_name: str, tool_input: dict[str, Any]) -> str:
    if tool_name == "get_current_date_info":
        tz_str = tool_input.get("timezone", "UTC")
        try:
            tz = pytz.timezone(tz_str)
            now = datetime.now(tz)
            day_of_year = now.timetuple().tm_yday
            days_remaining = 365 - day_of_year
            return json.dumps({
                "date": now.strftime("%Y-%m-%d"),
                "time": now.strftime("%H:%M:%S"),
                "timezone": tz_str,
                "day_of_year": day_of_year,
                "days_remaining_in_year": days_remaining,
            })
        except Exception as e:
            return json.dumps({"error": str(e)})
    
    elif tool_name == "calculate":
        op_name = tool_input.get("operation")
        a = tool_input.get("a", 0)
        b = tool_input.get("b", 0)
        op_func = SAFE_OPERATIONS.get(op_name)
        if op_func is None:
            return f"Error: 未知运算: {op_name}"
        try:
            if op_name == "divide" and b == 0:
                return "Error: 不能除以零"
            result = op_func(a, b)
            return str(result)
        except Exception as e:
            return f"Error: {e}"
    
    return f"Error: 未知工具: {tool_name}"

沙盒实际运行结果:

calculate(multiply, 15, 7) = 105
calculate(add, 105, 3) = 108
calculate(divide, 100, 4) = 25.0
输入验证(存在必填字段): True
输入验证(缺少必填字段): False, Missing required field: location

FastAPI + Claude API流式传输指南中涉及的错误分类策略同样适用于工具错误,可以提高生产环境稳定性。

处理多工具调用:能并行执行吗?

Claude可以在一轮中同时调用多个工具。问”比较首尔和东京的天气”时,会一次返回两个get_weather调用。

# 当Claude一次调用多个工具时
tool_use_blocks = [b for b in response.content if b.type == "tool_use"]

# 技术上可以并行运行
from concurrent.futures import ThreadPoolExecutor, as_completed

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = {
        executor.submit(process_tool_call, block.name, block.input): block
        for block in tool_use_blocks
    }
    tool_results = []
    for future in as_completed(futures):
        block = futures[future]
        result = future.result()
        tool_results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": result,
        })

沙盒验证的多工具执行结果:

{"type": "tool_result", "tool_use_id": "tool_1", "content": "25.0"}
{"type": "tool_result", "tool_use_id": "tool_2", "content": "{\"temp\": 18, \"condition\": \"Sunny\"}"}

建议只对具有幂等性的查询工具使用并行执行。有副作用的外部API调用需要仔细考虑速率限制和顺序问题。

错误处理:优雅地处理工具失败

工具失败时,添加is_error: true返回。模型读取到这个标志后会识别错误情况,尝试其他方法或向用户提供适当指引。

def safe_process_tool_call(tool_name: str, tool_input: dict) -> tuple[str, bool]:
    """工具执行 + 错误处理。返回(content, is_error)"""
    try:
        result = process_tool_call(tool_name, tool_input)
        return result, False
    except Exception as e:
        error_msg = f"工具执行失败: {type(e).__name__}: {str(e)}"
        return error_msg, True

for block in response.content:
    if block.type == "tool_use":
        content, is_error = safe_process_tool_call(block.name, block.input)
        tool_result = {
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": content,
        }
        if is_error:
            tool_result["is_error"] = True
        tool_results.append(tool_result)

设置is_error: true后,模型不会简单地跳过。我在测试中发现,模型会读取错误内容并给出”文件找不到,请检查路径”这样有上下文的提示。返回空字符串或忽略错误往往会导致模型产生混乱或幻觉式的响应。

Tool Use成本现实:会增加多少Token?

说实话,Tool Use会增加成本。根据Anthropic官方文档,每个工具定义约产生200〜300 token的开销。

5个工具定义 → ~1,250 token固定开销(每次请求)
1次工具调用 → 额外的输入 + 输出token
代理循环3轮 → 累积上下文增加

代理循环会持续累积上下文。循环5轮后,从第一条消息到第五次工具结果全部在上下文中。长时间运行的代理成本可能呈指数级增长。

有两种应对方案:

1. 结合Prompt Caching:工具定义在每次请求中都相同。参考Claude API Prompt Caching指南中介绍的缓存模式,可以有效降低重复的工具定义开销。

2. 只传递需要的工具:与其总是包含10个工具定义,不如只传递当前任务需要的2〜3个。工具越多,模型选择时消耗的”注意力”越多,有时还会选错。

流式传输Tool Use实现

with client.messages.stream(
    model="claude-opus-4-7",
    max_tokens=4096,
    tools=tools,
    messages=messages,
) as stream:
    for text_chunk in stream.text_stream:
        print(text_chunk, end="", flush=True)
    
    final_message = stream.get_final_message()

if final_message.stop_reason == "tool_use":
    # ... 与上面相同的处理

参考Vercel AI SDK方式,可以了解这部分在前端集成中是如何被抽象化的。

仍未解决的问题:诚实的局限性

以下是我在实际使用Tool Use过程中感受到的真实限制。

上下文累积问题:代理循环会持续累积上下文。循环10轮后,从第一条消息到第10次工具结果全都在里面。长时间运行的代理必须有上下文管理策略,但目前还没有标准模式。需要手动插入中间摘要或删除不再相关的旧消息。

工具选择的非确定性:相同的问题在不同运行中可能选择不同的工具。即使使用temperature=0也无法保证完全相同的行为。这使得测试比应有的难度更高。

工具定义质量直接决定效果description含糊就会导致选错工具或根本不用工具。写好工具描述本身就是独立的提示工程工作,没有框架能自动解决这个问题。

我认为Tool Use被低估了。代理框架提供了华丽的抽象,但归根结底底层运行的就是这个模式。像PydanticAI的类型安全工具定义方式这样的框架自动生成JSON Schema很方便,但只有理解底层机制,才能在出问题时找到根因。

什么时候用Tool Use,什么时候避免

这是我实际构建后总结出的判断标准。并非每个聊天机器人调用都需要挂上工具。

适合用Tool Use的情况

  • 准确性比流畅度更重要时。日期计算、汇率换算、数值运算这类不能出错的任务,应交给函数而非让模型直接生成。
  • 需要模型不掌握的实时数据时。训练截止之后的信息、内部数据库、外部API响应,只能通过工具获取。
  • 需要执行有副作用的动作时。写文件、发邮件、创建工单等,由模型决定”做什么”、实际执行交给经过验证的代码,这种结构才安全。
  • 需要经过多步骤组合结果时。取出issue列表 → 读取详情 → 汇总,这类多阶段任务由代理循环自然处理。

应该避免Tool Use的情况

  • 仅靠模型内部知识就足够的简单问答。给”Python里怎么给列表排序”挂工具只会徒增token开销。
  • 对延迟敏感的实时UX。代理循环每次工具调用都会产生一次往返。如果单次响应必须快,就严格限制循环次数或干脆不用工具。
  • 成本上限很紧的大批量任务。每个工具定义约250 token的固定开销和上下文累积会乘以调用次数。数百万条的批处理,无工具的单次调用可能更经济。
  • 必须确定性的流水线。工具选择本身是非确定的,如果工作流需要每次都保证相同的调用顺序,规则化代码更合适。

判断标准很简单:自问”模型直接回答会不会出错,或者它是否需要去取自己不知道的东西”。两者之一就用Tool Use,否则普通调用即可。需要更重的多代理编排的时间点,是在用Claude Agent Teams组建多代理时,但在那之前先把单代理的Tool Use吃透才是正确顺序。

参考的官方文档

本文的所有模式都以Anthropic官方文档为准进行了验证。为想深入研究的读者留下一手出处。

如果想用MCP把工具服务化并复用,用FastMCP构建Python MCP服务器这篇文章介绍了把这个Tool Use模式搬到标准协议上的下一步。

浓缩成五条的Tool Use要点

用anthropic 0.101.0直接实验下来,结论是这样:

  • 工具定义name + description + input_schema就是全部。description的质量决定工具是否被正确使用。
  • 代理循环:检测stop_reason == "tool_use" → 执行工具 → 添加tool_result消息 → 重复。模式简单,但messages结构必须完全正确。
  • 错误处理:使用is_error: true让模型识别失败并适当响应。不要返回空字符串。
  • 成本:每个工具定义约250 token开销。建议结合Prompt Caching,注意多轮代理的上下文累积。
  • 并行工具调用:只对具有幂等性的查询工具使用ThreadPoolExecutor并行执行。

Tool Use是将聊天机器人升级为代理最直接的方法。不需要复杂的框架,仅靠这个模式就能构建实用的代理。

常见问题

Tool Use 与普通的聊天机器人调用有什么不同?
聊天机器人由模型直接生成答案,因此像日期计算这类任务可能会充满自信地算错。Tool Use 让模型只负责决定调用哪个工具,实际执行交给 Python 代码,由 datetime 这样精确的函数返回结果。正是这种委托结构带来了可靠性。
定义一个工具必须包含哪些要素?
每个工具由 name、description、input_schema 三部分构成。name 是标识符,input_schema 是输入参数的 JSON Schema,而 description 是模型判断何时使用该工具的依据。description 含糊时,模型会选错工具甚至完全不用。
工具执行结果应以哪个 role(角色)加入消息?
tool_result 必须以 user 角色加入。直觉上像是 assistant,但 API 设计上把工具结果视为用户(环境)返回的内容。此外,模型的响应不要只取 block.text,应把整个 response.content 作为 assistant 消息追加,这样模型才能记住自己调用了哪个工具。
Tool Use 会增加多少成本,又如何降低?
根据 Anthropic 官方文档,每个工具定义每次请求约带来 200〜300 个 token 的固定开销,且代理循环会不断累积上下文。可对工具定义和系统提示应用 Prompt Caching(cache_control ephemeral),并只传入当前任务真正需要的 2〜3 个工具来降低成本。

阅读其他语言版本

这篇文章有帮助吗?

您的支持能帮助我创作更好的内容。请我喝杯咖啡吧。

关于作者

jw

Kim Jangwook

AI/LLM专业全栈开发者

凭借10年以上的Web开发经验,构建AI代理系统、LLM应用程序和自动化解决方案。分享Claude Code、MCP和RAG系统的实践经验。

返回博客列表