Tutorial: Einen Tool-nutzenden Agenten bauen
Eine geführte Schritt-für-Schritt-Anleitung von einem einzelnen Tool-Aufruf bis zu einer produktionsreifen agentischen Schleife.
Dieses Tutorial baut einen Agenten zur Kalenderverwaltung in fünf konzentrischen Ringen. Jeder Ring ist ein vollständiges, lauffähiges Programm, das dem vorherigen Ring genau ein Konzept hinzufügt. Am Ende wirst du die „agentic loop“ (agentische Schleife) von Hand geschrieben und sie anschließend 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 Eingabeformen umgeht statt nur mit einem einzelnen flachen String.
Ring 1: Ein Tool, ein Turn
Das kleinstmögliche Tool-nutzende Programm: ein Tool, eine Benutzernachricht, ein Tool-Aufruf, ein Ergebnis. Der Code ist ausführlich kommentiert, sodass du jede Zeile dem Lebenszyklus der Tool-Nutzung zuordnen kannst.
Die Anfrage sendet ein tools-Array zusammen mit der Benutzernachricht. Wenn Claude feststellt, dass ein Tool-Aufruf nötig 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-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.",
}
],
)
# 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-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),
}
],
},
],
)
# 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 dich erwartet
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.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 2: Die agentische Schleife
Ring 1 ging davon aus, dass Claude das Tool genau einmal aufruft. Echte Aufgaben erfordern oft mehrere Aufrufe: Claude könnte einen Termin erstellen, die Bestätigung lesen und dann einen weiteren erstellen. Die Lösung ist eine while-Schleife, die so lange 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 fortlaufende Liste und hänge daran an. Jeder Turn 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}"}
# Speichere den gesamten Gesprächsverlauf in einer Liste, 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, starting Monday, March 30, 2026. Invite the whole team: alice@example.com, bob@example.com, carol@example.com.",
}
]
response = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
messages=messages,
)
# Wiederhole, 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-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 dich erwartet
I've set up your weekly team standup for the next 4 Mondays at 9am with Alice, Bob, and Carol invited.Die Schleife läuft möglicherweise einmal oder mehrmals, je nachdem, wie Claude die Aufgabe zerlegt. Dein Code muss das nicht mehr im Voraus wissen.
Ring 3: Mehrere Tools, parallele Aufrufe
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 es etwas Neues erstellt.
Wenn Claude mehrere unabhängige Tool-Aufrufe durchführen muss, gibt es möglicherweise mehrere tool_use-Blöcke in einer einzigen Antwort zurück. Deine Schleife muss alle davon 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 on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
]
response = client.messages.create(
model="claude-opus-5-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-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 dich erwartet
I checked your calendar for Monday, March 30 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.
Ring 4: Fehlerbehandlung
Tools schlagen fehl. Eine Kalender-API könnte einen Termin 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 a one-hour all-hands on Monday, March 30, 2026 at 10am with everyone: " + ", ".join(f"user{i}@example.com" for i in range(15)),
}
]
response = client.messages.create(
model="claude-opus-5-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:
# Fehler signalisieren, damit Claude es erneut versuchen oder nachfragen 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-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 dich erwartet
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. Die vollständige Referenz zur Fehlerbehandlung findest du unter Tool-Aufrufe verarbeiten.
Ring 5: Die Tool-Runner-SDK-Abstraktion
Die Ringe 2 bis 4 haben dieselbe Schleife von Hand geschrieben: die 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 client.beta.messages.tool_runner() und rufe die finale Nachricht ab, sobald die Schleife abgeschlossen ist. Fehlerbehandlung, Ergebnisformatierung und Gesprächsverwaltung werden intern übernommen.
Jedes SDK stellt einen Helfer bereit, 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-5",
max_tokens=1024,
tools=[create_calendar_event, list_calendar_events],
messages=[
{
"role": "user",
"content": "Check what I have on Monday, March 30, 2026, then schedule a one-hour planning session that day that avoids any conflicts.",
}
],
).until_done()
for block in final_message.content:
if block.type == "text":
print(block.text)Was dich erwartet
I checked your calendar for Monday, March 30 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: etwa halb so viele Zeilen, keine manuelle Schleife, und das Schema steht direkt neben der Implementierung.
Was du gebaut hast
Du hast mit einem einzelnen hartcodierten Tool-Aufruf begonnen und bist bei einem produktionsnahen Agenten gelandet, der mehrere Tools, parallele Aufrufe und Fehler handhabt, und hast dann all das im Tool Runner zusammengefasst. Unterwegs hast du jeden Bestandteil des Tool-Nutzungs-Protokolls gesehen: tool_use-Blöcke, tool_result-Blöcke, tool_use_id-Abgleich, stop_reason-Prüfung und is_error-Signalisierung.
Nächste Schritte
Schema-Spezifikation und Best Practices.
Die vollständige Referenz zur SDK-Abstraktion.
Häufige Fehler bei der Tool-Nutzung beheben.
Was this page helpful?