Dieses Tutorial baut einen Kalenderverwaltungs-Agenten in fünf konzentrischen Ringen auf. Jeder Ring ist ein vollständiges, lauffähiges Programm, das genau ein Konzept zum vorherigen Ring hinzufügt. Am Ende wirst du die agentische Schleife von Hand geschrieben und sie dann durch die Tool-Runner-SDK-Abstraktion ersetzt haben.
Das Beispiel-Tool ist create_calendar_event. Sein Schema verwendet verschachtelte Objekte, Arrays und optionale Felder, sodass du siehst, wie Claude mit realistischen Eingabestrukturen umgeht, statt nur mit einem einzelnen flachen String.
Das kleinstmögliche Tool-nutzende Programm: ein Tool, eine Benutzernachricht, ein Tool-Aufruf, ein Ergebnis. Der Code ist ausführlich kommentiert, damit du jede Zeile dem Tool-Nutzungs-Lebenszyklus zuordnen kannst.
Die Anfrage sendet ein tools-Array zusammen mit der Benutzernachricht. Wenn Claude feststellt, dass ein Tool-Aufruf erforderlich ist, kommt die Antwort mit stop_reason: "tool_use" und einem tool_use-Content-Block zurück, der den Tool-Namen, eine eindeutige id und den strukturierten input enthält. Dein Code führt das Tool aus und sendet dann das Ergebnis in einem tool_result-Block zurück, dessen tool_use_id mit der id aus dem Aufruf übereinstimmt.
# Ring 1: Einzelnes Tool, einzelner Turn.
import json
import anthropic
# Erstelle einen Client. Er liest ANTHROPIC_API_KEY aus der Umgebung.
client = anthropic.Anthropic()
# Definiere ein Tool. Das input_schema ist ein JSON-Schema-Objekt, das
# die Argumente beschreibt, die Claude beim Aufruf dieses Tools übergeben soll.
# Dieses Schema enthält verschachtelte Objekte (recurrence), Arrays (attendees)
# und optionale Felder – näher an realen Tools als ein flaches 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"],
},
}
]
# Sende die Anfrage des Nutzers zusammen mit der Tool-Definition. Claude entscheidet
# anhand der Anfrage und der Tool-Beschreibung, ob es das Tool aufruft.
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.",
}
],
)
# Wenn Claude ein Tool aufruft, hat die Antwort den stop_reason "tool_use"
# und das content-Array enthält neben etwaigem Text einen tool_use-Block.
print(f"stop_reason: {response.stop_reason}")
# Finde den tool_use-Block. Eine Antwort kann Textblöcke vor dem tool_use-Block
# enthalten, also durchsuche das content-Array, statt eine Position anzunehmen.
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}")
# Führe das Tool aus. In einem echten System würde dies deine Kalender-API aufrufen.
# Hier ist das Ergebnis hartcodiert, damit das Beispiel in sich geschlossen bleibt.
result = {"event_id": "evt_123", "status": "created"}
# Sende das Ergebnis zurück. Der tool_result-Block gehört in eine user-Nachricht und
# seine tool_use_id muss mit der id aus dem obigen tool_use-Block übereinstimmen. Die
# vorherige Antwort des Assistenten wird mitgesendet, damit Claude den vollen Verlauf hat.
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),
}
],
},
],
)
# Mit dem Tool-Ergebnis in der Hand erzeugt Claude eine finale Antwort in natürlicher
# Sprache und stop_reason wird zu "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)Was zu erwarten ist
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.Der erste stop_reason ist tool_use, weil Claude auf das Kalenderergebnis wartet. Nachdem du das Ergebnis gesendet hast, ist der zweite stop_reason end_turn und der Inhalt ist natürliche Sprache für den Benutzer.
Ring 1 ging davon aus, dass Claude das Tool genau einmal aufruft. Echte Aufgaben benötigen oft mehrere Aufrufe: Claude könnte ein Ereignis erstellen, die Bestätigung lesen und dann ein weiteres erstellen. Die Lösung ist eine while-Schleife, die weiterhin Tools ausführt und Ergebnisse zurückgibt, bis stop_reason nicht mehr "tool_use" ist.
Die andere Änderung ist der Gesprächsverlauf. Anstatt das messages-Array bei jeder Anfrage von Grund auf neu aufzubauen, führe eine laufende Liste und hänge daran an. Jeder Durchlauf sieht den vollständigen vorherigen Kontext.
# Ring 2: Die agentische Schleife.
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}"}
# Bewahre den gesamten Gesprächsverlauf in einer Liste auf, damit jeder Turn den vorherigen Kontext sieht.
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,
)
# Schleife, bis Claude keine Tools mehr anfordert. Jede Iteration führt das angeforderte
# Tool aus, hängt das Ergebnis an den Verlauf an und bittet Claude fortzufahren.
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)Was zu erwarten ist
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.Die Schleife kann einmal oder mehrmals laufen, je nachdem, wie Claude die Aufgabe aufteilt. Dein Code muss das nicht mehr im Voraus wissen.
Agenten haben selten nur eine Fähigkeit. Füge ein zweites Tool hinzu, list_calendar_events, damit Claude den bestehenden Zeitplan prüfen kann, bevor etwas Neues erstellt wird.
Wenn Claude mehrere unabhängige Tool-Aufrufe durchführen muss, kann es mehrere tool_use-Blöcke in einer einzigen Antwort zurückgeben. Deine Schleife muss alle verarbeiten und alle Ergebnisse zusammen in einer Benutzernachricht zurücksenden. Iteriere über jeden tool_use-Block in response.content, nicht nur über den ersten.
# Ring 3: Mehrere Tools, parallele Aufrufe.
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":
# Eine einzelne Antwort kann mehrere tool_use-Blöcke enthalten. Verarbeite alle
# und gib alle Ergebnisse zusammen in einer User-Nachricht zurück.
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)Was zu erwarten ist
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.Mehr zu gleichzeitiger Ausführung und Reihenfolgegarantien findest du unter Parallele Tool-Nutzung.
Tools schlagen fehl. Eine Kalender-API könnte ein Ereignis mit zu vielen Teilnehmern ablehnen, oder ein Datum könnte fehlerhaft formatiert sein. Wenn ein Tool einen Fehler auslöst, sende die Fehlermeldung mit is_error: true zurück, anstatt abzustürzen. Claude liest den Fehler und kann es mit korrigierter Eingabe erneut versuchen, den Benutzer um Klärung bitten oder die Einschränkung erklären.
# Ring 4: Fehlerbehandlung.
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:
# Signalisiere den Fehler, damit Claude es erneut versuchen oder um Klärung bitten kann.
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)Was zu erwarten ist
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.Das is_error-Flag ist der einzige Unterschied zu einem erfolgreichen Ergebnis. Claude sieht das Flag und den Fehlertext und reagiert entsprechend. Siehe Tool-Aufrufe verarbeiten für die vollständige Referenz zur Fehlerbehandlung.
In den Ringen 2 bis 4 wurde dieselbe Schleife von Hand geschrieben: API aufrufen, stop_reason prüfen, Tools ausführen, Ergebnisse anhängen, wiederholen. Der Tool Runner erledigt das für dich. Definiere jedes Tool als Funktion, übergib die Liste an tool_runner und rufe die finale Nachricht ab, sobald die Schleife abgeschlossen ist. Fehler-Wrapping, Ergebnisformatierung und Gesprächsverwaltung werden intern gehandhabt.
Jedes SDK bietet einen Helfer, der eine gewöhnliche Funktion in ein ausführbares Tool verwandelt und das Eingabeschema aus ihrer Signatur ableitet; die Tabs unten zeigen die idiomatische Form für jede Sprache.
# Ring 5: Die Tool-Runner-SDK-Abstraktion.
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)Was zu erwarten ist
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.Die Ausgabe ist identisch mit Ring 3. Der Unterschied liegt im Code: ungefähr halb so viele Zeilen, keine manuelle Schleife, und das Schema befindet sich direkt neben der Implementierung.
Du hast mit einem einzelnen fest codierten Tool-Aufruf begonnen und mit einem produktionsnahen Agenten geendet, der mehrere Tools, parallele Aufrufe und Fehler handhabt, und hast dann all das in den Tool Runner überführt. Unterwegs hast du jeden Teil des Tool-Nutzungs-Protokolls gesehen: tool_use-Blöcke, tool_result-Blöcke, tool_use_id-Zuordnung, stop_reason-Prüfung und is_error-Signalisierung.
Schema-Spezifikation und Best Practices.
Die vollständige SDK-Abstraktionsreferenz.
Behebe häufige Tool-Nutzungs-Fehler.
Was this page helpful?