チュートリアル:ツールを使用するエージェントを構築する
単一のツール呼び出しから本番環境対応のエージェントループまでを順を追って解説するガイドです。
このチュートリアルでは、カレンダー管理エージェントを5つの同心円状のリングで構築します。各リングは完全に実行可能なプログラムであり、直前のリングにちょうど1つの概念を追加します。最後には、エージェントループを手書きで実装し、その後それを Tool Runner SDK の抽象化に置き換えることになります。
例として使用するツールは create_calendar_event です。そのスキーマはネストされたオブジェクト、配列、オプションフィールドを使用しているため、単一のフラットな文字列ではなく、現実的な入力形状を Claude がどのように扱うかを確認できます。
リング1:単一ツール、単一ターン
ツールを使用する最小限のプログラムです。1つのツール、1つのユーザーメッセージ、1つのツール呼び出し、1つの結果。コードには詳細なコメントが付いているため、各行をツール使用のライフサイクルに対応付けることができます。
リクエストはユーザーメッセージとともに tools 配列を送信します。Claude がツール呼び出しが必要だと判断すると、レスポンスは stop_reason: "tool_use" と、ツール名、一意の id、構造化された input を含む tool_use コンテンツブロックとともに返されます。コードはツールを実行し、その結果を tool_result ブロックで送り返します。このブロックの tool_use_id は呼び出し時の id と一致します。
# リング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 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 がカレンダーの結果を待っているためです。結果を送信すると、2番目の stop_reason は end_turn になり、コンテンツはユーザー向けの自然言語になります。
リング2:エージェントループ
リング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: 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 がタスクをどのように分解するかによって、ループは1回だけ実行されることもあれば、複数回実行されることもあります。コード側で事前に知っておく必要はもうありません。
リング3:複数ツール、並列呼び出し
エージェントが1つの機能しか持たないことはめったにありません。2つ目のツール list_calendar_events を追加して、Claude が新しいものを作成する前に既存のスケジュールを確認できるようにします。
Claude が複数の独立したツール呼び出しを行う必要がある場合、1つのレスポンスで複数の 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.並行実行と順序保証の詳細については、並列ツール使用を参照してください。
リング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 は、通常の関数を実行可能なツールに変換し、そのシグネチャから入力スキーマを導出するヘルパーを提供しています。以下のタブでは、各言語における慣用的な形式を示しています。
# リング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?