AI TOOLS

EP04. "Tool Use 基础:让 Claude 会「打电话」求助"

首页 › AI 工具 › Claude · Developer and Platform · Amazon Bedrock · EP04
约 27 分钟· #EP04#Claude#Amazon Bedrock
🔒 登录后可标记已读
  • Claude 只认识训练数据里的东西,问它「现在几点」「明天天气怎样」它答不出来——Tool Use 就是让 Claude 在需要额外信息时,能「打电话」请你的程序帮忙查
  • 概念上的流程是「你发问 → Claude 说要查什么 → 你的 server 去查 → Claude 拿结果给最终答案」这四步,但写代码的顺序刚好相反:先写工具函数、再写 JSON schema、再处理 ToolUse/ToolResult、最后才把 schema 塞进请求里
  • 一次 tool use 是一段完整的对话来回:user 问题 → assistant 的 ToolUse 请求 → user 身份送回 ToolResult → assistant 最终回答,四条消息缺一不可,schema 每次请求都要带上
  • 不是每个问题都需要工具——要用 stopReason 判断 Claude 这次是要调用工具还是已经给出最终答案,不能每次都无脑执行 tool 流程
  • 前置知识:EP01 的 converse()/多轮对话维护

重点内容


Tool Use 解决什么问题

Claude 默认只能参照训练数据,没办法提供天气、最新新闻这类「当下」的信息。Tools 让 Claude 能接上外部世界的信息来源,补上这个天生的限制。

概念上的四步流程:

  1. Initial Request:你发一个问题给 Claude,附上「需要额外数据时该怎么查」的说明
  2. Tool Request:Claude 判断出它需要哪个外部数据,发出请求
  3. Data Retrieval:你的 server 实际去查这个数据(比如呼叫天气 API)
  4. Final Response:Claude 把外部数据跟自己的回答结合,给出最终答案

📌 实作顺序跟概念顺序不一样:你得先写好工具函数,再写 JSON schema 描述它,再处理 Claude 传回来的 ToolUse / 你要送回去的 ToolResult,最后才是把 schema 塞进请求发出去——不是照着上面四步的顺序写代码。

课程示例场景:设提醒(reminder)。Claude 本身有三个跟时间相关的硬伤——知道今天日期但不知道精确时间、日期加减算不准(尤其是往后推很多天)、不知道怎么设提醒。对应要写三个工具:拿当前日期时间、日期时间加上一段时长、设提醒。


写 Tool Function

工具函数就是普通的 Python 函数,Claude 需要额外信息时会请你执行它。

最佳实践:

  • 参数命名要清楚、有描述性(后面写 schema 时会很关键)
  • 要验证输入,验证失败就丢错误
  • 📌 要回传有意义的错误信息——Claude 拿到错误后会自己尝试重新调用一次这个函数

第一个工具函数示例:

from datetime import datetime, timedelta

def get_current_datetime(date_format="%Y-%m-%d %H:%M:%S"):
    return datetime.now().strftime(date_format)

命名清楚、参数有合理默认值、回传的就是承诺要回传的东西——这几点是写工具函数的基本要求。


写 JSON Schema 描述工具

写完函数后,要写一份 JSON Schema 告诉 Claude 这个工具怎么用——包含哪些必要参数、该怎么用。这份 schema 同时也能拿去做数据校验。

步骤:

  1. 先写一个 Python 字典,把所有关键字参数配上范例值
  2. 转成合法 JSON(主要差异是 Python 的 True 要变成 JSON 的 true)
  3. 找一个「JSON to JSON Schema converter」在线工具,贴进去自动生成 schema 骨架,把生成结果里的 $schema 声明删掉
  4. 最关键的一步:给每个属性加上详细描述——这些描述是 Claude 判断「这个参数到底该怎么填」的唯一依据

写描述的准则:

  • 说清楚这个工具做什么、什么时候该用、会回传什么
  • 工具整体描述抓 3-4 句话
  • 每个参数的描述要写得够细
  • 卡住写不出来时,可以把函数贴给 Claude,直接请它帮忙写描述

最终 schema 的骨架长这样:一个 toolSpec,里面包含 name、description,以及描述参数细节的 inputSchema。

get_current_datetime_schema = {
    "toolSpec": {
        "name": "get_current_datetime",
        "description": "Gets the current date and time. Use this whenever you need to know what time it is right now, for example before calculating a future date/time or setting a reminder.",
        "inputSchema": {
            "json": {
                "type": "object",
                "properties": {
                    "date_format": {
                        "type": "string",
                        "description": "The strftime format string to format the returned datetime with, e.g. '%Y-%m-%d %H:%M:%S'."
                    }
                },
                "required": []
            }
        }
    }
}

💡 Bedrock 的 converse() 要求 tool schema 的实际 JSON 内容包在 inputSchema 下的 json 这一层里,这跟直接用 Anthropic API 的 input_schema 写法不完全一样,照抄别处代码时要注意这层包装。


toolChoice:要不要强制用工具

toolChoice 有三种选项:

选项行为
auto(默认)Claude 自己判断要不要用工具
anyClaude 必须调用某个工具,但由它挑用哪一个
指定某个工具强制 Claude 调用你指定的那一个

📌 第三种(强制指定)在测试阶段特别好用——想确认某个工具本身能不能正常跑,不想等 Claude 自己判断要不要用。


Claude 的回应:Text Part + ToolUse Part

Claude 决定用工具时,回传的 assistant message 不再是单纯一段文字,而是含多个内容片段:

  • Text Part:给人看的说明文字,例如「I can help you find out the current time. Let me find that information for you」
  • ToolUse Part:结构化信息,说明要调用哪个工具、传什么参数

ToolUse 里有三个关键字段:

  • toolUseId:唯一识别码,之后送回结果时要用同一个 ID 对上
  • name:Claude 要调用的工具名(要跟 schema 里的 name 完全一致)
  • input:一个字典,是 Claude 准备传给这个工具函数的参数

改造 message 处理函数,支持多段内容

原本的 add_user_message/add_assistant_message 只处理纯文字,现在要能同时处理字符串和「多段内容」的 list:

def add_user_message(messages, content):
    if isinstance(content, str):
        user_message = {"role": "user", "content": [{"text": content}]}
    else:
        user_message = {"role": "user", "content": content}
    messages.append(user_message)

def add_assistant_message(messages, content):
    if isinstance(content, str):
        assistant_message = {"role": "assistant", "content": [{"text": content}]}
    else:
        assistant_message = {"role": "assistant", "content": content}
    messages.append(assistant_message)

chat() 也要跟着改,同时回传文字和完整的 parts 列表:

def chat(messages, system=None, temperature=1.0, stop_sequences=[], tools=None):
    # ... 其他设置代码 ...

    response = client.converse(**params)

    text = response["output"]["message"]["content"][0]["text"]
    parts = response["output"]["message"]["content"]

    return text, parts

📌 Claude 回应里有个顶层字段 stopReason,等于 "tool_use" 时代表 Claude 这次是要调用工具,不是给最终文字答案——这时候要去 parts 列表里找带 toolUse 这个 key 的项目,取出工具信息来执行。


执行工具:run_tools() / run_tool()

Claude 一次回应可能同时请求好几个工具,所以要用循环处理,不能只处理第一个。

先过滤出所有带 toolUse 的片段:

def run_tools(parts):
    tool_requests = [part for part in parts if "toolUse" in part]
    tool_result_parts = []

    for tool_request in tool_requests:
        tool_use_id = tool_request["toolUse"]["toolUseId"]
        tool_name = tool_request["toolUse"]["name"]
        tool_input = tool_request["toolUse"]["input"]

用一个 run_tool() 按名字分派到实际的函数:

def run_tool(tool_name, tool_input):
    if tool_name == "get_current_datetime":
        return get_current_datetime(**tool_input)
    else:
        raise Exception(f"Unknown tool name: {tool_name}")

**tool_input 把字典参数原样展开传进函数。

工具结果要包成三个字段:toolUseId(对上原始请求的 ID)、content(工具输出,序列化成字符串)、status("success" 或 "error")。加上错误处理,完整版本:

def run_tools(parts):
    tool_requests = [part for part in parts if "toolUse" in part]
    tool_result_parts = []

    for tool_request in tool_requests:
        tool_use_id = tool_request["toolUse"]["toolUseId"]
        tool_name = tool_request["toolUse"]["name"]
        tool_input = tool_request["toolUse"]["input"]

        try:
            tool_output = run_tool(tool_name, tool_input)
            tool_result_part = {
                "toolResult": {
                    "toolUseId": tool_use_id,
                    "content": [{"text": json.dumps(tool_output)}],
                    "status": "success"
                }
            }
        except Exception as e:
            tool_result_part = {
                "toolResult": {
                    "toolUseId": tool_use_id,
                    "content": [{"text": f"Error: {e}"}],
                    "status": "error"
                }
            }

        tool_result_parts.append(tool_result_part)

    return tool_result_parts

📌 toolUseId 是配对用的——Claude 一次请求多个工具并行执行时,靠这个 ID 才能把结果跟原始请求正确对上。


把结果送回 Claude

完整闭环要走满四条消息:

  1. 先把 Claude 的 tool use 请求本身,用 add_assistant_message() 加进消息历史
  2. 用 run_tools() 跑完所有工具、拿到结果
  3. 把结果用 add_user_message() 塞进去——用户身份送回结果,不是助手身份:
   add_user_message(messages, run_tools(parts))
  1. 再呼叫一次 chat(),这次请求同样要带上 tool schema,否则 Claude 没办法理解对话里那些工具引用是什么意思:
   text, parts = chat(messages, tools=[get_current_datetime_schema])

跑通之后,Claude 就能基于工具结果给出完整回答,例如「The current date and time is 2025-04-03, 12:54:00.」。


多轮对话循环:不是每个问题都要跑工具

如果不管三七二十一每次都去跑 run_tools(),遇到像「1+1 等于几」这种根本不需要工具的问题,会硬塞进一堆空的工具结果消息,把对话历史弄乱。

正确做法:检查 stopReason 决定要不要执行工具。Claude 停止生成有四种原因:

stopReason含义
tool_useClaude 想调用工具
end_turnClaude 已经生成完整回答
max_tokens撞到输出长度上限
stop_sequence撞到你设定的 stop sequence

把 chat() 改成回传一个信息更完整的字典:

def chat(messages, tools=None, system=None, **kwargs):
    # ... 既有代码 ...

    return {
        "parts": parts,
        "stop_reason": response["stopReason"],
        "text": "\n".join([p["text"] for p in parts if "text" in p])
    }

搭一个对话循环,只要 Claude 还在要求用工具就继续跑,直到它给出真正的最终答案:

def run_conversation(messages):
    while True:
        result = chat(messages, tools=[get_current_datetime_schema])

        add_assistant_message(messages, result["parts"])
        print(result["text"])

        if result["stop_reason"] != "tool_use":
            break

        tool_result_parts = run_tools(result["parts"])
        add_user_message(messages, tool_result_parts)

    return messages

同一个 run_conversation() 可以处理两种截然不同的问题:

# 需要工具的问题
messages = []
add_user_message(messages, "What time is it?")
run_conversation(messages)

# 不需要工具、直接能答的问题
messages = []
add_user_message(messages, "What is 1+1?")
run_conversation(messages)

两种场景都能自动处理,靠的就是 stop_reason 这个判断点。

常见错误

  • ❌ 工具函数出错时不回传有意义的错误信息——Claude 会尝试重新调用一次,含糊的错误信息帮不了它修正
  • ❌ 忘记每次请求都要重新带上 tool schema——即使是在送回 ToolResult 之后的那次请求,没带 schema,Claude 会看不懂对话里的工具引用
  • ❌ 把 ToolResult 当成 assistant 身份的消息塞回去——应该用 add_user_message(),以用户身份送回结果
  • ❌ 不管三七二十一每次都执行工具流程——要先检查 stopReason 是不是 tool_use,不是所有问题都需要工具
  • ❌ 只处理 Claude 请求的第一个工具——一次回应可能同时有多个 toolUse,要用循环全部处理完

Sources

官方文档:

  1. Claude with Amazon Bedrock(Claude Academy 课程)— https://academy.claude.com/courses/claude-with-amazon-bedrock