このチュートリアルでは、5つの同心円状のリングでカレンダー管理エージェントを構築します。各リングは完全で実行可能なプログラムであり、前のリングに対して正確に1つの概念を追加します。最後まで進むと、エージェントループを手書きで実装し、それをTool Runner SDKの抽象化に置き換えることになります。
例として使用するツールは create_calendar_event です。そのスキーマはネストされたオブジェクト、配列、オプションフィールドを使用しているため、単一のフラットな文字列ではなく、Claudeが現実的な入力形式をどのように処理するかを確認できます。
最小限のツール使用プログラムです。1つのツール、1つのユーザーメッセージ、1つのツール呼び出し、1つの結果。コードには詳細なコメントが付いているので、各行をツール使用のライフサイクルに対応付けることができます。
リクエストはユーザーメッセージと一緒に tools 配列を送信します。Claudeがツール呼び出しが必要だと判断すると、レスポンスは stop_reason: "tool_use" と、ツール名、一意の id、構造化された input を含む tool_use コンテンツブロックとともに返されます。コードがツールを実行し、その後、呼び出しの id と一致する tool_use_id を持つ tool_result ブロックで結果を送り返します。
# リング1:単一ツール、単一ターン。
import json
import anthropic
# クライアントを作成します。環境変数からANTHROPIC_API_KEYを読み取ります。
client = anthropic.Anthropic()
# ツールを1つ定義します。input_schemaは、Claudeがこのツールを呼び出す際に
# 渡すべき引数を記述するJSON 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 [email protected] and [email protected] 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 [email protected] and [email protected] 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': ['[email protected]', '[email protected]']}
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がカレンダーの結果を待っているためです。結果を送信した後、2番目の stop_reason は end_turn となり、コンテンツはユーザー向けの自然言語になります。
リング1では、Claudeがツールを正確に1回だけ呼び出すことを前提としていました。実際のタスクでは複数回の呼び出しが必要になることがよくあります。Claudeはイベントを作成し、確認を読み取り、その後別のイベントを作成するかもしれません。解決策は、stop_reason が "tool_use" でなくなるまでツールを実行し結果をフィードバックし続ける while ループです。
もう1つの変更点は会話履歴です。リクエストごとに 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-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がタスクをどのように分解するかによって、ループは1回または複数回実行される可能性があります。コードは事前にそれを知る必要がなくなりました。
エージェントが1つの機能しか持たないことはほとんどありません。2つ目のツール list_calendar_events を追加して、Claudeが新しいものを作成する前に既存のスケジュールを確認できるようにします。
Claudeが複数の独立したツール呼び出しを行う必要がある場合、単一のレスポンスで複数の tool_use ブロックを返すことがあります。ループはそれらすべてを処理し、すべての結果を1つのユーザーメッセージにまとめて送り返す必要があります。最初のブロックだけでなく、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":
# 1つのレスポンスに複数のtool_useブロックが含まれる場合があります。
# すべて処理し、結果をまとめて1つのユーザーメッセージで返します。
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.並行実行と順序保証の詳細については、並列ツール使用を参照してください。
ツールは失敗します。カレンダー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はフラグとエラーテキストを確認し、それに応じて対応します。エラーハンドリングの完全なリファレンスについては、ツール呼び出しの処理を参照してください。
リング2から4では、同じループを手書きで実装しました。APIを呼び出し、stop_reason を確認し、ツールを実行し、結果を追加し、繰り返す。Tool Runnerはこれを代わりに行います。各ツールを関数として定義し、リストを tool_runner に渡し、ループが完了したら最終メッセージを取得します。エラーのラッピング、結果のフォーマット、会話の管理は内部で処理されます。
各SDKは、通常の関数を実行可能なツールに変換し、そのシグネチャから入力スキーマを導出するヘルパーを提供します。以下のタブは各言語の慣用的な形式を示しています。
# リング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と同一です。違いはコードにあります。行数はおよそ半分になり、手動のループはなく、スキーマは実装のすぐ隣に配置されます。
単一のハードコードされたツール呼び出しから始めて、複数のツール、並列呼び出し、エラーを処理する本番環境に近い形のエージェントを完成させ、その後すべてをTool Runnerに集約しました。その過程で、ツール使用プロトコルのすべての要素を確認しました。tool_use ブロック、tool_result ブロック、tool_use_id のマッチング、stop_reason のチェック、is_error のシグナリングです。
Was this page helpful?