Tool Use 工具调用 入门教程
Tool Use 工具调用 是一种让大型语言模型通过用户定义的函数与外部世界交互的能力——模型不再只能生成文本,还能执行代码、查询数据库、调用 API 或操作文件。本文将从零开始,带你走完完整的实践流程,并深入剖析那些容易忽略的细节。
如果你已经翻阅过 API 文档,却依然遭遇"模型就是不调用工具"或"工具返回了但模型不认"等问题,这篇文章将重点为你拆解这些边界情形,提供可复用的排查路径。
前置准备
开始 Tool Use 之前,请确保你已经准备好:
- API 密钥:以 Anthropic Claude API 为例,你需要从 console.anthropic.com 获取有效的 API Key。本教程基于 Claude API 的
messages接口,请求头中需包含"anthropic-version": "2023-06-01"。 - 支持模型:并非所有模型都支持 Tool Use。截至本文撰写时,Claude 3 Sonnet、Claude 3 Opus、Claude 3.5 Sonnet 以及 Claude 3 Haiku 均支持此功能。若测试中发现模型对工具调用请求毫无反应,请首先核对
model参数是否填写正确。 - 运行环境:你可以使用 cURL、Python(
requests或anthropicSDK)或 Node.js 来发送请求。本教程以 Python 3.10+ 配合anthropic库为例,推荐版本>=0.25.0。
# 最小化安装
pip install anthropic>=0.25.0
# 引入库
from anthropic import Anthropic
client = Anthropic(api_key="your-api-key")
常见陷阱:许多初学者忽略了版本检查。如果使用 0.20.0 之前的 SDK,
tool_use的响应格式会有所不同,后续解析代码可能失效。建议运行pip show anthropic确认版本号。
Tool Use 的核心机制与操作步骤
理解 Tool Use 的工作流程是避免后续踩坑的关键。整个过程并非"模型主动调用函数",而是分为四个阶段:
- 你定义工具(函数名、参数描述、输入格式)
- 你发送消息时附上工具定义
- 模型决定是否调用某个工具,并返回一个
tool_use类型的 content block - 你执行工具,并将结果以
tool_result的形式发回给模型
步骤一:定义工具(Tool Definition)
工具定义使用 JSON Schema 来描述函数签名。这里有一处关键误区:参数描述不够具体,会导致模型不愿意调用或传错参数。
假设我们要创建一个城市天气查询工具,理想的定义如下:
{
"name": "get_weather",
"description": "获取指定城市当前天气信息",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,默认摄氏度"
}
},
"required": ["city"]
}
}
注意这里的写法:input_schema 是固定字段名,不能写成 parameters(后者是 OpenAI 的格式,两套标准不同)。description 应包含示例值,以帮助模型理解数据格式。
工具定义字段速查表:
| 字段 | 作用 | 注意点 |
|---|---|---|
name |
工具名称 | 建议使用大写字母加下划线或驼峰命名 |
description |
告诉模型何时使用此工具 | 写清楚触发条件和返回值含义 |
input_schema |
参数定义(JSON Schema) | 必须包含 type 和 properties |
required |
必填参数列表 | 非必填字段不要放入此列表 |
步骤二:构造请求消息
在首次 API 请求中,你需要将工具定义放入 tools 数组中。下面是一个完整的工作示例,使用 Python SDK:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=[{
"name": "get_weather",
"description": "获取指定城市当前天气信息",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如'北京'、'上海'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["city"]
}
}],
messages=[{
"role": "user",
"content": "北京今天天气怎么样?"
}]
)
步骤三:解析模型响应并执行工具
模型的响应会包含 stop_reason: "tool_use" 以及一个或多个 content block。你需要遍历 content,找到 type 为 tool_use 的 block。
一个真实的 tool_use block 结构如下:
{
"type": "tool_use",
"id": "toolu_01AbCdEfGhIjKlMnOpQrStUv",
"name": "get_weather",
"input": {
"city": "北京",
"unit": "celsius"
}
}
id 是此次调用请求的唯一标识,后续返回 tool_result 时必须携带同一个 id。
常见错误:新手最容易直接将整个 API 响应传给工具函数,而不提取
input。正确做法如下:
import json
for block in response.content:
if block.type == "tool_use":
# 提取参数
city = block.input["city"]
unit = block.input.get("unit", "celsius")
# 调用你自己的函数
weather_data = get_real_weather(city, unit) # 这是你的业务函数
# 构造 tool_result
tool_result_block = {
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(weather_data)
}
步骤四:将工具结果返回模型
你需要将原始的 messages 数组、模型的 response 以及 tool_result 拼接成新的请求。注意顺序:
# 这是第二轮对话,将之前所有内容加上新结果一起发送
messages = [
{"role": "user", "content": "北京今天天气怎么样?"},
{"role": "assistant", "content": response.content}, # 包含 tool_use block
{"role": "user", "content": [tool_result_block]}
]
second_response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools, # 工具定义需要继续携带
messages=messages
)
常见疑问:为什么需要将
assistant的响应原封不动传回去?因为模型需要看到它之前发出的tool_use请求,才能将后续的tool_result关联起来。如果跳过这一步,模型会误认为这是一次全新的对话,继续要求调用工具而非利用结果。
典型场景与边界情况
场景一:模型一次请求调用多个工具
当用户请求涉及多个独立信息查询时,模型的响应中可能包含多个 tool_use block。例如,当用户要求"同时查询北京和上海的天气"时,你可能会收到两个 tool_use。处理方法:并发执行所有工具调用,然后将所有 tool_result 一次性返回。注意,多个 tool_result 的 tool_use_id 必须与对应的 id 一一对应。
场景二:模型不调用工具
这是最令新手困惑的情况。如果模型在应该使用工具的任务中直接返回文本答案(例如它自己编造了一个天气数据),可能的原因包括:
- 工具描述写得太窄:模型没有意识到这个任务应该调用工具。将
description改为更明确的话术,例如"当用户询问天气时,必须使用此工具获取实时数据,不要编造答案"。 - 模型不支持:检查
model参数是否正确。Claude 3 Haiku 也支持,但旧版 Opus(如claude-2)不支持。 max_tokens太小:如果令牌数量限制不足,模型可能在生成完整的tool_use之前被截断。
场景三:工具执行失败
如果实际函数抛出异常(如网络超时或参数无效),不要在 tool_result 中传递异常对象。正确的做法是返回一个明确的错误信息字符串,让模型自主决定如何处理:
try:
result = call_external_api(city)
except TimeoutError:
result = "请求超时,请稍后重试或更换城市名称"
tool_result_block = {
"type