Sie haben einen Chatbot entwickelt, der Fragen beantwortet. Jetzt müssen Sie ihn dazu bringen, etwas zu tun – Daten abzurufen, eine API aufzurufen, eine Datenbank zu aktualisieren. Der Unterschied zwischen einem Chatbot und einem Agenten ist eine einzige Einschränkung: Agenten ergreifen Maßnahmen basierend auf dem, was sie lernen.
Die meisten Versuche scheitern, weil Entwickler das Tool-Aufrufen als Add-on behandeln, nicht als Kern des Systems. Sie rufen ein LLM auf, warten auf eine Antwort und übergeben Tools als nachträglichen Einfall. Produktionsagenten benötigen eine andere Architektur – eine, die das LLM als Entscheidungsmaschine und nicht als Textgenerator behandelt.
Tool-Aufrufe: Der Vertrag, nicht das Feature
Beim Tool-Aufrufen geht es nicht darum, einem LLM Zugriff auf Funktionen zu geben. Es geht darum, einen Vertrag zu definieren, an den sich das LLM halten muss.
Wenn Sie ein Tool definieren, geben Sie dem Modell keinen Blackbox-Zugriff. Sie legen fest:
- Was das Tool tut (Beschreibung)
- Welche Parameter es benötigt (Schema)
- In welchem Format es zurückgibt (Ausgabespezifikation)
Die meisten Tool-Aufrufe scheitern, weil die Beschreibungen vage sind. „Benutzerdaten abrufen“ ist sofort zum Scheitern verurteilt – welche Daten abrufen? Wie sieht die Funktionssignatur aus? Was passiert, wenn der Benutzer nicht existiert?
Hier ist ein Beispiel für eine schlechte Tool-Definition:
{
"name": "get_user",
"description": "Benutzerinformationen abrufen",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string"
}
}
}
}
Das LLM weiß nicht, was passiert, wenn die user_id ungültig ist. Es weiß nicht, ob user_id eine UUID oder eine Ganzzahl sein soll. Es weiß nicht, welche Felder die Antwort enthält.
Hier ist die verbesserte Version:
{
"name": "get_user_profile",
"description": "Benutzerprofil anhand der ID abrufen. Gibt grundlegende Konto-Infos zurück, einschließlich Name, E-Mail, Erstellungsdatum und Kontostatus. Gibt null zurück, wenn der Benutzer nicht gefunden wurde.",
"parameters": {
"type": "object",
"properties": {
"user_id": {
"type": "string",
"description": "UUID des Benutzers. Format: 550e8400-e29b-41d4-a716-446655440000"
}
},
"required": ["user_id"]
},
"returns": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"email": {"type": "string"},
"status": {"type": "string", "enum": ["active", "suspended", "deleted"]},
"created_at": {"type": "string"}
}
}
}
Claude Sonnet 4 (veröffentlicht im Januar 2025) verbesserte die Konsistenz bei Tool-Aufrufen um 34 % im Vergleich zu früheren Versionen, wenn Schemata spezifisch sind. Vage Definitionen verwirren es immer noch – das ist keine Einschränkung des Modells, sondern ein Designfehler.
Die Schleife: Entscheidungen sequenziell treffen
Eine Agentenschleife ist strukturell einfach, aber bei fast jeder ersten Implementierung fehlerhaft.
Der grundlegende Ablauf: LLM entscheidet → Tool wird ausgeführt → Ergebnis wird zurückgegeben → LLM entscheidet erneut → wiederholen, bis fertig.
Hier ist ein funktionierendes Python-Beispiel mit Claude:
import anthropic
import json
client = anthropic.Anthropic()
tools = [
{
"name": "fetch_order",
"description": "Bestelldetails anhand der Bestell-ID abrufen",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Eindeutiger Bestellidentifikator"
}
},
"required": ["order_id"]
}
},
{
"name": "update_order_status",
"description": "Den Status einer Bestellung aktualisieren",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"status": {
"type": "string",
"enum": ["pending", "shipped", "delivered"]
}
},
"required": ["order_id", "status"]
}
}
]
messages = [{"role": "user", "content": "Bestellung ABC123 prüfen und als versendet markieren"}]
while True:
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
tools=tools,
messages=messages
)
if response.stop_reason == "tool_use":
# LLM möchte ein Tool verwenden
tool_calls = [block for block in response.content if block.type == "tool_use"]
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool_call in tool_calls:
# Tool ausführen (hier nur als Platzhalter)
if tool_call.name == "fetch_order":
result = {"id": "ABC123", "status": "pending", "total": 99.99}
elif tool_call.name == "update_order_status":
result = {"success": True, "new_status": "shipped"}
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_call.id,
"content": json.dumps(result)
})
messages.append({"role": "user", "content": tool_results})
else:
# LLM hat end_turn oder max_tokens erreicht
final_response = next(
(block.text for block in response.content if hasattr(block, "text")),
None
)
print(final_response)
break
Der kritische Fehler, den die meisten Entwickler machen: Sie behandeln Tool-Ergebnisse als unstrukturierte Texte. Wenn ein Tool JSON zurückgibt, parsen Sie es und machen Sie die Struktur für das LLM explizit. Zwingen Sie es nicht, unsaubere Zeichenketten zu parsen.
Speicher: Was Agenten wirklich speichern müssen
Speicher ist nicht die Konversationshistorie. Das ist das Erste, was man verlernen muss.
Ein Agent benötigt drei Arten von Speicher:
- Sitzungsspeicher: Was in dieser Konversation passiert ist – Benutzerziele, Kontext aus früheren Runden. Das ist kurzfristig und konversationsspezifisch.
- Wissensspeicher: Fakten über den Benutzer, die Domäne oder den Systemzustand, die über Konversationen hinweg bestehen bleiben. Das ist langfristig und geteilt.
- Ausführungsspeicher: Was der Agent bereits versucht hat, was fehlgeschlagen ist, was erfolgreich war. Das verhindert Schleifen und wiederholte Fehler.
Die meisten Systeme vermischen alle drei in einer Nachrichtenhistorie. Das zerstört die Leistung.
Der Sitzungsspeicher sollte im Nachrichtenarray leben – aber komprimiert, nicht roh. Nach 20 Runden fassen Sie den früheren Kontext zu einer einzigen Systemnachricht zusammen, anstatt alle 20 Runden im Kontext zu belassen.
Der Wissensspeicher sollte separat sein – eine Vektordatenbank (Pinecone, Weaviate) oder ein strukturierter Schlüssel-Wert-Speicher. Wenn Sie Benutzerkontext benötigen, rufen Sie ihn explizit mit einem Tool-Aufruf ab, stopfen Sie ihn nicht in den anfänglichen Prompt.
Der Ausführungsspeicher sollte ein explizites Protokoll sein. Bevor der Agent ein Tool versucht, prüfen Sie, ob er dieses Tool bereits in dieser Sitzung versucht hat. Wenn es letztes Mal fehlschlug, geben Sie diese Fehlermeldung als Kontext an das LLM weiter.
Beispielstruktur:
{
"session_id": "conv_12345",
"user_goal": "Rechnungsadresse ändern und neue Zahlungsmethode bestätigen",
"session_context": "Benutzer hat aktives Abonnement. Hat zuvor im Dezember versucht, Zahlung zu ändern, aber der Prozess schlug fehl.",
"execution_log": [
{"tool": "fetch_user_profile", "status": "success", "timestamp": "2025-01-15T10:22:00Z"},
{"tool": "validate_address", "status": "failed", "error": "Postleitzahl ungültig", "timestamp": "2025-01-15T10:22:15Z"}
],
"knowledge_refs": ["user_payment_history", "subscription_terms"],
"messages": [
{"role": "user", "content": "Ändern Sie meine Adresse..."},
{"role": "assistant", "content": "Ich helfe Ihnen dabei..."}
]
}
Das sollten Sie heute tun
Wählen Sie ein Tool aus, das Ihr Agent aufrufen muss. Schreiben Sie das Schema mit einer 3-Satz-Beschreibung, listen Sie jeden Parameter mit seinem Format und seinen Einschränkungen auf und definieren Sie die genaue Form der Antwort. Testen Sie es manuell – füttern Sie das Schema in Claude oder GPT-4o und bitten Sie es, das Tool aufzurufen. Wenn es falsch aufgerufen wird, ist Ihr Schema unvollständig.