Bisher haben wir uns mit Daten beschäftigt, die stillstehen. PDFs auf der Festplatte. Dokumente in Vector Stores. Zeilen in Datenbanken. Diese Quellen ändern sich selten – oder wenn, dann durch explizite Updates. Sie sind statisch. Ihr Wissen ist eingefroren zum Zeitpunkt der letzten Aktualisierung.
Doch die Welt bewegt sich. Aktienkurse ändern sich sekündlich. Wettervorhersagen werden stündlich aktualisiert. Kalenderereignisse kommen hinzu und verschwinden. Ticketsysteme füllen sich mit neuen Einträgen. Diese Informationen leben in APIs – Application Programming Interfaces. Sie sind nicht gespeichert und wartend. Sie sind dynamisch, abrufbar auf Anfrage, immer aktuell.
APIs sind die Fenster zur Live-Welt.
Für LLM-gestützte Anwendungen sind APIs fundamental anders als statische Datenquellen. Ein Vector Store kennt nur, was man ihm einmal gegeben hat. Eine API weiß, was gerade jetzt ist. Ein Dokument beschreibt, wie etwas funktioniert. Eine API zeigt, was gerade passiert. Diese Unterscheidung ist nicht nur technisch – sie ist konzeptionell. Sie verändert, wie wir über Retrieval, über Kontext, über die Rolle von Language Models denken.
Wenn ein Nutzer fragt “Wie wird das Wetter morgen?”, hilft kein Embedding und kein Vector Store. Die Antwort steht nicht in einem Dokument. Sie muss von einer Wetter-API abgerufen werden – jetzt, in diesem Moment. Wenn ein Agent einen Termin buchen soll, reicht Textverstehen nicht. Es braucht Zugriff auf ein Kalendersystem – über eine API. Wenn ein Chatbot den Status einer Bestellung prüfen soll, muss er mit dem Backend-System sprechen – ebenfalls über eine API.
Language Models können nicht selbst APIs aufrufen. Sie sind Text-in-Text-out-Systeme. Doch sie können beschreiben, welche API aufgerufen werden sollte, mit welchen Parametern. Sie können die Ergebnisse eines API-Aufrufs interpretieren und in natürliche Sprache übersetzen. Dazwischen braucht es eine Orchestrierungsschicht – und genau hier kommt LangChains Tool-Konzept ins Spiel.
REST (Representational State Transfer) ist das dominierende Paradigma für Web-APIs. Eine REST-API organisiert Ressourcen – Objekte wie Nutzer, Bestellungen, Dokumente – und bietet HTTP-Endpunkte für deren Manipulation. GET liest Daten. POST erstellt neue Einträge. PUT aktualisiert bestehende. DELETE entfernt sie. Die Kommunikation ist zustandslos: Jeder Request trägt alle nötigen Informationen in sich.
Für LLM-Anwendungen sind REST-APIs primär Informationsquellen. Das Modell braucht aktuelle Daten, die API liefert sie. Ein Beispiel: Eine Anwendung soll Kundenfragen zu Bestellstatus beantworten. Die Bestelldaten liegen nicht in einem Vector Store – sie sind zu dynamisch. Stattdessen fragt das System eine REST-API ab.
import requests
from typing import Optional, Dict, Any
def get_order_status(order_id: str) -> Optional[Dict[str, Any]]:
"""
Ruft den aktuellen Status einer Bestellung von einer REST-API ab.
Args:
order_id: Die eindeutige Bestell-ID
Returns:
Dict mit Bestelldetails oder None bei Fehler
"""
api_url = f"https://api.example.com/orders/{order_id}"
headers = {"Authorization": "Bearer YOUR_API_TOKEN"}
try:
response = requests.get(api_url, headers=headers, timeout=5)
response.raise_for_status() # Wirft Exception bei HTTP-Fehler
return response.json()
except requests.exceptions.RequestException as e:
print(f"API-Fehler: {e}")
return None
# Verwendung
order_data = get_order_status("ORD-12345")
if order_data:
status = order_data.get("status")
estimated_delivery = order_data.get("estimated_delivery")
print(f"Status: {status}, Lieferung: {estimated_delivery}")Diese Funktion ist einfach, aber sie illustriert das Kernprinzip: Wir kapseln API-Logik in Python-Funktionen. Diese Funktionen werden später als Tools für das Language Model verfügbar gemacht. Das Modell entscheidet, wann es die Funktion aufrufen muss. Die Orchestrierungsschicht führt den Aufruf aus. Das Resultat fließt zurück ins Modell.
Die Herausforderung liegt in der Übersetzung. Eine API gibt strukturierte Daten zurück – JSON, XML, manchmal Binärformate. Ein Language Model versteht Text. Wir brauchen eine Transformation. Entweder wandeln wir JSON in eine textuelle Beschreibung um, die ins Prompt passt. Oder wir geben dem Modell die rohen JSON-Daten und lassen es selbst interpretieren. Moderne Modelle wie GPT-4 verstehen JSON gut – sie können relevante Felder extrahieren und in natürliche Sprache übersetzen.
Betrachten wir ein realistisches Szenario. Eine E-Commerce-Plattform hat eine API, die Produktinformationen liefert. Die API gibt komplexe JSON-Strukturen zurück: Preis, Verfügbarkeit, Varianten, Reviews, technische Spezifikationen. Nicht alle davon sind für jede Anfrage relevant. Wie integriert man das ins Prompt?
Der naive Ansatz: Alles reinwerfen. Das JSON wird als String ins Prompt gepackt. Das Modell soll sich selbst zurechtfinden. Das funktioniert, ist aber ineffizient. JSON kann verbose sein. Verschachtelte Strukturen verbrauchen Token. Irrelevante Felder lenken ab.
Der bessere Ansatz: Selektive Transformation. Wir analysieren die Nutzeranfrage, identifizieren, welche Informationen relevant sind, und extrahieren nur diese aus der API-Response. Eine Frage nach dem Preis braucht keine Review-Texte. Eine Frage nach Kompatibilität braucht keine Versandinformationen.
from typing import List, Dict, Any
import requests
from langchain_core.tools import tool
@tool
def get_product_info(product_id: str, fields: List[str]) -> Dict[str, Any]:
"""
Ruft spezifische Produktinformationen von der API ab.
Args:
product_id: Die Produkt-ID
fields: Liste der gewünschten Felder (z.B. ["price", "availability"])
Returns:
Dict mit den angeforderten Produktdaten
"""
# API-Aufruf
api_url = f"https://api.shop.com/products/{product_id}"
response = requests.get(api_url)
full_data = response.json()
# Nur angeforderte Felder extrahieren
filtered_data = {
field: full_data.get(field, "Nicht verfügbar")
for field in fields
}
return filtered_dataDer @tool-Decorator ist LangChains Mechanismus, um
Python-Funktionen zu Tools zu machen. Das Modell sieht die
Funktionssignatur, die Docstring-Beschreibung und kann entscheiden, ob
und wie es die Funktion aufrufen möchte. Die Type Hints helfen dem
Modell, die erwarteten Parameter zu verstehen.
Doch manchmal ist selektive Extraktion nicht genug. Manche API-Responses sind so komplex, dass selbst gefilterte Versionen schwer zu verarbeiten sind. Hier kommt eine zweistufige Transformation ins Spiel: Erst API-Aufruf, dann Summarization durch ein kleineres, schnelleres Modell. Das Haupt-Modell bekommt eine kompakte, textuelle Zusammenfassung statt rohen JSON.
REST-APIs sind Request-Response-basiert. Man fragt, man bekommt eine Antwort, fertig. Doch manche Datenquellen sind kontinuierlich. Finanzmärkte senden Tick-Daten. IoT-Sensoren liefern Messwerte. Logs fließen in Echtzeit. Für solche Szenarien gibt es Streaming-APIs – Server-Sent Events (SSE), WebSockets oder spezialisierte Protokolle wie gRPC Streaming.
Streaming-APIs stellen besondere Anforderungen. Sie sind nicht
zustandslos. Die Verbindung bleibt offen. Daten kommen asynchron. Man
kann nicht einfach requests.get() aufrufen und das Ergebnis
zurückgeben. Man muss einen Stream konsumieren, möglicherweise über
längere Zeit, und entscheiden, wann man relevante Informationen
extrahiert hat.
Für LLM-Anwendungen sind Streaming-APIs oft zu granular. Ein Modell braucht keine einzelnen Tick-Daten – es braucht aggregierte Informationen. “Der Aktienkurs ist in den letzten 10 Minuten um 2% gestiegen” ist wertvoller als hundert einzelne Kurswerte. Die Lösung: Eine Middleware-Schicht, die den Stream konsumiert, aggregiert und strukturierte Snapshots bereitstellt.
import asyncio
from typing import AsyncIterator, Dict, Any
from collections import deque
import json
class StreamAggregator:
"""
Konsumiert einen Daten-Stream und aggregiert Werte über Zeitfenster.
"""
def __init__(self, window_size: int = 10):
self.window_size = window_size
self.buffer = deque(maxlen=window_size)
async def consume_stream(self, stream_url: str) -> AsyncIterator[Dict[str, Any]]:
"""
Konsumiert einen SSE-Stream und liefert aggregierte Daten.
In einer realen Implementierung würde hier eine Bibliothek wie
httpx oder aiohttp zum Einsatz kommen.
"""
# Vereinfachte Simulation eines Streams
async for event in self._mock_stream():
self.buffer.append(event)
# Alle 10 Events eine Aggregation liefern
if len(self.buffer) == self.window_size:
yield self._aggregate()
def _aggregate(self) -> Dict[str, Any]:
"""
Aggregiert die Werte im Buffer.
"""
if not self.buffer:
return {}
# Beispiel: Durchschnittswert berechnen
values = [event['value'] for event in self.buffer]
return {
"avg_value": sum(values) / len(values),
"min_value": min(values),
"max_value": max(values),
"data_points": len(values)
}
async def _mock_stream(self):
"""Mock-Stream für Demonstration."""
import random
for i in range(50):
yield {"timestamp": i, "value": random.uniform(10, 100)}
await asyncio.sleep(0.1)
# Verwendung
async def main():
aggregator = StreamAggregator(window_size=10)
async for snapshot in aggregator.consume_stream("https://api.example.com/stream"):
print(f"Aggregierte Daten: {snapshot}")
# Hier könnte man den Snapshot an ein LLM weitergeben
# asyncio.run(main())Diese Middleware-Schicht entkoppelt den Stream von der LLM-Anwendung. Das Modell sieht keine einzelnen Events, sondern aggregierte Snapshots. Es kann auf diese Snapshots reagieren – etwa Alerts generieren, wenn Schwellwerte überschritten werden, oder Trends zusammenfassen.
Die Integration in LangChain erfordert Custom Tools, die asynchron
arbeiten können. LangChain unterstützt asynchrone Tools über
async def-Funktionen. Das ermöglicht non-blocking IO, was
bei API-Aufrufen entscheidend für Performance ist.
Ein Language Model ist eine Vorhersagemaschine für Text. Es kann keinen Code ausführen, keine APIs aufrufen, keine Datenbanken abfragen. Doch es kann beschreiben, was getan werden sollte. Das Tool-Konzept macht sich das zunutze. Wir geben dem Modell eine Liste von verfügbaren Funktionen – Tools genannt. Jedes Tool hat einen Namen, eine Beschreibung und eine Signatur. Das Modell entscheidet, welches Tool mit welchen Parametern aufgerufen werden soll. Die Orchestrierungsschicht führt den Aufruf aus und gibt das Resultat zurück ans Modell.
Das ist Function Calling in Reinform. Moderne Modelle wie GPT-4 sind
darauf trainiert, strukturierte Tool-Aufrufe zu generieren. Sie geben
nicht “Ruf die Wetter-API auf” als Text zurück. Sie generieren ein
JSON-Objekt:
{"name": "get_weather", "arguments": {"location": "Berlin"}}.
LangChain parst dieses JSON und führt den entsprechenden Funktionsaufruf
aus.
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
import requests
# Tool 1: Wetter-API
@tool
def get_weather(location: str) -> str:
"""
Ruft die aktuelle Wettervorhersage für einen Ort ab.
Args:
location: Der Ort, für den das Wetter abgerufen werden soll
"""
# In Realität: echter API-Aufruf
# Hier: Simulation
api_url = f"https://api.weather.com/v1/forecast?location={location}"
# response = requests.get(api_url)
# return response.json()["forecast"]
# Mock-Antwort
return f"Das Wetter in {location}: Sonnig, 22°C, leichter Wind"
# Tool 2: Kalender-API
@tool
def check_calendar(date: str) -> str:
"""
Prüft, ob an einem bestimmten Datum Termine vorliegen.
Args:
date: Datum im Format YYYY-MM-DD
"""
# Mock-Antwort
return f"Am {date} sind 2 Termine: Meeting um 10:00, Review um 14:00"
# Tool 3: Bestellstatus-API
@tool
def get_order_status(order_id: str) -> str:
"""
Ruft den aktuellen Status einer Bestellung ab.
Args:
order_id: Die Bestellnummer
"""
# Mock-Antwort
return f"Bestellung {order_id}: In Zustellung, geschätzte Ankunft morgen"
# Tools zusammenstellen
tools = [get_weather, check_calendar, get_order_status]
# Modell mit Tool-Calling-Fähigkeit
model = ChatOpenAI(model="gpt-4o", temperature=0)
# Prompt-Template für den Agent
prompt = ChatPromptTemplate.from_messages([
("system", """Du bist ein hilfreicher Assistent mit Zugriff auf verschiedene
Tools. Nutze die verfügbaren Tools, um Fragen zu beantworten. Wenn du ein Tool
brauchst, rufe es auf. Wenn du alle nötigen Informationen hast, gib eine
finale Antwort."""),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
# Agent erstellen
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# Agent nutzen
result = agent_executor.invoke({
"input": "Wie wird das Wetter morgen in Berlin und habe ich an diesem Tag Termine?"
})
print("\n=== FINALE ANTWORT ===")
print(result["output"])Was passiert hier unter der Haube? Der Agent bekommt die Frage. Er
sieht, dass zwei Informationen nötig sind: Wetter und Kalender. Er
entscheidet, beide Tools aufzurufen. Erst
get_weather("Berlin"), dann
check_calendar("2025-10-20") – vorausgesetzt, “morgen” ist
der 20. Oktober 2025. Die Tool-Resultate kommen zurück. Der Agent
integriert sie in eine kohärente Antwort: “Das Wetter wird sonnig bei
22°C. Du hast zwei Termine: Meeting um 10:00 und Review um 14:00.”
Das Tool-Konzept ist mächtig, weil es generisch ist. Ein Tool kann eine REST-API wrappen. Ein anderes kann eine Datenbank-Query ausführen. Ein drittes kann lokale Berechnungen durchführen. Dem Modell ist das egal – es sieht nur die Beschreibung und entscheidet basierend darauf.
LangChain bietet viele vorgefertigte Integrations für populäre APIs – Google Calendar, Gmail, Slack, Jira, Wikipedia, und mehr. Doch oft braucht man Zugriff auf proprietäre oder spezialisierte APIs. Hier kommen Custom Loaders ins Spiel. Das sind eigene Python-Klassen, die die Loader-Schnittstelle von LangChain implementieren.
Ein Loader ist konzeptionell simpler als ein Tool. Ein Loader lädt Dokumente von einer Quelle. Ein Tool führt Aktionen aus und gibt strukturierte Daten zurück. Für API-basierte Datenquellen können beide Konzepte relevant sein. Wenn die API primär Lesezugriff bietet und Document-ähnliche Strukturen zurückgibt, ist ein Loader passend. Wenn die API Aktionen ausführt oder komplex strukturierte Daten liefert, ist ein Tool besser.
from typing import List, Iterator
from langchain_core.document_loaders import BaseLoader
from langchain_core.documents import Document
import requests
class CustomAPILoader(BaseLoader):
"""
Custom Loader für eine proprietäre REST-API, die Artikel zurückgibt.
"""
def __init__(self, api_url: str, api_key: str, category: str = None):
"""
Args:
api_url: Basis-URL der API
api_key: Authentifizierungs-Token
category: Optional, filtert nach Kategorie
"""
self.api_url = api_url
self.api_key = api_key
self.category = category
def lazy_load(self) -> Iterator[Document]:
"""
Lazy Loading: Generiert Dokumente on-demand.
Wichtig für große Datenmengen, um Speicher zu sparen.
"""
headers = {"Authorization": f"Bearer {self.api_key}"}
params = {"category": self.category} if self.category else {}
# Pagination handhaben
page = 1
while True:
params["page"] = page
response = requests.get(
f"{self.api_url}/articles",
headers=headers,
params=params
)
if response.status_code != 200:
break
data = response.json()
articles = data.get("articles", [])
if not articles:
break
# Jeder Artikel wird zu einem Document
for article in articles:
yield Document(
page_content=article["content"],
metadata={
"source": f"{self.api_url}/articles/{article['id']}",
"title": article["title"],
"author": article["author"],
"published": article["published_date"],
"category": article.get("category"),
}
)
page += 1
# Abbruchbedingung: keine weitere Seite
if not data.get("has_next_page"):
break
def load(self) -> List[Document]:
"""
Lädt alle Dokumente auf einmal.
Nutzt lazy_load intern, sammelt aber alle Resultate.
"""
return list(self.lazy_load())
# Verwendung
loader = CustomAPILoader(
api_url="https://api.company.com",
api_key="your-api-key-here",
category="technical"
)
# Dokumente laden
documents = loader.load()
print(f"Geladen: {len(documents)} Dokumente")
# Diese können jetzt in einen Vector Store eingefügt werden
# oder für RAG genutzt werdenDieser Custom Loader kapselt die gesamte API-Logik: Authentifizierung, Pagination, Error Handling. Er gibt standardisierte Document-Objekte zurück. Das erlaubt es, API-Daten in bestehende RAG-Pipelines zu integrieren. Die Dokumente können gesplittet, eingebettet und in Vector Stores gespeichert werden – genau wie PDFs oder Markdown-Dateien.
Der Unterschied zu statischen Loaders: Man führt ihn nicht einmal aus und cached das Resultat. Man führt ihn regelmäßig aus – stündlich, täglich – um aktuelle Daten zu ziehen. Die Vector Store-IDs sollten mit API-IDs korrespondieren, sodass Updates bestehende Dokumente überschreiben statt duplizieren.
Direkte API-Integration ist selten optimal. APIs sind für Maschinen gebaut, nicht für Language Models. Sie geben tiefe, verschachtelte JSON-Strukturen zurück. Sie nutzen kryptische Feld-Namen. Sie liefern binäre Daten. Sie haben Rate Limits. Sie erfordern komplexe Authentifizierungs-Flows. All das ist problematisch für LLM-Integration.
Middleware ist die Lösung. Eine Schicht zwischen API und LLM, die übersetzt, transformiert, cached und robuster macht. Sie abstrahiert die Komplexität der API und bietet dem LLM eine saubere, textorientierte Schnittstelle.
APIs haben Rate Limits. 100 Requests pro Stunde. 1000 pro Tag. Überschreitet man diese, wird man geblockt. Ein naiver Agent könnte in einer Konversation dieselbe API mehrfach aufrufen – für dieselben Daten. Das ist Verschwendung und riskiert Rate Limit-Überschreitungen.
Die Lösung: Ein simpler Cache. Wenn ein API-Aufruf gemacht wird, speichern wir das Resultat mit einem Timeout. Weitere Aufrufe mit denselben Parametern innerhalb des Timeouts nutzen das gecachte Resultat.
from functools import wraps
from datetime import datetime, timedelta
from typing import Any, Dict, Optional, Callable
import hashlib
import json
class APICache:
"""
Einfacher In-Memory-Cache für API-Responses.
"""
def __init__(self, default_ttl: int = 300):
"""
Args:
default_ttl: Time-to-live in Sekunden (Standard: 5 Minuten)
"""
self.cache: Dict[str, tuple[Any, datetime]] = {}
self.default_ttl = default_ttl
def _make_key(self, func_name: str, args: tuple, kwargs: dict) -> str:
"""
Generiert einen eindeutigen Cache-Key aus Funktionsname und Parametern.
"""
# Serialisiere Parameter zu String
key_data = f"{func_name}:{args}:{sorted(kwargs.items())}"
# Hash für kompakte Keys
return hashlib.md5(key_data.encode()).hexdigest()
def get(self, key: str) -> Optional[Any]:
"""
Holt einen Wert aus dem Cache, wenn er existiert und nicht abgelaufen ist.
"""
if key not in self.cache:
return None
value, expiry = self.cache[key]
if datetime.now() > expiry:
# Abgelaufen, entfernen
del self.cache[key]
return None
return value
def set(self, key: str, value: Any, ttl: Optional[int] = None):
"""
Speichert einen Wert im Cache.
"""
ttl = ttl or self.default_ttl
expiry = datetime.now() + timedelta(seconds=ttl)
self.cache[key] = (value, expiry)
def cached_call(self, ttl: Optional[int] = None):
"""
Decorator, der API-Aufrufe cached.
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args, **kwargs):
# Cache-Key generieren
cache_key = self._make_key(func.__name__, args, kwargs)
# Prüfen, ob gecached
cached_result = self.get(cache_key)
if cached_result is not None:
print(f"Cache Hit für {func.__name__}")
return cached_result
# Nicht gecached, API aufrufen
print(f"Cache Miss für {func.__name__}, rufe API auf")
result = func(*args, **kwargs)
# Resultat cachen
self.set(cache_key, result, ttl)
return result
return wrapper
return decorator
# Globale Cache-Instanz
api_cache = APICache(default_ttl=300)
# Verwendung mit Tool
@tool
@api_cache.cached_call(ttl=600) # 10 Minuten Cache
def get_stock_price(symbol: str) -> str:
"""
Ruft den aktuellen Aktienkurs ab.
Args:
symbol: Das Aktiensymbol (z.B. 'AAPL')
"""
# Simulierter API-Aufruf
import random
price = random.uniform(100, 200)
return f"Aktienkurs für {symbol}: ${price:.2f}"
# Erster Aufruf: Cache Miss, API wird aufgerufen
print(get_stock_price("AAPL"))
# Zweiter Aufruf innerhalb von 10 Minuten: Cache Hit
print(get_stock_price("AAPL"))Dieser Cache ist simpel, aber effektiv. Für produktive Systeme würde man Redis oder Memcached nutzen – verteilte Caches, die mehrere Prozesse oder Server bedienen können. Doch das Prinzip bleibt gleich: API-Aufrufe sind teuer. Cachen reduziert Kosten, Latenz und Rate Limit-Risiken.
APIs schlagen fehl. Netzwerk-Timeouts. Server-Fehler. Ungültige Parameter. Rate Limits. Authentifizierungs-Probleme. Ein robustes System muss diese Fehler abfangen und sinnvoll handhaben. Ein Agent sollte nicht abstürzen, wenn eine API antwortet mit 503 Service Unavailable. Er sollte es dem Nutzer kommunizieren: “Die Wetter-API ist momentan nicht erreichbar. Kann ich anderweitig helfen?”
from typing import Optional, Dict, Any
import requests
from requests.exceptions import RequestException, Timeout
from langchain_core.tools import tool
def safe_api_call(
url: str,
method: str = "GET",
timeout: int = 5,
retries: int = 3,
**kwargs
) -> Optional[Dict[str, Any]]:
"""
Wrapper für robuste API-Aufrufe mit Retry-Logik.
Args:
url: Die API-URL
method: HTTP-Methode (GET, POST, etc.)
timeout: Timeout in Sekunden
retries: Anzahl Wiederholungsversuche bei Fehler
**kwargs: Weitere Parameter für requests (headers, params, etc.)
Returns:
Response-JSON oder None bei Fehler
"""
for attempt in range(retries):
try:
response = requests.request(
method=method,
url=url,
timeout=timeout,
**kwargs
)
# Erfolgreicher Response
if 200 <= response.status_code < 300:
return response.json()
# Client-Fehler (4xx) - kein Retry sinnvoll
if 400 <= response.status_code < 500:
print(f"Client-Fehler {response.status_code}: {response.text}")
return None
# Server-Fehler (5xx) - Retry könnte helfen
print(f"Server-Fehler {response.status_code}, Versuch {attempt + 1}/{retries}")
except Timeout:
print(f"Timeout bei Versuch {attempt + 1}/{retries}")
except RequestException as e:
print(f"Request-Fehler: {e}, Versuch {attempt + 1}/{retries}")
# Exponentielles Backoff zwischen Retries
if attempt < retries - 1:
import time
time.sleep(2 ** attempt)
# Alle Retries fehlgeschlagen
return None
@tool
def get_weather_robust(location: str) -> str:
"""
Ruft Wetterdaten ab mit robustem Error Handling.
Args:
location: Der Ort für die Wettervorhersage
"""
api_url = f"https://api.weather.com/v1/forecast"
params = {"location": location}
headers = {"Authorization": "Bearer API_KEY"}
result = safe_api_call(
url=api_url,
method="GET",
params=params,
headers=headers,
timeout=5,
retries=3
)
if result is None:
return f"Entschuldigung, die Wetter-API ist momentan nicht verfügbar für {location}."
# Daten extrahieren und formatieren
forecast = result.get("forecast", {})
temp = forecast.get("temperature", "unbekannt")
condition = forecast.get("condition", "unbekannt")
return f"Wetter in {location}: {condition}, {temp}°C"Diese Robust-Wrapper-Funktion implementiert Best Practices: Timeouts verhindern, dass ein langsamer Server die Anwendung blockiert. Retries mit exponentiellem Backoff geben temporären Problemen Zeit sich zu lösen. Differenzierung zwischen Client- und Server-Fehlern verhindert sinnlose Retries. Rückgabe von aussagekräftigen Fehlermeldungen statt Exceptions erlaubt dem Agenten, gracefully zu degradieren.
Die wahre Stärke API-basierter Tools entfaltet sich, wenn ein Agent mehrere APIs in einer Konversation orchestriert. Ein Nutzer fragt: “Buche mir einen Flug nach Berlin nächste Woche, aber nur wenn das Wetter gut ist und ich keine Termine habe.” Das erfordert drei API-Aufrufe: Kalender prüfen, Wetter abrufen, Flug buchen. Die Reihenfolge und Bedingungen sind nicht hart codiert – der Agent entscheidet dynamisch.
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from datetime import datetime, timedelta
# Tools definieren
@tool
def check_calendar(date: str) -> str:
"""Prüft Termine an einem Datum (YYYY-MM-DD)."""
# Mock
return f"Am {date}: 1 Meeting um 15:00"
@tool
def get_weather_forecast(location: str, date: str) -> str:
"""Holt Wettervorhersage für Ort und Datum."""
# Mock
return f"Wetter in {location} am {date}: Sonnig, 24°C"
@tool
def book_flight(destination: str, date: str) -> str:
"""Bucht einen Flug zum Ziel am Datum."""
# Mock
return f"Flug nach {destination} am {date} gebucht (Buchungsnummer: FL-12345)"
@tool
def get_next_week_date() -> str:
"""Gibt das Datum in einer Woche zurück."""
next_week = datetime.now() + timedelta(days=7)
return next_week.strftime("%Y-%m-%d")
# Agent konfigurieren
tools = [check_calendar, get_weather_forecast, book_flight, get_next_week_date]
model = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", """Du bist ein Reise-Assistent. Nutze die verfügbaren Tools, um
Reisen zu planen. Prüfe immer erst Kalender und Wetter, bevor du Flüge buchst.
Wenn Termine vorhanden sind oder das Wetter schlecht ist, rate vom Flug ab."""),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(model, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=10 # Verhindert Endlosschleifen
)
# Komplexe Anfrage
result = agent_executor.invoke({
"input": """Buche mir einen Flug nach Berlin nächste Woche, aber nur wenn
das Wetter gut ist und ich keine wichtigen Termine habe."""
})
print("\n=== FINALE ANTWORT ===")
print(result["output"])Der Agent geht methodisch vor. Erst ermittelt er das Datum nächster
Woche via get_next_week_date(). Dann prüft er Kalender und
Wetter parallel oder sequenziell. Basierend auf den Resultaten
entscheidet er, ob er book_flight() aufruft. Wenn Wetter
schlecht oder Termine wichtig, empfiehlt er gegen die Buchung. Das ist
keine hartcodierte If-Else-Logik – das Modell interpretiert die
Tool-Ergebnisse und trifft Entscheidungen.
Diese Flexibilität ist der Kern von Agent-Systemen. Sie sind nicht Skripte, die eine feste Sequenz abarbeiten. Sie sind adaptive Workflows, die basierend auf Zwischenergebnissen umplanen können. APIs sind die Sensoren und Aktuatoren dieser Agents – sie liefern Informationen und führen Aktionen aus.
APIs und statische Datenquellen sind keine Konkurrenten. Sie sind Komplementäre. Ein RAG-System könnte Vector Stores für Wissensbasis nutzen – Dokumentation, Best Practices, historische Daten. Gleichzeitig ruft es APIs auf für aktuelle Metriken, Live-Status, Echtzeit-Informationen. Die Kombination ergibt ein System, das sowohl auf stabilem Wissen als auch auf aktuellen Fakten basiert.
Ein Beispiel: Ein technischer Support-Agent. Der Vector Store enthält Troubleshooting-Guides, Handbücher, FAQ-Artikel – statisches Wissen. Die API liefert den aktuellen System-Status des Kunden, Logs der letzten Stunde, aktive Incidents – dynamische Informationen. Der Agent kombiniert beides: “Laut Dokumentation (Vector Store) könnte das Problem X sein. Basierend auf Ihren System-Logs (API) sehe ich, dass Service Y seit 30 Minuten nicht antwortet. Das bestätigt die Hypothese. Hier ist die Lösung aus dem Troubleshooting-Guide…”
Diese Orchestrierung zwischen statisch und dynamisch ist die Zukunft von LLM-Anwendungen. Modelle werden nicht isoliert operieren. Sie werden eingebettet sein in ein Ökosystem aus Vector Stores, Datenbanken, APIs, Tools. Sie werden als Orchestrierungsschicht fungieren, die Informationen aus heterogenen Quellen aggregiert, interpretiert und in Aktionen oder Antworten übersetzt.
Die technischen Herausforderungen sind real. API-Authentifizierung, Rate Limits, Error Handling, Caching, Latenz-Optimierung, Monitoring. Doch LangChain bietet die Abstraktionen, um diese Komplexität zu handhaben. Das Tool-Konzept ist simpel, aber mächtig. Custom Loaders erlauben beliebige Datenquellen. Middleware-Patterns machen APIs LLM-kompatibel. Agenten orchestrieren mehrschrittige Workflows.
APIs sind keine Nachgedanken. Sie sind erste-Klasse-Bürger in der LLM-Architektur.
Wer heute intelligente Anwendungen baut, muss über Vector Stores und Embeddings hinausdenken. Die Welt ist dynamisch. Daten bewegen sich. APIs sind die Schnittstellen zu dieser bewegten Welt. Sie in LLM-Systeme zu integrieren ist nicht optional – es ist essentiell für Anwendungen, die in der Realität operieren, nicht nur in statischen Korpora. Die Werkzeuge sind da. Die Patterns sind etabliert. Was bleibt, ist die Implementierung – und die beginnt mit dem Verständnis, dass APIs nicht nur Datenquellen sind, sondern Aktionsschnittstellen. Sie erlauben LLMs, nicht nur zu lesen, sondern zu handeln. Das ist der Schritt von passivem Retrieval zu aktiven Agenten.