EP04. "Tool Use 基础:让 Claude 会「打电话」求助"
🔒 登录后可标记已读- 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 能接上外部世界的信息来源,补上这个天生的限制。
概念上的四步流程:
- Initial Request:你发一个问题给 Claude,附上「需要额外数据时该怎么查」的说明
- Tool Request:Claude 判断出它需要哪个外部数据,发出请求
- Data Retrieval:你的 server 实际去查这个数据(比如呼叫天气 API)
- 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 同时也能拿去做数据校验。
步骤:
- 先写一个 Python 字典,把所有关键字参数配上范例值
- 转成合法 JSON(主要差异是 Python 的
True要变成 JSON 的true) - 找一个「JSON to JSON Schema converter」在线工具,贴进去自动生成 schema 骨架,把生成结果里的
$schema声明删掉 - 最关键的一步:给每个属性加上详细描述——这些描述是 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 自己判断要不要用工具 |
any | Claude 必须调用某个工具,但由它挑用哪一个 |
| 指定某个工具 | 强制 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
完整闭环要走满四条消息:
- 先把 Claude 的 tool use 请求本身,用
add_assistant_message()加进消息历史 - 用
run_tools()跑完所有工具、拿到结果 - 把结果用
add_user_message()塞进去——用户身份送回结果,不是助手身份:
add_user_message(messages, run_tools(parts))
- 再呼叫一次
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_use | Claude 想调用工具 |
end_turn | Claude 已经生成完整回答 |
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
官方文档:
- Claude with Amazon Bedrock(Claude Academy 课程)— https://academy.claude.com/courses/claude-with-amazon-bedrock