Tool-Nutzungsmuster: Zuverlässige Agent-Tool-Schnittstellen entwickeln
Dein Agent hat ein Tool aufgerufen und einen 40-zeiligen JSON-Blob zurückbekommen — rohe API-Antwort, verschachtelte Objekte, Fehlercodes tief vergraben in einem status-Feld. Das Modell las es, wählte einen plausibel wirkenden Wert und machte weiter. Der Wert war falsch. Drei Schritte später schrieb der Agent zuversichtlich einen Bericht auf Basis falscher Daten.
Das Tool funktionierte. Die Schnittstelle versagte.
Tool-Nutzung ist der Mechanismus, der ein Sprachmodell in einen Agenten verwandelt. Jede Fähigkeit deines Agenten — Datenbanksuchen, Dateien schreiben, APIs aufrufen, Dienste abfragen — kommt über eine Tool-Schnittstelle an. Ist die Schnittstelle schlecht gestaltet, trifft das Modell schlechtere Entscheidungen, selbst wenn der zugrundeliegende Dienst korrekt funktioniert. Dieser Leitfaden behandelt fünf Muster für präzise, zuverlässige und produktionsreife Tool-Schnittstellen.
Voraussetzungen: Kenntnisse in Python und der Claude API. Für Hintergrundwissen zu MCP als Tool-Transport-Schicht, siehe Deinen ersten MCP-Server bauen.
Warum Schnittstellendesign wichtig ist
Wenn ein Agent ein Tool auswählt und nutzt, trifft er zwei Entscheidungen:
- Welches Tool aufzurufen — gesteuert durch
nameunddescriptiondes Tools - Welche Argumente zu übergeben — gesteuert durch das
input_schemades Tools
Mehrdeutige Beschreibungen führen zu falscher Tool-Auswahl. Lockere Schemas erlauben dem Modell, fehlerhafte Eingaben zu übergeben. Unstrukturierte Ergebnisse zwingen das Modell zu raten. Die meisten Agent-Bugs liegen nicht im Reasoning — sie liegen an der Tool-Grenze.
Muster 1: Schema-First-Design
Schreibe das JSON-Schema, bevor du die Implementierung schreibst. Ein strenges Schema schränkt das Modellverhalten in der Eingabephase ein — bevor irgendetwas ausgeführt wird.
import anthropic
client = anthropic.Anthropic()
tools = [ { "name": "search_products", "description": ( "Durchsucht den Produktkatalog nach Stichwörtern. " "Gibt eine Liste passender Produkte mit IDs, Namen und Preisen zurück. " "Verwende es, wenn der Benutzer Produkte finden oder durchstöbern möchte." ), "input_schema": { "type": "object", "properties": { "query": { "type": "string", "description": "Suchbegriffe" }, "category": { "type": "string", "enum": ["electronics", "clothing", "food", "home", "all"], "description": "Produktkategorie zum Filtern. Verwende 'all' wenn nicht angegeben." }, "max_results": { "type": "integer", "minimum": 1, "maximum": 20, "description": "Anzahl der zurückzugebenden Ergebnisse. Standard: 5" } }, "required": ["query", "category"] } }]
response = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, tools=tools, messages=[{"role": "user", "content": "Finde Elektronik unter 100€"}])Schema-Regeln, die Fehler reduzieren:
- Verwende
enumfür Felder mit festen gültigen Werten - Setze
minimum/maximumauf numerischen Feldern - Markiere Felder nur als
required, wenn das Tool ohne sie wirklich nicht laufen kann - Schreibe Beschreibungen aus Sicht des Modells: “Verwende es, wenn…”
Wann verwenden: Bei jeder Tool-Definition.
Muster 2: Strukturierte Tool-Ergebnisse
Gib typisierte, maschinenlesbare Ergebnisse zurück. Gib niemals rohe API-Antworten zurück.
import jsonfrom dataclasses import dataclass, asdictfrom typing import Any, Optional
@dataclassclass ToolResult: success: bool data: Optional[Any] = None error: Optional[str] = None
def to_content(self) -> str: return json.dumps(asdict(self))
def search_products( query: str, category: str, max_results: int = 5,) -> ToolResult: try: raw_results = _query_database(query, category, limit=max_results) products = [ {"id": r["product_id"], "name": r["title"], "price": r["price_usd"]} for r in raw_results ] return ToolResult(success=True, data={"products": products, "count": len(products)}) except ConnectionError as e: return ToolResult(success=False, error=f"Datenbank nicht erreichbar: {e}") except Exception as e: return ToolResult(success=False, error=f"Suche fehlgeschlagen: {type(e).__name__}: {e}")
def _query_database(query, category, limit): return []Das konsistente {success, data, error}-Envelope bedeutet, dass das Modell immer weiß, wo es suchen muss. Für die elegante Fehlerbehandlung, siehe Agent-Fehlerwiederherstellungsmuster.
Muster 3: Parallele Tool-Aufrufe
Claude kann in einer einzigen Antwort mehrere Tools anfordern. Verarbeite sie parallel statt sequenziell.
from concurrent.futures import ThreadPoolExecutor, as_completed
TOOL_REGISTRY = { "search_products": search_products,}
def dispatch_tool(name: str, inputs: dict) -> ToolResult: handler = TOOL_REGISTRY.get(name) if not handler: return ToolResult(success=False, error=f"Unbekanntes Tool: {name}") return handler(**inputs)
def process_tool_calls(response: anthropic.types.Message) -> list[dict]: tool_uses = [ block for block in response.content if block.type == "tool_use" ] if not tool_uses: return []
def execute(tool_use): result = dispatch_tool(tool_use.name, tool_use.input) return { "type": "tool_result", "tool_use_id": tool_use.id, "content": result.to_content(), }
with ThreadPoolExecutor(max_workers=len(tool_uses)) as executor: futures = {executor.submit(execute, tu): tu for tu in tool_uses} results = [] for future in as_completed(futures): results.append(future.result())
return results
def run_agent(user_message: str) -> str: messages = [{"role": "user", "content": user_message}]
while True: response = client.messages.create( model="claude-sonnet-4-6", max_tokens=4096, tools=tools, messages=messages, )
if response.stop_reason == "end_turn": for block in response.content: if hasattr(block, "text"): return block.text return ""
if response.stop_reason == "tool_use": tool_results = process_tool_calls(response) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results}) else: break
return ""Für die Orchestrierung mehrerer Agenten, die jeweils Tools aufrufen, siehe Multi-Agent-Muster.
Muster 4: Sicherer Tool-Aufruf-Wrapper
Lass Tool-Ausnahmen niemals unbehandelt den Agent-Loop erreichen.
import signal
def timeout_handler(signum, frame): raise TimeoutError("Tool-Ausführung hat das Zeitlimit überschritten")
def safe_tool_call( name: str, inputs: dict, timeout_seconds: int = 30,) -> ToolResult: """ Führt ein Tool mit Zeitlimit aus und fängt alle Ausnahmen ab. Gibt immer ein ToolResult zurück — wirft niemals eine Ausnahme. """ signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout_seconds) try: return dispatch_tool(name, inputs) except TimeoutError: return ToolResult( success=False, error=f"Tool '{name}' hat nach {timeout_seconds}s das Zeitlimit überschritten" ) except Exception as e: return ToolResult( success=False, error=f"Tool '{name}' hat {type(e).__name__} ausgelöst: {e}" ) finally: signal.alarm(0)Wenn ein Tool success: false zurückgibt, kann das Modell entscheiden, ob es erneut versuchen, eine Alternative ausprobieren oder den Fehler melden soll. Für umfassendere Retry-Strategien, siehe Fehlerwiederherstellungsmuster.
Muster 5: Ergebnisvalidierung und Kürzung
Validiere Tool-Ergebnisse, bevor du sie an das Modell zurückgibst.
MAX_TOOL_RESULT_CHARS = 8000
def validate_result(result: ToolResult, expected_keys: list[str]) -> ToolResult: if not result.success or not isinstance(result.data, dict): return result missing = [k for k in expected_keys if k not in result.data] if missing: return ToolResult( success=False, error=f"Tool-Antwort fehlt erwartete Felder: {missing}" ) return result
def truncate_result(result: ToolResult) -> ToolResult: content = result.to_content() if len(content) <= MAX_TOOL_RESULT_CHARS: return result
truncated_data = { "truncated": True, "chars_omitted": len(content) - MAX_TOOL_RESULT_CHARS, "content": content[:MAX_TOOL_RESULT_CHARS], } return ToolResult( success=result.success, data=truncated_data, error="Ergebnis gekürzt — zu groß für das Kontextfenster", )
def safe_tool_call_validated( name: str, inputs: dict, expected_keys: list[str] | None = None,) -> ToolResult: result = safe_tool_call(name, inputs) if expected_keys: result = validate_result(result, expected_keys) result = truncate_result(result) return resultFür die Beobachtung und das Debuggen dieser Fehler in der Produktion, siehe Debugging und Observability.
Häufige Fehler
Fehler 1: Rohe API-Antworten zurückgeben
Das Modell erhält ein verschachteltes Objekt mit 30 Feldern, die meisten irrelevant. Es wählt das falsche.
Lösung: Forme die Antwort vor der Rückgabe. Gib nur zurück, was das Modell für seine nächste Entscheidung braucht.
Fehler 2: Tools mit Nebenwirkungen ohne Bestätigung
Ein Tool, das eine E-Mail sendet oder einen Eintrag löscht, sollte nicht stillschweigend ausgeführt werden.
Lösung: Für irreversible Aktionen verwende ein Zwei-Tool-Muster: plan_email gibt eine Vorschau zurück, send_email sendet tatsächlich.
Fehler 3: Überlappende Tool-Verantwortlichkeiten
Zwei Tools, die ähnliche Dinge tun, zwingen das Modell zum Raten.
Lösung: Jedes Tool sollte einen eindeutigen, nicht überlappenden Zweck haben.
Fehler 4: Kein Timeout bei externen Tools
Ein langsamer Drittanbieter-API-Aufruf blockiert den gesamten Agent-Loop auf unbestimmte Zeit.
Lösung: Setze immer ein Timeout (Muster 4).
Produktions-Checkliste
- Jedes Tool hat eine Beschreibung aus Modellsicht (“Verwende es, wenn…”)
- Enum-Felder für alle Eingaben mit festen Werten
- Jedes Tool gibt
{success, data, error}zurück — niemals rohe Antworten - Tool-Aufrufe in
safe_tool_calleingewickelt - Parallele Ausführung für Multi-Tool-Antworten
- Timeout für jedes externe Tool gesetzt
- Ergebniskürzung für variable Nutzlastgrößen
- Validierung für Tools, die externe APIs aufrufen
Nächste Schritte
- Beginne mit dem Schema — schreibe dein
input_schemavor dem Funktionskörper - Füge den
ToolResult-Wrapper zu jedem bestehenden Tool hinzu - Integriere
safe_tool_callum deinen Agent-Loop abzusichern - Richte Ergebnisvalidierung ein für Tools, die externe APIs aufrufen
Verwandte Leitfäden:
- Deinen ersten MCP-Server bauen
- Multi-Agent-Muster
- Agent-Gedächtnissysteme
- Agent-Fehlerwiederherstellungsmuster
- Debugging und Observability