AI TOOLS

EP04. "工具调用基础:让 Claude 自己去查数据、做计算"

首页 › AI 工具 › Claude · Developer and Platform · Claude API · EP04
约 28 分钟· #EP04#Claude#Claude API
🔒 登录后可标记已读
  • 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 只能老实说自己的知识有截止日期,答不了。

解法是一套四步流程:

  1. 一开始的请求里说明"如果需要外部数据就去查"
  2. Claude 分析问题,判断需要什么具体信息,发出请求
  3. 你的程序去实际的 API/数据库把数据抓回来
  4. Claude 拿到新鲜数据,组织出完整答案

以天气查询为例:用户问天气 → Claude 意识到自己需要实时数据 → 系统呼叫天气 API → 新数据传回 Claude → Claude 结合原始问题和最新数据给出准确回答。

工具调用的好处:

  • 能拿到训练数据里没有的最新信息
  • 能接上外部数据库和 API
  • 回答基于最新可用数据
  • 交互过程是结构化的,清楚定义了需要什么数据

示范项目:提醒事项系统

目标:让 Claude 能听懂"帮我设个提醒,我下周四有医生预约"这类自然语言请求,并正确执行。

Claude 本身有三个具体短板:

短板说明
对当前时间没有精确认知不知道"现在"具体是几点几分
日期计算容易出错尤其是推算比较远的未来日期时
没有真正设提醒的能力它只是个语言模型,本身不连着任何提醒系统

对应引入三个自定义工具:

  1. Get current date/time tool:给 Claude 精确的当前时间
  2. Add duration to date/time tool:可靠地做日期加减法
  3. 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
})

完整的工具调用流程:

  1. 发送带 tool schema 的用户消息
  2. 收到带 text block 和 tool use block 的 assistant 消息
  3. 真正执行对应的函数
  4. 把工具执行结果连同完整对话历史一起发回去
  5. 收到最终答案

⚠️ 既有的 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 天后的日期。

六步流程:

  1. 用户提出需要多个工具才能回答的问题
  2. Claude 回应一个 tool use block,要求调用第一个函数
  3. 程序执行该函数,把结果传回去
  4. Claude 判断还需要更多信息
  5. Claude 要求调用第二个工具
  6. 程序执行第二个函数,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 日,再设提醒,中间串联了两个工具的调用。

新增工具的通用套路,四步走:

  1. 写工具函数的实现
  2. 定义工具 schema
  3. 把 schema 加进 tools 列表
  4. 在 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

官方文档:

  1. Building with the Claude API(Claude Academy 课程)— https://academy.claude.com/courses/building-with-the-claude-api