Was this page helpful?
本教程通過五個同心環構建一個日曆管理代理。每個環都是一個完整的、可運行的程序,它在前一個環的基礎上恰好添加一個概念。最後,你將手動編寫代理循環,然後用 Tool Runner SDK 抽象替換它。
示例工具是 create_calendar_event。其架構使用嵌套對象、數組和可選字段,因此你將看到 Claude 如何處理現實的輸入形狀,而不是單個平面字符串。
每個環都獨立運行。將任何環複製到新文件中,它將執行而無需來自早期環的代碼。
最小的工具使用程序:一個工具、一條用戶消息、一次工具調用、一個結果。代碼有大量註釋,因此你可以將每一行映射到工具使用生命週期。
請求在用戶消息旁邊發送一個 tools 數組。當 Claude 決定調用工具時,響應返回 stop_reason: "tool_use" 和一個包含工具名稱、唯一 id 和結構化 input 的 tool_use 內容塊。你的代碼執行工具,然後在 tool_result 塊中發送結果,其 tool_use_id 與調用中的 id 匹配。
# Ring 1: Single tool, single turn.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
import json
import anthropic
# Create a client. It reads ANTHROPIC_API_KEY from the environment.
client = anthropic.Anthropic()
# Define one tool. The input_schema is a JSON Schema object describing
# the arguments Claude should pass when it calls this tool. This schema
# includes nested objects (recurrence), arrays (attendees), and optional
# fields, which is closer to real-world tools than a flat string argument.
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"],
},
}
]
# Send the user's request along with the tool definition. Claude decides
# whether to call the tool based on the request and the tool description.
response = client.messages.create(
model="claude-opus-4-6",
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.",
}
],
)
# When Claude calls a tool, the response has stop_reason "tool_use"
# and the content array contains a tool_use block alongside any text.
print(f"stop_reason: {response.stop_reason}")
# Find the tool_use block. A response may contain text blocks before the
# tool_use block, so scan the content array rather than assuming position.
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}")
# Execute the tool. In a real system this would call your calendar API.
# Here the result is hardcoded to keep the example self-contained.
result = {"event_id": "evt_123", "status": "created"}
# Send the result back. The tool_result block goes in a user message and
# its tool_use_id must match the id from the tool_use block above. The
# assistant's previous response is included so Claude has the full history.
followup = client.messages.create(
model="claude-opus-4-6",
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),
}
],
},
],
)
# With the tool result in hand, Claude produces a final natural-language
# answer and stop_reason becomes "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 數組,不如保持一個運行列表並追加到它。每次轉換都會看到完整的先前上下文。
預期結果
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 塊,而不僅僅是第一個。
預期結果
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 讀取錯誤,可以使用更正的輸入重試、要求用戶澄清或解釋限制。
預期結果
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 裝飾器從類型提示和文檔字符串推斷架構。TypeScript SDK 使用帶有 Zod 架構的 betaZodTool。
Tool Runner 在 Python、TypeScript 和 Ruby SDK 中可用。Shell 和 CLI 選項卡顯示注釋而不是代碼;為基於 shell 的腳本保留環 4 循環。
預期結果
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 相同。區別在於代碼:大約一半的行數、沒有手動循環,架構與實現並存。
你從單個硬編碼的工具調用開始,以處理多個工具、並行調用和錯誤的生產形狀代理結束,然後將所有這些折疊到 Tool Runner 中。在此過程中,你看到了工具使用協議的每一部分:tool_use 塊、tool_result 塊、tool_use_id 匹配、stop_reason 檢查和 is_error 信號。
# Ring 2: The agentic loop.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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}"}
# Keep the full conversation history in a list so each turn sees prior context.
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-6",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Loop until Claude stops asking for tools. Each iteration runs the requested
# tool, appends the result to history, and asks Claude to continue.
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-6",
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)# Ring 3: Multiple tools, parallel calls.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
while response.stop_reason == "tool_use":
# A single response can contain multiple tool_use blocks. Process all of
# them and return all results together in one user message.
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-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)# Ring 4: Error handling.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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-6",
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:
# Signal failure so Claude can retry or ask for clarification.
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-6",
max_tokens=1024,
tools=tools,
messages=messages,
)
final_text = next(block for block in response.content if block.type == "text")
print(final_text.text)# Ring 5: The Tool Runner SDK abstraction.
# Source for <CodeSource> in build-a-tool-using-agent.mdx.
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-6",
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)