教程:构建一个使用工具的智能体
从单次工具调用到生产就绪的智能体循环的引导式演练。
本教程以五个同心环的方式构建一个日历管理智能体。每一环都是一个完整、可运行的程序,在前一环的基础上恰好增加一个概念。到最后,您将亲手编写出 "agentic loop"(智能体循环),然后用 Tool Runner SDK 抽象来替换它。
示例工具是 create_calendar_event。它的 schema 使用了嵌套对象、数组和可选字段,因此您将看到 Claude 如何处理真实的输入结构,而不仅仅是单个扁平字符串。
第 1 环:单个工具,单轮对话
最小的工具使用程序:一个工具、一条用户消息、一次工具调用、一个结果。代码带有大量注释,便于您将每一行对应到工具使用生命周期。
请求在用户消息之外还发送一个 tools 数组。当 Claude 判断需要调用工具时,响应会返回 stop_reason: "tool_use" 以及一个 tool_use 内容块,其中包含工具名称、唯一的 id 和结构化的 input。您的代码运行该工具,然后在一个 tool_result 块中将结果发回,该块的 tool_use_id 与调用中的 id 相匹配。
# 第 1 环:单个工具,单轮对话。
import json
import anthropic
# 创建客户端。它会从环境变量中读取 ANTHROPIC_API_KEY。
client = anthropic.Anthropic()
# 定义一个工具。input_schema 是一个 JSON Schema 对象,用于描述
# Claude 调用此工具时应传递的参数。此 schema
# 包含嵌套对象(recurrence)、数组(attendees)和可选
# 字段,这比扁平的字符串参数更接近真实世界的工具。
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
}
]
# 将用户的请求连同工具定义一起发送。Claude 会根据
# 请求内容和工具描述来决定是否调用该工具。
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",
}
],
)
# 当 Claude 调用工具时,响应的 stop_reason 为 "tool_use",
# 并且 content 数组中会包含一个 tool_use 块以及可能的文本。
print(f"stop_reason: {response.stop_reason}")
# 查找 tool_use 块。响应中 tool_use 块之前可能包含文本块,
# 因此应遍历 content 数组,而不是假定其位置。
tool_use = next(block for block in response.content if block.type == "tool_use")
print(f"Tool: {tool_use.name}")
print(f"Input: {tool_use.input}")
# 执行工具。在真实系统中,这里会调用您的日历 API。
# 此处结果是硬编码的,以保持示例的自包含性。
result = {"event_id": "evt_123", "status": "created"}
# 将结果发回。tool_result 块需放在 user 消息中,
# 其 tool_use_id 必须与上面 tool_use 块中的 id 匹配。
# 同时包含助手的上一条响应,以便 Claude 拥有完整的历史记录。
followup = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with alice@example.com and bob@example.com on Monday, March 30, 2026 at 10am.",
},
{"role": "assistant", "content": response.content},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
},
],
)
# 获得工具结果后,Claude 会生成最终的自然语言
# 回答,此时 stop_reason 变为 "end_turn"。
print(f"stop_reason: {followup.stop_reason}")
final_text = next(block for block in followup.content if block.type == "text")
print(final_text.text)预期结果
stop_reason: tool_use
Tool: create_calendar_event
Input: {'title': 'Sync', 'start': '2026-03-30T10:00:00', 'end': '2026-03-30T10:30:00', 'attendees': ['alice@example.com', 'bob@example.com']}
stop_reason: end_turn
I've scheduled your 30-minute sync with Alice and Bob for Monday, March 30 at 10am.第一个 stop_reason 是 tool_use,因为 Claude 正在等待日历结果。在您发送结果之后,第二个 stop_reason 是 end_turn,其内容是面向用户的自然语言。
第 2 环:智能体循环
第 1 环假设 Claude 恰好只调用一次工具。实际任务通常需要多次调用:Claude 可能会创建一个事件,读取确认信息,然后再创建另一个。解决办法是使用一个 while 循环,持续运行工具并将结果反馈回去,直到 stop_reason 不再是 "tool_use"。
另一个变化是对话历史。不要在每次请求时从头重建 messages 数组,而是维护一个持续的列表并向其追加内容。每一轮都能看到完整的先前上下文。
# 第 2 环:代理循环。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
}
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
return {"error": f"Unknown tool: {name}"}
# 将完整的对话历史保存在列表中,以便每一轮都能看到先前的上下文。
messages = [
{
"role": "user",
"content": "Schedule a weekly team standup every Monday at 9am for the next 4 weeks. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
}
]
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# 循环直到 Claude 不再请求工具。每次迭代运行所请求的
# 工具,将结果追加到历史记录中,然后让 Claude 继续。
while response.stop_reason == "tool_use":
tool_use = next(block for block in response.content if block.type == "tool_use")
result = run_tool(tool_use.name, tool_use.input)
messages.append({"role": "assistant", "content": response.content})
messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
}
],
}
)
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)预期结果
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.循环可能运行一次或多次,取决于 Claude 如何拆解任务。您的代码不再需要事先知道。
第 3 环:多个工具,并行调用
智能体很少只有一种能力。添加第二个工具 list_calendar_events,这样 Claude 就可以在创建新事件之前检查现有日程。
当 Claude 需要进行多个相互独立的工具调用时,它可能会在单个响应中返回多个 tool_use 块。您的循环需要处理所有这些块,并在一条用户消息中将所有结果一起发回。请遍历 response.content 中的每一个 tool_use 块,而不仅仅是第一个。
# 第 3 环:多个工具,并行调用。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
},
{
"name": "list_calendar_events",
"description": "List all calendar events on a given date.",
"input_schema": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
},
"required": ["date"],
},
},
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
if name == "list_calendar_events":
return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
return {"error": f"Unknown tool: {name}"}
messages = [
{
"role": "user",
"content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# 单个响应可以包含多个 tool_use 块。处理所有这些块,
# 并将所有结果一起放在一条用户消息中返回。
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = run_tool(block.name, block.input)
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(result),
}
)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)预期结果
I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.有关并发执行和顺序保证的更多信息,请参阅并行工具使用。
第 4 环:错误处理
工具会失败。日历 API 可能会拒绝参与者过多的事件,或者日期格式可能有误。当工具抛出错误时,请使用 is_error: true 将错误消息发回,而不是让程序崩溃。Claude 会读取错误信息,并可以使用修正后的输入重试、向用户请求澄清,或解释该限制。
# 第 4 环:错误处理。
import json
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "create_calendar_event",
"description": "Create a calendar event with attendees and optional recurrence.",
"input_schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"start": {"type": "string", "format": "date-time"},
"end": {"type": "string", "format": "date-time"},
"attendees": {
"type": "array",
"items": {"type": "string", "format": "email"},
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {"enum": ["daily", "weekly", "monthly"]},
"count": {"type": "integer", "minimum": 1},
},
},
},
"required": ["title", "start", "end"],
},
},
{
"name": "list_calendar_events",
"description": "List all calendar events on a given date.",
"input_schema": {
"type": "object",
"properties": {
"date": {"type": "string", "format": "date"},
},
"required": ["date"],
},
},
]
def run_tool(name, tool_input):
if name == "create_calendar_event":
if "attendees" in tool_input and len(tool_input["attendees"]) > 10:
raise ValueError("Too many attendees (max 10)")
return {"event_id": "evt_123", "status": "created", "title": tool_input["title"]}
if name == "list_calendar_events":
return {"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]}
raise ValueError(f"Unknown tool: {name}")
messages = [
{
"role": "user",
"content": "Schedule an all-hands with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
try:
result = run_tool(block.name, block.input)
tool_results.append(
{"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(result)}
)
except Exception as exc:
# 发出失败信号,以便 Claude 重试或请求澄清。
tool_results.append(
{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(exc),
"is_error": True,
}
)
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
response = client.messages.create(
model="claude-opus-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)预期结果
I tried to schedule the all-hands but the calendar only allows 10 attendees per event. I can split this into two sessions, or you can let me know which 10 people to prioritize.is_error 标志是与成功结果的唯一区别。Claude 会看到该标志和错误文本,并做出相应的响应。完整的错误处理参考请参阅处理工具调用。
第 5 环:Tool Runner SDK 抽象
第 2 环到第 4 环手工编写了同样的循环:调用 API、检查 stop_reason、运行工具、追加结果、重复。Tool Runner 会替您完成这些工作。将每个工具定义为一个函数,把列表传给 tool_runner,并在循环完成后获取最终消息。错误包装、结果格式化和对话管理都在内部处理。
每个 SDK 都提供了一个辅助工具,可将普通函数转换为可运行的工具,并从其签名推导出输入 schema;下面的选项卡展示了每种语言的惯用写法。
# 第 5 环:Tool Runner SDK 抽象层。
import json
import anthropic
from anthropic import beta_tool
client = anthropic.Anthropic()
@beta_tool
def create_calendar_event(
title: str,
start: str,
end: str,
attendees: list[str] | None = None,
recurrence: dict | None = None,
) -> str:
"""Create a calendar event with attendees and optional recurrence.
Args:
title: Event title.
start: Start time in ISO 8601 format.
end: End time in ISO 8601 format.
attendees: Email addresses to invite.
recurrence: Dict with 'frequency' (daily, weekly, monthly) and 'count'.
"""
if attendees and len(attendees) > 10:
raise ValueError("Too many attendees (max 10)")
return json.dumps({"event_id": "evt_123", "status": "created", "title": title})
@beta_tool
def list_calendar_events(date: str) -> str:
"""List all calendar events on a given date.
Args:
date: Date in YYYY-MM-DD format.
"""
return json.dumps({"events": [{"title": "Existing meeting", "start": "14:00", "end": "15:00"}]})
final_message = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have next Monday, then schedule a planning session that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)预期结果
I checked your calendar for next Monday and found an existing meeting from 2pm to 3pm. I've scheduled the planning session for 10am to 11am to avoid the conflict.输出与第 3 环完全相同。区别在于代码:行数大约减少一半,没有手动循环,并且 schema 与实现放在一起。
您构建了什么
您从一次硬编码的工具调用开始,最终得到了一个具备生产形态的智能体,它能够处理多个工具、并行调用和错误,然后又将这一切收拢到 Tool Runner 中。在此过程中,您见到了工具使用协议的每一个组成部分:tool_use 块、tool_result 块、tool_use_id 匹配、stop_reason 检查以及 is_error 信号。
后续步骤
Was this page helpful?