EP04. "工具调用基础:让 Claude 自己去查数据、做计算"
🔒 登录后可标记已读- Claude 的知识有训练截止日期,问它「现在几点」「今天天气怎样」,它答不出来——工具调用(Tool Use)就是让 Claude 在回答前先「打电话」问一下外部系统,拿到实时数据再回答
- 这篇笔记用官方课程的示范项目「提醒事项系统」贯穿:教 Claude 用三个工具(拿当前时间、日期加减法、设提醒)去完成"帮我 177 天后设个提醒"这类它自己算不准的任务
- 一次完整的工具调用是一个来回:Claude 说"我需要用某个工具"→ 你的代码真正执行那个 Python 函数 → 把结果传回去 → Claude 才给出最终答案,中间至少要两次 API 请求
- 涉及多个工具、多轮调用时,要靠一个
while True循环不断检查stop_reason是不是"tool_use",直到 Claude 不再要求用工具为止 - 前置知识:读过 [EP01](./EP01_claude-api基础入门.md)(
messages.create()基本用法),进阶用法(工具流式输出、内建的 text editor / web search 工具)见 [EP05](./EP05_进阶工具调用与官方工具.md)
重点内容
为什么需要工具调用
Claude 没办法直接访问实时信息。用户问"现在天气如何"这类问题,Claude 只能老实说自己的知识有截止日期,答不了。
解法是一套四步流程:
- 一开始的请求里说明"如果需要外部数据就去查"
- Claude 分析问题,判断需要什么具体信息,发出请求
- 你的程序去实际的 API/数据库把数据抓回来
- Claude 拿到新鲜数据,组织出完整答案
以天气查询为例:用户问天气 → Claude 意识到自己需要实时数据 → 系统呼叫天气 API → 新数据传回 Claude → Claude 结合原始问题和最新数据给出准确回答。
工具调用的好处:
- 能拿到训练数据里没有的最新信息
- 能接上外部数据库和 API
- 回答基于最新可用数据
- 交互过程是结构化的,清楚定义了需要什么数据
示范项目:提醒事项系统
目标:让 Claude 能听懂"帮我设个提醒,我下周四有医生预约"这类自然语言请求,并正确执行。
Claude 本身有三个具体短板:
| 短板 | 说明 |
|---|---|
| 对当前时间没有精确认知 | 不知道"现在"具体是几点几分 |
| 日期计算容易出错 | 尤其是推算比较远的未来日期时 |
| 没有真正设提醒的能力 | 它只是个语言模型,本身不连着任何提醒系统 |
对应引入三个自定义工具:
- Get current date/time tool:给 Claude 精确的当前时间
- Add duration to date/time tool:可靠地做日期加减法
- Set reminder tool:真正执行"设提醒"这个动作
💡 官方课程刻意按「先简单、后复杂」的顺序一个个工具往上加,不是一次性全部实现——方便先把基础打牢,再叠加复杂功能。
第一步:写 Tool Function(普通 Python 函数)
Tool function 就是一个普通 Python 函数,Claude 判断需要额外信息时,会由你的程序自动执行它。
三个写法上的准则:
- 函数名、参数名都要用有描述性的名字
- 要检查输入合不合法,空值/非法值直接拒绝
- 出错时给出有意义的错误信息——这些信息 Claude 是看得到的,能帮它学会怎么修正下一次调用
示范:
def get_current_datetime(date_format="%Y-%m-%d %H:%M:%S"):
if not date_format:
raise ValueError("date_format cannot be empty")
return datetime.now().strftime(date_format)
测试:
get_current_datetime() # Returns "2024-01-15 14:30:25"
get_current_datetime("%H:%M") # Returns "14:30"
写完函数只是第一步,接下来还要写一份 JSON Schema 告诉 Claude 这个函数长什么样,再把它接进聊天系统。
第二步:写 Tool Schema
JSON Schema 不是 AI 专属的东西——它是一套用了很多年、业界通用的数据校验规范,Claude 借用它来理解一个工具需要什么参数。
一份完整的工具定义需要三个部分:
| 字段 | 作用 |
|---|---|
name | 有描述性的标识符,例如 "get_weather" |
description | 说明这个工具做什么、什么时候该用、会返回什么样的数据 |
input_schema | 用 JSON Schema 详细描述函数需要哪些参数 |
写 description 的建议:用 3-4 句话说明功能,讲清楚 Claude 该在什么情况下调用它,说明返回数据的类型,参数也要写清楚说明。
def get_current_datetime(date_format="%Y-%m-%d %H:%M:%S"):
if not date_format:
raise ValueError("date_format cannot be empty")
return datetime.now().strftime(date_format)
get_current_datetime_schema = {
"name": "get_current_datetime",
"description": "Returns the current date and time formatted according to the specified format",
"input_schema": {
"type": "object",
"properties": {
"date_format": {
"type": "string",
"description": "A string specifying the format of the returned datetime. Uses Python's strftime format codes.",
"default": "%Y-%m-%d %H:%M:%S"
}
},
"required": []
}
}
💡 不想手写 schema 的话,可以把 tool function 的代码贴给 Claude、附上 Anthropic 官方文档当参考,直接叫它帮你生成一份格式正确的 schema。
📌 代码组织建议:函数叫 function_name,对应的 schema 就叫 function_name_schema,命名规律一致,代码库大了也好维护。
用 ToolParam 类型能加强类型安全:
from anthropic.types import ToolParam
get_current_datetime_schema = ToolParam({
"name": "get_current_datetime",
"description": "Returns the current date and time formatted according to the specified format",
# ... 其余 schema 内容
})
第三步:理解多 Block 的 Message 结构
开启工具功能之后,API 返回的不再是单纯一段文字,而是一条包含多个内容块(block)的消息,既有说明性文字、也有"要求调用工具"的指令。
发起带工具的请求:
messages = []
messages.append({
"role": "user",
"content": "What is the exact time, formatted as HH:MM:SS?"
})
response = client.messages.create(
model=model,
max_tokens=1000,
messages=messages,
tools=[get_current_datetime_schema],
)
Assistant 回应可能包含的两种 block:
- Text Block:Claude 给人看的说明文字
- ToolUse Block:要调用的函数名、参数(是一个字典)、这次调用的追踪 ID、以及 block 类型标记
要留住这次调用的完整上下文,需要把整个 response.content(包含 text block 和 tool use block)整包存进历史:
messages.append({
"role": "assistant",
"content": response.content
})
完整的工具调用流程:
- 发送带 tool schema 的用户消息
- 收到带 text block 和 tool use block 的 assistant 消息
- 真正执行对应的函数
- 把工具执行结果连同完整对话历史一起发回去
- 收到最终答案
⚠️ 既有的 helper function(比如存历史用的那几个)原本只处理单纯的文字,现在要能处理这种多 block 的结构,得跟着改。
第四步:把工具结果传回去
先真正执行函数:用 response.content[1].input 拿到 Claude 要求的参数(是个字典),再用 ** 解包语法当关键字参数传进去:
get_current_datetime(**response.content[1].input)
Tool Result Block 的结构:执行完函数后,要用一个 tool result block 把结果传回去,包含:
| 字段 | 说明 |
|---|---|
tool_use_id | 必须对应这个结果所回应的那个 ToolUse block 的 id |
content | 执行结果,序列化成字符串 |
is_error | 布尔值,标记有没有出错 |
Claude 可能一次要求多个工具、每个都有各自独立的 ID——回传结果时要让 ID 一一对应,Claude 才知道哪个结果对应哪次请求。
后续请求示范:
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": response.content[1].id,
"content": "15:04:22",
"is_error": False
}]
})
⚠️ 即使这次请求不会再触发新的工具调用,后续请求也要继续带上 tool schema——Claude 需要这份 schema 才能理解对话历史里那些工具调用引用的是什么。
第五步:处理多轮工具调用
有些问题需要 Claude 连续调用好几次工具才能回答。比如"从今天算起 103 天后是星期几",Claude 得先拿到今天的日期,再算出 103 天后的日期。
六步流程:
- 用户提出需要多个工具才能回答的问题
- Claude 回应一个 tool use block,要求调用第一个函数
- 程序执行该函数,把结果传回去
- Claude 判断还需要更多信息
- Claude 要求调用第二个工具
- 程序执行第二个函数,Claude 这才给出最终答案
用一个循环处理任意轮数:
def run_conversation(messages):
while True:
response = chat(messages)
add_assistant_message(messages, response)
# Pseudo code
if response isn't asking for a tool:
break
tool_result_blocks = run_tools(response)
add_user_message(messages, tool_result_blocks)
return messages
一直循环,"直到 Claude 不再要求使用工具为止"。
Helper function 也要跟着升级,支持接收字符串、block 列表、或完整 message 对象:
from anthropic.types import Message
def add_user_message(messages, message):
user_message = {
"role": "user",
"content": message.content if isinstance(message, Message) else message
}
messages.append(user_message)
chat() 也要能传 tools 参数、并且返回完整的 message 对象(不再只回传文字):
def chat(messages, system=None, temperature=1.0, stop_sequences=[], tools=None):
params = {
"model": model,
"max_tokens": 1000,
"messages": messages,
"temperature": temperature,
"stop_sequences": stop_sequences,
}
if tools:
params["tools"] = tools
if system:
params["system"] = system
message = client.messages.create(**params)
return message
要看纯文字内容时,用这个工具函数把所有 text block 挑出来拼在一起:
def text_from_message(message):
return "\n".join(
[block.text for block in message.content if block.type == "text"]
)
第六步:用 stop_reason 精确判断是否要继续循环
Claude 到底要不要用工具,看回应里的 stop_reason 字段——等于 "tool_use" 就代表它要求调用工具,程序该继续循环;不等于就代表已经给出最终答案,可以跳出循环。
def run_conversation(messages):
while True:
response = chat(messages, tools=[get_current_datetime_schema])
add_assistant_message(messages, response)
print(text_from_message(response))
if response.stop_reason != "tool_use":
break
tool_results = run_tools(response)
add_user_message(messages, tool_results)
return messages
run_tools 负责把一条消息里所有 type == "tool_use" 的 block 挑出来,逐个处理:
def run_tools(message):
tool_requests = [
block for block in message.content if block.type == "tool_use"
]
tool_result_blocks = []
for tool_request in tool_requests:
# 对每个 tool_request 执行对应函数、组装成 tool_result block
...
run_tool 是个路由函数,根据工具名分派到具体实现:
def run_tool(tool_name, tool_input):
if tool_name == "get_current_datetime":
return get_current_datetime(**tool_input)
elif tool_name == "another_tool":
return another_tool(**tool_input)
# 有新工具就继续往下加
📌 执行工具那段代码要包一层 try-except——出错了也要照样组装出一个 tool result block,只是把 is_error 设成 True,不能让程序直接崩掉。
第七步:一次接入多个工具
基础架构(循环 + 路由)搭好之后,再加新工具就是重复同一个套路,不用重新设计整个流程。
把「提醒事项系统」三个工具全部接进去,run_conversation 里传一份工具列表:
response = chat(messages, tools=[
get_current_datetime_schema,
add_duration_to_datetime_schema,
set_reminder_schema
])
run_tool 路由函数跟着加分支:
def run_tool(tool_name, tool_input):
if tool_name == "get_current_datetime":
return get_current_datetime(**tool_input)
elif tool_name == "add_duration_to_datetime":
return add_duration_to_datetime(**tool_input)
elif tool_name == "set_reminder":
return set_reminder(**tool_input)
测试请求:"Set a reminder for my doctors appointment. Its 177 days after Jan 1st, 2050"——Claude 会自己先算出目标日期是 2050 年 6 月 27 日,再设提醒,中间串联了两个工具的调用。
新增工具的通用套路,四步走:
- 写工具函数的实现
- 定义工具 schema
- 把 schema 加进
tools列表 - 在
run_tool里加对应的分支
这种模块化设计的好处是:加新工具不用推翻重写既有代码。
常见错误
- ❌ Tool function 出错直接抛异常让程序崩溃——应该包 try-except,把错误信息包进 tool result block(
is_error: True)传回去,让 Claude 看得到、有机会自己调整 - ❌ 传回 tool result 时
tool_use_id对不上——Claude 同时要求多个工具时,ID 一定要一一对应,对不上 Claude 没法把结果跟请求配对 - ❌ 已经不需要新工具调用的后续请求,就把
tools参数拿掉——只要对话历史里还有工具调用记录,每次请求都要继续带上 schema - ❌ 判断"要不要继续循环"用文字内容猜,而不是检查
stop_reason == "tool_use"——用字段判断更准确、不会因为 Claude 的措辞变化而误判 - ❌ 工具函数不做输入校验——空值/非法参数直接放行,容易在执行阶段才炸,不如提前拒绝、给出清楚的报错信息
Sources
官方文档:
- Building with the Claude API(Claude Academy 课程)— https://academy.claude.com/courses/building-with-the-claude-api