Ein Language Model kann Text generieren. Mehr nicht.
Es kann keine Datenbank abfragen. Es kann keine API aufrufen. Es kann keine Datei schreiben. Es ist gefangen in seiner Trainingsverteilung, isoliert von der Welt außerhalb seines Kontextfensters.
Für viele Anwendungsfälle ist das tödlich. Ein Chatbot, der keine Bestellungen nachschlagen kann, ist nutzlos. Ein Assistant, der keine E-Mails senden kann, ist ein Spielzeug. Ein Agent, der keine externen Daten abrufen kann, ist blind.
Tools lösen dieses Problem. Sie sind die Brücke zwischen der Textwelt des Modells und der strukturierten Welt der APIs, Datenbanken und Systeme.
Ein Tool ist keine Magie. Es ist eine Funktion mit explizitem Schema.
Das Schema definiert: Welche Parameter akzeptiert die Funktion? Welche Typen haben sie? Welche sind optional? Was macht die Funktion?
Dieses Schema wird dem Modell übergeben. Das Modell „sieht” die verfügbaren Tools als Teil seines Kontexts. Es kann entscheiden, ein Tool aufzurufen – aber es führt das Tool nicht selbst aus. Es generiert einen strukturierten Request: „Rufe Tool X mit Parameter Y auf.”
Der Agent oder die Runtime parst diesen Request und führt die eigentliche Funktion aus. Das Ergebnis wird zurück ins Modell gegeben. Der Loop schließt sich.
Ein Tool ist also: Eine annotierte Funktion, die vom Modell angefordert, aber von der Runtime ausgeführt wird.
Frühe LLM-Integrationen basieren auf String-Parsing. Das Modell schreibt: „TOOL: calculator, PARAMETER: 5 + 3”. Die Anwendung parst diesen String, extrahiert Tool-Name und Parameter, führt die Funktion aus.
Das ist fragil. Ein Tippfehler im Tool-Namen, ein falsch formatierter Parameter, ein fehlendes Trennzeichen – und der Parse schlägt fehl.
Moderne Modelle unterstützen Function Calling nativ. Sie generieren strukturierte JSON-Objekte, die Tool-Aufrufe beschreiben. Keine String-Extraktion. Keine regulären Ausdrücke. Nur validiertes JSON gegen ein definiertes Schema.
Das ist robuster, performanter und erlaubt komplexere Parameter-Strukturen.
OpenAI, Anthropic, Google und andere Anbieter haben Function Calling in ihre APIs integriert. LangChain abstrahiert diese Unterschiede und bietet ein einheitliches Interface.
Ein Tool besteht aus vier Komponenten:
Name: Ein Identifier, der das Tool eindeutig
kennzeichnet. Standardmäßig der Funktionsname, aber überschreibbar. Der
Name sollte beschreibend sein: get_weather ist besser als
tool1.
Beschreibung: Ein Text, der dem Modell erklärt, wann und wie das Tool zu nutzen ist. Diese Beschreibung ist kritisch. Sie bestimmt, ob das Modell das Tool korrekt auswählt. Eine gute Beschreibung ist präzise, konkret und fokussiert auf den Use Case.
Input-Schema: Eine formale Spezifikation der Parameter. Typ, Name, Optionalität, Constraints. Generiert aus Type Hints oder explizit als Pydantic-Modell definiert. Das Schema ist das, was dem Modell übergeben wird.
Implementation: Die eigentliche Funktion. Hier geschieht die Arbeit: API-Calls, Datenbankzugriffe, Berechnungen. Die Implementation ist vom Modell unsichtbar. Es sieht nur Name, Beschreibung und Schema.
Diese Trennung ist elegant. Das Modell entscheidet, ob und wie ein Tool aufgerufen wird. Die Runtime entscheidet, was das Tool tut.
LangChain bietet den @tool-Dekorator als Standard-Weg
zur Tool-Definition. Er folgt dem Prinzip: Maximale Funktionalität mit
minimalem Code.
from langchain.tools import tool
@tool
def search_database(query: str, limit: int = 10) -> str:
"""Durchsucht die Kundendatenbank nach passenden Einträgen.
Args:
query: Suchbegriffe
limit: Maximale Anzahl der Ergebnisse
"""
return f"Gefunden: {limit} Ergebnisse für '{query}'"Was passiert hier?
Der Dekorator extrahiert alle nötigen Informationen aus der Funktion
selbst: - Name: search_database (vom
Funktionsnamen) - Beschreibung: Der Docstring (erste
Zeile) - Schema: Aus den Type Hints
(query: str, limit: int = 10)
Das ist deklarativ. Man beschreibt, was das Tool ist, nicht wie es registriert wird.
Type Hints sind mandatory. Ohne sie kann LangChain kein Schema generieren. Das ist eine bewusste Design-Entscheidung: Tools müssen typisiert sein.
Für einfache Tools reichen Type Hints. Ein String-Parameter, ein Integer-Parameter, fertig.
Für komplexere Szenarien braucht man mehr Kontrolle. Enumerationen, verschachtelte Objekte, Validierungsregeln. Hier kommen Pydantic-Modelle ins Spiel.
Ein Pydantic-Modell ist ein typisiertes Schema mit eingebauter Validierung. Man definiert Felder mit Typen, Defaults, Beschreibungen und Constraints:
from pydantic import BaseModel, Field
from typing import Literal
class WeatherInput(BaseModel):
"""Input-Schema für Wetterabfragen."""
location: str = Field(description="Stadt oder Koordinaten")
units: Literal["celsius", "fahrenheit"] = Field(
default="celsius",
description="Temperatureinheit"
)
include_forecast: bool = Field(
default=False,
description="5-Tage-Vorhersage einbeziehen"
)Dieses Modell wird an das Tool gebunden:
@tool(args_schema=WeatherInput)
def get_weather(location: str, units: str = "celsius", include_forecast: bool = False) -> str:
"""Liefert aktuelle Wetterdaten und optionale Vorhersage."""
temp = 22 if units == "celsius" else 72
result = f"Aktuelles Wetter in {location}: {temp}°{units[0].upper()}"
if include_forecast:
result += "\nNächste 5 Tage: Sonnig"
return resultDas Modell definiert das Interface. Die Funktion implementiert die Logik. Das Modell sieht nur das Schema.
Das ermöglicht präzise Kontrolle:
Literal["celsius", "fahrenheit"] schränkt den Parameter auf
genau diese zwei Werte ein. Das Modell kann keine anderen Optionen
wählen.
Ein häufiges Missverständnis: Das Tool-Schema und die Funktionssignatur müssen nicht identisch sein.
Das Schema definiert, was das Modell sieht. Die Signatur definiert, was die Funktion erwartet.
Warum sollte man das trennen? Weil manche Parameter nicht vom Modell kommen, sondern aus dem Runtime-Kontext.
Beispiel: Ein Tool, das Nutzerdaten abruft. Die User-ID sollte nicht vom Modell gewählt werden (das wäre ein Sicherheitsrisiko), sondern aus dem Session-Context stammen.
Die Lösung: Man übergibt die User-ID als Runtime-Parameter, der nicht im Schema erscheint.
Das führt uns zum zentralen Konzept der modernen LangChain-Tool-API: ToolRuntime.
ToolRuntime ist ein Parameter-Typ mit besonderer
Bedeutung. Wenn eine Tool-Funktion einen
ToolRuntime-Parameter hat, wird er automatisch zur Laufzeit
injiziert – aber er erscheint nicht im Schema, das dem Modell gezeigt
wird.
Das ist der Mechanismus für Kontext-Zugriff. Ein Tool kann auf State, Context, Store und andere Runtime-Informationen zugreifen, ohne dass das Modell diese Parameter sieht oder füllen muss.
from langchain.tools import tool, ToolRuntime
@tool
def get_user_preference(
pref_name: str,
runtime: ToolRuntime
) -> str:
"""Liest eine Nutzer-Präferenz aus."""
preferences = runtime.state.get("user_preferences", {})
return preferences.get(pref_name, "Nicht gesetzt")Das Modell sieht nur pref_name im Schema.
runtime ist unsichtbar. Aber in der Funktionsausführung
steht er zur Verfügung und enthält den gesamten Agent-State.
Das ist eleganter als die alten Ansätze mit
InjectedState oder get_runtime(). Ein
Parameter, ein Typ, alles verfügbar.
ToolRuntime bietet Zugriff auf drei unterschiedliche
Kontextebenen. Jede hat eine spezifische Semantik.
State (runtime.state) ist der
veränderliche Agent-Zustand. Er enthält die Message-Historie, Custom
Fields, temporäre Berechnungen. Tools können State lesen und – über
Command-Objekte – auch modifizieren.
Context (runtime.context) ist
unveränderlich und Request-spezifisch. Typische Anwendung: User-ID,
Session-Metadaten, API-Keys. Der Context wird beim Agent-Aufruf
übergeben und bleibt während der Ausführung konstant.
Store (runtime.store) ist langfristiger
Speicher über Konversationen hinweg. Ein Key-Value-Store, der
Nutzer-Profile, Präferenzen, historische Daten persistiert. Tools können
Daten speichern und später abrufen.
Die Trennung ist wichtig. State ist temporär und Thread-gebunden. Context ist unveränderlich und Request-gebunden. Store ist persistent und global.
Welche Ebene nutzt man wann?
State für Informationen, die sich während einer Konversation ändern, aber nur in diesem Thread relevant sind.
Context für Informationen, die zur Laufzeit bekannt sind, aber nicht persistiert werden müssen oder sensibel sind.
Store für Informationen, die über Konversationen hinweg erhalten bleiben sollen.
Ein Tool führt eine Aktion aus und gibt ein Ergebnis zurück. Manchmal soll es auch den Agent-State modifizieren.
Früher war das umständlich: Tools mussten State-Updates als Seiteneffekte implementieren oder über komplexe Callback-Mechanismen kommunizieren.
LangChain v1.0 führt Command-Objekte ein. Ein Tool kann
ein Command zurückgeben, das State-Updates beschreibt:
from langgraph.types import Command
@tool
def update_user_name(new_name: str, runtime: ToolRuntime) -> Command:
"""Aktualisiert den Namen des Nutzers im State."""
return Command(update={"user_name": new_name})Das Command-Objekt wird vom Agent Executor
interpretiert. Der State wird aktualisiert. Das nächste Tool oder der
nächste Modellaufruf sieht den neuen State.
Das ist deklarativ und nachvollziehbar. Das Tool beschreibt, was geändert werden soll, nicht wie.
Command kann auch die Graph-Ausführung steuern: Zu einem
bestimmten Node springen, die Ausführung abbrechen, mehrere Updates
kombinieren. Das erweitert Tools von passiven Funktionen zu aktiven
Akteuren im Agent-Workflow.
Die Tool-Beschreibung ist das Wichtigste an einem Tool.
Warum? Weil sie bestimmt, ob das Modell das Tool überhaupt auswählt.
Ein Modell sieht dutzende Tools. Es muss entscheiden, welches relevant ist. Diese Entscheidung basiert primär auf der Beschreibung.
Eine schlechte Beschreibung führt zu falscher Tool-Auswahl. Eine gute Beschreibung führt zu präziser Nutzung.
Was macht eine gute Beschreibung aus?
Spezifität. Nicht: „Sucht in der Datenbank.” Sondern: „Sucht in der Kundendatenbank nach Namen, E-Mail-Adressen oder Bestell-IDs.”
Use Case. Nicht: „Führt Berechnungen aus.” Sondern: „Nutze dies für mathematische Operationen, die du nicht mental durchführen kannst.”
Grenzen. Nicht: „Liefert Wetterdaten.” Sondern: „Liefert aktuelle Wetterdaten für Städte. Unterstützt keine historischen Daten.”
Die Beschreibung ist ein Prompt für das Modell. Sie sollte mit derselben Sorgfalt geschrieben werden wie der System-Prompt des Agents selbst.
Tools können lange laufen. Ein API-Call, der fünf Sekunden braucht. Eine Datenbankabfrage, die zehn Sekunden dauert.
Ohne Feedback wirkt die Anwendung eingefroren. Der Nutzer weiß nicht, ob etwas passiert oder ob das System hängt.
Die Lösung: Stream Writer. Tools können während ihrer Ausführung Zwischenmeldungen streamen:
@tool
def analyze_document(doc_id: str, runtime: ToolRuntime) -> str:
"""Analysiert ein Dokument und extrahiert Metadaten."""
writer = runtime.stream_writer
writer("Lade Dokument...")
# Document laden (2 Sekunden)
writer("Extrahiere Text...")
# Text-Extraktion (3 Sekunden)
writer("Analysiere Inhalt...")
# NLP-Analyse (5 Sekunden)
return "Analyse abgeschlossen: 42 Entitäten gefunden"Diese Zwischenmeldungen werden in Echtzeit an den Client gestreamt. Der Nutzer sieht, dass Fortschritt stattfindet.
Das verbessert die User Experience massiv. Lange Tool-Calls sind akzeptabel, solange sie transparent sind.
Tools interagieren mit externen Systemen. Externe Systeme sind unzuverlässig. APIs fallen aus. Datenbanken sind überlastet. Netzwerke haben Timeouts.
Ein Tool muss mit Fehlern umgehen können.
Die einfachste Strategie: Exceptions werfen. Der Agent Executor fängt sie ab und gibt eine Fehlermeldung an das Modell zurück.
Die elegantere Strategie: Fehler in der Tool-Response beschreiben. Das Tool gibt einen String zurück: „Fehler: API nicht erreichbar. Bitte später erneut versuchen.”
Das Modell kann auf diese Nachricht reagieren. Es kann dem Nutzer eine verständliche Antwort geben oder ein alternatives Tool wählen.
Wichtig: Fehler sollten strukturiert sein. Nicht: „Error 500”. Sondern: „Die Datenbank ist momentan nicht verfügbar. Das kann einige Minuten dauern.”
Das Modell ist gut darin, technische Fehler in nutzerfreundliche Sprache zu übersetzen. Aber es braucht ausreichend Kontext.
Beginnen wir mit dem einfachsten denkbaren Tool. Eine Funktion, die einen String zurückgibt.
from langchain.tools import tool
@tool
def get_server_time() -> str:
"""Gibt die aktuelle Serverzeit zurück."""
from datetime import datetime
return datetime.now().strftime("%H:%M:%S Uhr")Das ist alles. Kein explizites Schema. Keine Konfiguration. Der Dekorator macht den Rest.
Was generiert LangChain daraus?
Ein Tool mit: - Name: get_server_time - Beschreibung:
„Gibt die aktuelle Serverzeit zurück.” - Schema: Keine Parameter -
Return Type: String
Dieses Tool kann ein Agent sofort nutzen:
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
agent = create_agent(
model=ChatOpenAI(model="gpt-4"),
tools=[get_server_time]
)
response = agent.invoke({
"messages": [{"role": "user", "content": "Wie spät ist es?"}]
})
print(response["messages"][-1].content)
# "Es ist aktuell 14:23:17 Uhr."Das Modell sieht das Tool in seinem Kontext, entscheidet es zu nutzen, der Agent Executor führt es aus, das Ergebnis geht zurück ins Modell, das Modell formuliert eine Antwort.
Ein kompletter Loop in wenigen Zeilen.
Standardnamen sind oft suboptimal. get_server_time ist
technisch korrekt, aber für das Modell nicht ideal.
Wir überschreiben Name und Beschreibung:
@tool(
name="current_time",
description="Nutze dies, um die aktuelle Uhrzeit zu erfahren. Wichtig für zeitbasierte Anfragen."
)
def get_server_time() -> str:
"""Gibt die aktuelle Serverzeit zurück."""
from datetime import datetime
return datetime.now().strftime("%H:%M:%S Uhr")Jetzt heißt das Tool current_time und die Beschreibung
ist expliziter. Das Modell versteht besser, wann es dieses Tool nutzen
soll.
Die Beschreibung ist der entscheidende Faktor. Sie sollte den Use Case klar machen: „Nutze dies für…” statt nur zu beschreiben, was das Tool tut.
Ein realistisches Beispiel: Ein Tool, das Produktsuchen durchführt.
from langchain.tools import tool
from pydantic import BaseModel, Field
from typing import Literal, Optional
class ProductSearchInput(BaseModel):
"""Schema für Produktsuchanfragen."""
query: str = Field(description="Suchbegriff oder Produktname")
category: Optional[Literal["electronics", "books", "clothing"]] = Field(
default=None,
description="Optional: Produktkategorie zur Einschränkung"
)
max_price: Optional[float] = Field(
default=None,
description="Optional: Maximaler Preis in Euro"
)
sort_by: Literal["relevance", "price", "rating"] = Field(
default="relevance",
description="Sortierreihenfolge der Ergebnisse"
)
@tool(args_schema=ProductSearchInput)
def search_products(
query: str,
category: Optional[str] = None,
max_price: Optional[float] = None,
sort_by: str = "relevance"
) -> str:
"""Durchsucht den Produktkatalog nach passenden Artikeln."""
# Simulierte Produktsuche
results = []
if category:
results.append(f"Gefiltert nach Kategorie: {category}")
if max_price:
results.append(f"Maximaler Preis: {max_price}€")
results.append(f"Sortierung: {sort_by}")
results.append(f"Gefunden: 12 Produkte für '{query}'")
return "\n".join(results)Dieses Tool demonstriert mehrere Konzepte:
Optionale Parameter: category und
max_price haben Default-Werte. Das Modell kann sie
weglassen.
Enumerationen:
Literal["electronics", "books", "clothing"] beschränkt die
Werte. Das Modell kann keine anderen Kategorien wählen.
Field-Beschreibungen: Jedes Feld hat eine
description, die dem Modell hilft, den Parameter zu
verstehen.
Das resultierende Tool ist präzise nutzbar. Das Modell weiß genau, welche Optionen es hat.
Ein Tool, das Informationen aus dem Agent-State nutzt.
from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent, AgentState
from langgraph.checkpoint.memory import InMemorySaver
from typing import TypedDict
# Custom State mit User-Präferenzen
class ShoppingAgentState(AgentState):
user_preferences: dict
cart_items: list[str]
@tool
def recommend_products(
category: str,
runtime: ToolRuntime[None, ShoppingAgentState]
) -> str:
"""Empfiehlt Produkte basierend auf Nutzerpräferenzen."""
# Präferenzen aus dem State lesen
preferences = runtime.state.get("user_preferences", {})
preferred_brands = preferences.get("brands", [])
budget = preferences.get("budget", "medium")
# Empfehlungen generieren (simuliert)
recommendations = []
if preferred_brands:
recommendations.append(f"Basierend auf deinen bevorzugten Marken: {', '.join(preferred_brands)}")
recommendations.append(f"Budget-Level: {budget}")
recommendations.append(f"Top-Empfehlung in {category}: Premium Widget Pro")
return "\n".join(recommendations)
# Agent mit Custom State erstellen
agent = create_agent(
model="openai:gpt-4",
tools=[recommend_products],
state_schema=ShoppingAgentState,
checkpointer=InMemorySaver()
)
# Erste Interaktion: Präferenzen setzen
response1 = agent.invoke(
{
"messages": [{"role": "user", "content": "Ich mag die Marken Apple und Sony, Budget ist hoch"}],
"user_preferences": {"brands": ["Apple", "Sony"], "budget": "high"},
"cart_items": []
},
{"configurable": {"thread_id": "user_001"}}
)
# Zweite Interaktion: Tool nutzt gespeicherte Präferenzen
response2 = agent.invoke(
{"messages": [{"role": "user", "content": "Empfiehl mir Elektronik"}]},
{"configurable": {"thread_id": "user_001"}}
)Das Tool greift über runtime.state auf die
User-Präferenzen zu. Diese wurden im ersten Request gesetzt und im State
persistiert. Der zweite Request lädt den State, das Tool sieht die
Präferenzen, die Empfehlungen sind personalisiert.
Das ist der Kern kontextsensitiver Tools: Sie reagieren nicht nur auf ihre direkten Parameter, sondern auch auf den breiteren Agent-Kontext.
Ein Tool, das auf Context statt State zugreift.
from dataclasses import dataclass
from langchain.tools import tool, ToolRuntime
# Context-Schema definieren
@dataclass
class UserContext:
user_id: str
api_key: str
region: str
# Simulierte Datenbank
USER_DATABASE = {
"user_123": {
"name": "Anna Schmidt",
"orders": ["Order-001", "Order-042", "Order-103"],
"membership": "Gold"
},
"user_456": {
"name": "Thomas Müller",
"orders": ["Order-234"],
"membership": "Standard"
}
}
@tool
def get_order_history(
runtime: ToolRuntime[UserContext, AgentState]
) -> str:
"""Ruft die Bestellhistorie des aktuellen Nutzers ab."""
# User-ID aus Context holen
user_id = runtime.context.user_id
if user_id not in USER_DATABASE:
return "Nutzer nicht gefunden"
user_data = USER_DATABASE[user_id]
orders = user_data["orders"]
membership = user_data["membership"]
return f"Mitgliedschaft: {membership}\nBestellungen: {', '.join(orders)}"
@tool
def get_account_status(
runtime: ToolRuntime[UserContext, AgentState]
) -> str:
"""Zeigt den Status des Nutzer-Accounts an."""
user_id = runtime.context.user_id
region = runtime.context.region
if user_id not in USER_DATABASE:
return "Nutzer nicht gefunden"
user_data = USER_DATABASE[user_id]
name = user_data["name"]
membership = user_data["membership"]
return f"Account: {name}\nMitgliedschaft: {membership}\nRegion: {region}"
# Agent mit Context-Schema erstellen
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
agent = create_agent(
model=ChatOpenAI(model="gpt-4"),
tools=[get_order_history, get_account_status],
context_schema=UserContext
)
# Context bei jedem Request übergeben
response = agent.invoke(
{"messages": [{"role": "user", "content": "Zeig mir meine Bestellungen"}]},
context=UserContext(
user_id="user_123",
api_key="secret_key_xyz",
region="EU"
)
)
print(response["messages"][-1].content)
# "Du hast folgende Bestellungen: Order-001, Order-042, Order-103.
# Deine Mitgliedschaft ist Gold."Der Context wird bei jedem Request neu übergeben. Er persistiert nicht. Das ist ideal für Session-Daten, API-Keys oder Request-spezifische Metadaten.
Die Tools greifen über runtime.context darauf zu, ohne
dass diese Parameter im Tool-Schema erscheinen. Das Modell sieht sie
nicht und kann sie nicht manipulieren.
Das ist ein Sicherheitsgewinn: Sensitive Daten wie
api_key sind für das Modell unsichtbar.
Ein Tool, das nicht nur Daten zurückgibt, sondern auch den State modifiziert.
from langchain.tools import tool, ToolRuntime
from langgraph.types import Command
from langchain.agents import AgentState
from typing import TypedDict
class ShoppingState(AgentState):
cart_items: list[str]
cart_total: float
@tool
def add_to_cart(
product_id: str,
price: float,
runtime: ToolRuntime[None, ShoppingState]
) -> Command:
"""Fügt ein Produkt zum Warenkorb hinzu."""
# Aktuellen Cart aus State lesen
current_items = runtime.state.get("cart_items", [])
current_total = runtime.state.get("cart_total", 0.0)
# Neue Werte berechnen
new_items = current_items + [product_id]
new_total = current_total + price
# State-Update via Command zurückgeben
return Command(
update={
"cart_items": new_items,
"cart_total": new_total
},
# Tool-Response (wird auch ans Modell zurückgegeben)
result=f"Produkt {product_id} für {price}€ hinzugefügt. Warenkorbsumme: {new_total}€"
)
@tool
def clear_cart(runtime: ToolRuntime[None, ShoppingState]) -> Command:
"""Leert den Warenkorb komplett."""
return Command(
update={
"cart_items": [],
"cart_total": 0.0
},
result="Warenkorb wurde geleert."
)
@tool
def show_cart(runtime: ToolRuntime[None, ShoppingState]) -> str:
"""Zeigt den aktuellen Warenkorbinhalt."""
items = runtime.state.get("cart_items", [])
total = runtime.state.get("cart_total", 0.0)
if not items:
return "Dein Warenkorb ist leer."
return f"Warenkorb: {', '.join(items)}\nGesamtsumme: {total}€"Diese Tools nutzen Command um den State zu modifizieren.
Der Agent Executor interpretiert das Command und aktualisiert den State.
Nachfolgende Tool-Aufrufe oder Modell-Interaktionen sehen den neuen
State.
Das ermöglicht zustandsabhängige Workflows: Ein Tool ändert den State, ein anderes Tool reagiert darauf.
Ein Tool, das Daten im Store persistiert und später abruft.
from langchain.tools import tool, ToolRuntime
from langgraph.store.memory import InMemoryStore
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
from typing import Any
@tool
def save_user_preference(
key: str,
value: str,
runtime: ToolRuntime
) -> str:
"""Speichert eine Nutzer-Präferenz dauerhaft."""
store = runtime.store
user_id = "default_user" # In Production aus Context holen
# Im Store speichern: (namespace, key) -> value
store.put(("preferences",), user_id, {key: value})
return f"Präferenz gespeichert: {key} = {value}"
@tool
def get_user_preference(
key: str,
runtime: ToolRuntime
) -> str:
"""Liest eine gespeicherte Nutzer-Präferenz."""
store = runtime.store
user_id = "default_user"
# Aus Store lesen
item = store.get(("preferences",), user_id)
if item is None:
return f"Keine Präferenz für '{key}' gefunden"
prefs = item.value
value = prefs.get(key, "Nicht gesetzt")
return f"{key}: {value}"
@tool
def list_preferences(runtime: ToolRuntime) -> str:
"""Listet alle gespeicherten Präferenzen auf."""
store = runtime.store
user_id = "default_user"
item = store.get(("preferences",), user_id)
if item is None or not item.value:
return "Keine Präferenzen gespeichert"
prefs = item.value
lines = [f"{k}: {v}" for k, v in prefs.items()]
return "Gespeicherte Präferenzen:\n" + "\n".join(lines)
# Agent mit Store erstellen
store = InMemoryStore()
agent = create_agent(
model=ChatOpenAI(model="gpt-4"),
tools=[save_user_preference, get_user_preference, list_preferences],
store=store
)
# Session 1: Präferenzen speichern
response1 = agent.invoke({
"messages": [{"role": "user", "content": "Meine Lieblingsfarbe ist Blau"}]
})
# Session 2: Präferenzen abrufen (komplett neue Konversation)
response2 = agent.invoke({
"messages": [{"role": "user", "content": "Was ist meine Lieblingsfarbe?"}]
})
print(response2["messages"][-1].content)
# "Deine Lieblingsfarbe ist Blau."Der Store persistiert Daten über Thread-Grenzen hinweg. Session 1 und Session 2 haben unterschiedliche Message-Historien, aber sie teilen sich den Store.
Das ist Long-Term Memory: Informationen, die über einzelne Konversationen hinweg erhalten bleiben.
In Production würde man einen datenbankgestützten Store nutzen (z.B.
PostgreSQL), keinen InMemoryStore. Aber das Prinzip bleibt
gleich.
Ein Tool, das länger läuft und Zwischenmeldungen streamt.
from langchain.tools import tool, ToolRuntime
import time
@tool
def analyze_large_dataset(
dataset_id: str,
runtime: ToolRuntime
) -> str:
"""Analysiert einen großen Datensatz (dauert einige Sekunden)."""
writer = runtime.stream_writer
# Phase 1
writer("📥 Lade Datensatz...")
time.sleep(1) # Simulierte Ladezeit
# Phase 2
writer("🔍 Extrahiere Features...")
time.sleep(1.5)
# Phase 3
writer("📊 Führe statistische Analyse durch...")
time.sleep(1.5)
# Phase 4
writer("🧮 Berechne Korrelationen...")
time.sleep(1)
# Finales Ergebnis
return f"Analyse von {dataset_id} abgeschlossen: 42 signifikante Muster gefunden"Die writer()-Calls werden in Echtzeit gestreamt. Der
Client sieht jede Phase während sie läuft.
Das ist essentiell für Tools mit langer Laufzeit: Datenbankexporte, API-Calls mit hoher Latenz, aufwändige Berechnungen.
Wichtig: Streaming funktioniert nur, wenn der Agent in einem LangGraph-Stream-Context läuft. Details dazu in der LangChain-Dokumentation zu Streaming.
Ein Tool, das mit Fehlern elegant umgeht.
from langchain.tools import tool
import requests
@tool
def fetch_api_data(endpoint: str) -> str:
"""Ruft Daten von einer externen API ab."""
base_url = "https://api.example.com"
try:
response = requests.get(
f"{base_url}/{endpoint}",
timeout=5
)
response.raise_for_status()
data = response.json()
return f"Daten erhalten: {data}"
except requests.Timeout:
return "Fehler: API-Anfrage hat das Zeitlimit überschritten. Bitte versuche es später erneut."
except requests.ConnectionError:
return "Fehler: Verbindung zur API konnte nicht hergestellt werden. Prüfe deine Netzwerkverbindung."
except requests.HTTPError as e:
if response.status_code == 404:
return f"Fehler: Endpoint '{endpoint}' existiert nicht."
elif response.status_code == 429:
return "Fehler: Zu viele Anfragen. Rate Limit erreicht. Bitte warte eine Minute."
else:
return f"Fehler: API gab Statuscode {response.status_code} zurück."
except Exception as e:
return f"Unerwarteter Fehler: {str(e)}"Dieses Tool wirft keine Exceptions. Es fängt alle möglichen Fehler ab und gibt beschreibende Nachrichten zurück.
Das Modell kann diese Nachrichten interpretieren und sinnvoll reagieren: Dem Nutzer eine verständliche Fehlermeldung geben, ein alternatives Tool versuchen, oder die Anfrage umformulieren.
Das ist robuster als Stack Traces an das Modell zu übergeben.
Bringen wir alle Konzepte zusammen. Ein Agent für Kundenservice mit mehreren Tools.
from langchain.tools import tool, ToolRuntime
from langchain.agents import create_agent, AgentState
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from typing import TypedDict
from dataclasses import dataclass
# Context: Unveränderliche Session-Daten
@dataclass
class CustomerContext:
customer_id: str
session_id: str
# State: Veränderliche Konversationsdaten
class CustomerServiceState(AgentState):
customer_name: str
pending_issues: list[str]
satisfaction_score: int
# Simulierte Datenbanken
CUSTOMER_DB = {
"cust_001": {
"name": "Lisa Meier",
"orders": ["ORD-123", "ORD-456"],
"email": "lisa@example.com"
},
"cust_002": {
"name": "Max Weber",
"orders": ["ORD-789"],
"email": "max@example.com"
}
}
ORDER_DB = {
"ORD-123": {"status": "Versandt", "tracking": "DE123456789"},
"ORD-456": {"status": "In Bearbeitung", "tracking": None},
"ORD-789": {"status": "Zugestellt", "tracking": "DE987654321"}
}
# Tools definieren
@tool
def get_customer_info(
runtime: ToolRuntime[CustomerContext, CustomerServiceState]
) -> Command:
"""Lädt Kundeninformationen aus der Datenbank."""
customer_id = runtime.context.customer_id
if customer_id not in CUSTOMER_DB:
return Command(result="Kunde nicht gefunden")
customer = CUSTOMER_DB[customer_id]
# State aktualisieren mit Kundenname
return Command(
update={"customer_name": customer["name"]},
result=f"Kunde: {customer['name']}\nBestellungen: {', '.join(customer['orders'])}\nE-Mail: {customer['email']}"
)
@tool
def track_order(order_id: str) -> str:
"""Verfolgt den Status einer Bestellung."""
if order_id not in ORDER_DB:
return f"Bestellung {order_id} nicht gefunden"
order = ORDER_DB[order_id]
status = order["status"]
tracking = order["tracking"]
result = f"Bestellung {order_id}:\nStatus: {status}"
if tracking:
result += f"\nTracking-Nummer: {tracking}"
return result
@tool
def report_issue(
issue_description: str,
runtime: ToolRuntime[CustomerContext, CustomerServiceState]
) -> Command:
"""Meldet ein Problem und fügt es zur Liste offener Issues hinzu."""
current_issues = runtime.state.get("pending_issues", [])
new_issues = current_issues + [issue_description]
return Command(
update={"pending_issues": new_issues},
result=f"Issue gemeldet: {issue_description}\nIssue-Nummer: #{len(new_issues)}"
)
@tool
def show_pending_issues(
runtime: ToolRuntime[CustomerContext, CustomerServiceState]
) -> str:
"""Zeigt alle offenen Issues des Kunden."""
issues = runtime.state.get("pending_issues", [])
if not issues:
return "Keine offenen Issues"
lines = [f"#{i+1}: {issue}" for i, issue in enumerate(issues)]
return "Offene Issues:\n" + "\n".join(lines)
@tool
def rate_service(
rating: int,
runtime: ToolRuntime[CustomerContext, CustomerServiceState]
) -> Command:
"""Bewertet den Kundenservice (1-5 Sterne)."""
if rating < 1 or rating > 5:
return Command(result="Bewertung muss zwischen 1 und 5 liegen")
return Command(
update={"satisfaction_score": rating},
result=f"Vielen Dank für deine Bewertung: {rating}/5 Sternen"
)
# Agent erstellen
agent = create_agent(
model=ChatOpenAI(model="gpt-4", temperature=0),
tools=[
get_customer_info,
track_order,
report_issue,
show_pending_issues,
rate_service
],
state_schema=CustomerServiceState,
context_schema=CustomerContext,
checkpointer=InMemorySaver(),
system_prompt="""Du bist ein freundlicher Kundenservice-Agent.
Nutze die verfügbaren Tools, um Kundenanfragen zu bearbeiten.
Lade zuerst immer die Kundeninformationen, bevor du andere Aktionen durchführst."""
)
# Verwendung
config = {"configurable": {"thread_id": "session_001"}}
context = CustomerContext(customer_id="cust_001", session_id="sess_xyz")
# Interaktion 1: Kundeninfo laden
response1 = agent.invoke(
{
"messages": [{"role": "user", "content": "Hallo, ich brauche Hilfe"}],
"customer_name": "",
"pending_issues": [],
"satisfaction_score": 0
},
context=context,
config=config
)
print("Bot:", response1["messages"][-1].content)
# Interaktion 2: Bestellung tracken
response2 = agent.invoke(
{"messages": [{"role": "user", "content": "Wo ist meine Bestellung ORD-123?"}]},
context=context,
config=config
)
print("Bot:", response2["messages"][-1].content)
# Interaktion 3: Issue melden
response3 = agent.invoke(
{"messages": [{"role": "user", "content": "Die Sendung ist beschädigt angekommen"}]},
context=context,
config=config
)
print("Bot:", response3["messages"][-1].content)
# Interaktion 4: Service bewerten
response4 = agent.invoke(
{"messages": [{"role": "user", "content": "Ich gebe 5 Sterne"}]},
context=context,
config=config
)
print("Bot:", response4["messages"][-1].content)Dieser Agent demonstriert das gesamte Tool-Ökosystem:
Mehrere Tools mit unterschiedlichen Funktionen arbeiten zusammen.
Context-Zugriff: customer_id kommt aus
dem unveränderlichen Context.
State-Updates: Tools wie
get_customer_info und report_issue
modifizieren den State via Command.
State-Persistierung: Der Checkpointer speichert
State zwischen Interaktionen. pending_issues wächst über
mehrere Anfragen.
Kontextsensitive Logik: Tools lesen aus State (z.B.
show_pending_issues liest die Issue-Liste).
Das Ergebnis: Ein Agent, der komplexe Multi-Step-Workflows abwickeln kann, dabei Kontext erhält und auf externe Systeme zugreift.
Do: Beschreibende Tool-Namen.
track_order ist besser als tool_2.
Don’t: Zu viele Tools. Mehr als 20 Tools überfordern das Modell. Gruppiere verwandte Funktionen.
Do: Präzise Beschreibungen. „Nutze dies für X, nicht für Y” ist besser als „Macht irgendwas mit X”.
Don’t: Sensible Daten im Schema. API-Keys, Passwörter gehören in Context, nicht in Tool-Parameter.
Do: Fehlerbehandlung. Jedes Tool, das externe Systeme anspricht, braucht Try-Catch-Blöcke.
Don’t: State-Mutation als Seiteneffekt. Nutze
Command für explizite State-Updates, keine versteckten
Änderungen.
Do: Streaming für lange Operations. Nutze
runtime.stream_writer bei Tools, die länger als 2 Sekunden
laufen.
Don’t: Überladene Parameter. Ein Tool mit 10 Parametern ist zu komplex. Splitte es in mehrere Tools.
Tools sind die Schnittstelle zwischen der abstrakten Textwelt des LLMs und der konkreten Welt der Systeme und Daten. Sie erweitern Agenten von reinen Textgeneratoren zu aktionsfähigen Assistenten.
Die Kunst liegt nicht im Code der Tools – das ist meist trivial. Die Kunst liegt im Design: Welche Tools bietet man an? Wie beschreibt man sie? Wie strukturiert man ihre Parameter? Wie behandelt man Fehler?
Ein gut designtes Tool-Set macht den Unterschied zwischen einem Agent, der beeindruckt, und einem, der produktiv ist.