本教程通过五个同心环构建一个日历管理智能体。每个环都是一个完整、可运行的程序,在前一个环的基础上恰好增加一个概念。完成本教程后,您将亲手编写智能体循环,然后用 Tool Runner SDK 抽象层替换它。
示例工具是 create_calendar_event。它的 schema 使用了嵌套对象、数组和可选字段,因此您将看到 Claude 如何处理真实的输入结构,而不仅仅是单个扁平字符串。
每个环都可以独立运行。将任意一个环复制到新文件中,无需前面环的代码即可执行。
最小的工具使用程序:一个工具、一条用户消息、一次工具调用、一个结果。代码中包含大量注释,以便您将每一行对应到工具使用生命周期。
请求在发送用户消息的同时发送一个 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-4-8",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with [email protected] and [email protected] next Monday 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-4-8",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=[
{
"role": "user",
"content": "Schedule a 30-minute sync with [email protected] and [email protected] next Monday 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': ['[email protected]', '[email protected]']}
stop_reason: end_turn
I've scheduled your 30-minute sync with Alice and Bob for next Monday at 10am.第一个 stop_reason 是 tool_use,因为 Claude 正在等待日历结果。在您发送结果后,第二个 stop_reason 是 end_turn,内容是面向用户的自然语言。
环 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: [email protected], [email protected], [email protected].",
}
]
response = client.messages.create(
model="claude-opus-4-8",
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-4-8",
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 如何分解任务。您的代码不再需要提前知道这一点。
智能体很少只有一种能力。添加第二个工具 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-4-8",
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-4-8",
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.有关并发执行和顺序保证的更多信息,请参阅并行工具使用。
工具会失败。日历 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-4-8",
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-4-8",
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 会看到该标志和错误文本,并做出相应的响应。有关完整的错误处理参考,请参阅处理工具调用。
环 2 到环 4 都是手动编写相同的循环:调用 API、检查 stop_reason、运行工具、追加结果、重复。Tool Runner 会为您完成这些工作。将每个工具定义为一个函数,将列表传递给 tool_runner,然后在循环完成后获取最终消息。错误包装、结果格式化和对话管理都在内部处理。
Python SDK 使用 @beta_tool 装饰器从类型提示和文档字符串推断 schema。TypeScript SDK 使用带有 Zod schema 的 betaZodTool。其他 SDK 使用各自的辅助工具遵循相同的模式:C# 和 PHP 中的 BetaRunnableTool、Java 和 Ruby 中的类型化工具类,以及 Go 中的 toolrunner.NewBetaToolFromJSONSchema。
Tool Runner 在所有七个 SDK 中均可用:Python、TypeScript、C#、Go、Java、PHP 和 Ruby。有关完整参考,请参阅 Tool Runner。cURL 和 CLI 选项卡显示的是说明而非代码;对于基于 curl 或 CLI 的脚本,请继续使用环 4 的循环。
# 第 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-4-8",
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?