** DEPRECATED **
Ein LLM-Aufruf ist schnell geschrieben. Man übergibt einen Prompt, erhält eine Antwort, fertig. Doch sobald Anwendungen komplexer werden, zeigen sich die Grenzen: Wie verarbeitet man die Ausgabe eines Modells weiter? Wie füttert man ein zweites Modell mit dem Ergebnis des ersten? Wie verzweigt man je nach Kontext in unterschiedliche Verarbeitungswege? Wie hält man den Code wartbar, testbar, erweiterbar?
Genau hier setzen Chains an.
Eine Chain in LangChain ist mehr als ein Wrapper um einen LLM-Aufruf. Sie ist eine formalisierte Abfolge von Schritten, die Eingaben transformiert, Prompts rendert, Modelle aufruft und Ausgaben strukturiert weitergibt. Chains machen aus lose gekoppelten API-Calls eine nachvollziehbare Pipeline. Sie bringen Struktur in den Kontrollfluss und ermöglichen es, komplexe Verarbeitungslogik modular zu komponieren.
Was unterscheidet eine Chain von einer gewöhnlichen Python-Funktion? Auf den ersten Blick nicht viel. Auch eine Funktion kann mehrere Schritte hintereinander ausführen. Der entscheidende Unterschied liegt in der Abstraktion: Chains folgen einer einheitlichen Schnittstelle. Sie können verschachtelt, kombiniert und ausgetauscht werden, ohne dass der aufrufende Code sich ändert. Sie bringen Konventionen mit – für Input-Validierung, Output-Parsing, Fehlerbehandlung, Logging. Sie sind nicht nur ausführbar, sondern auch komponierbar.
Jede Chain implementiert ein gemeinsames Interface. Das Herzstück ist die Methode zum Ausführen: Man übergibt strukturierte Eingaben, die Chain führt ihre interne Logik aus und liefert strukturierte Ausgaben zurück. Diese Uniformität macht Chains stapelbar. Eine Chain kann eine andere Chain aufrufen, als wäre sie ein primitiver Baustein.
Die Klassenhierarchie ist einfach gehalten:
Chain --> <name>Chain
Das bedeutet: Es gibt eine abstrakte Basisklasse Chain,
und davon leiten sich spezialisierte Implementierungen ab –
LLMChain, SequentialChain,
RouterChain und so weiter. Jede dieser Spezialisierungen
löst ein bestimmtes Orchestrierungsproblem.
Doch die Landschaft hat sich gewandelt. Viele der ursprünglichen Chain-Klassen sind mittlerweile als deprecated markiert. LangChain hat sich in Richtung eines funktionaleren, auf Runnables basierenden Ansatzes entwickelt. Dennoch bleibt das Konzept der Chain konzeptionell wertvoll – und die Prinzipien gelten auch für die neueren Abstraktionen.
Es gibt verschiedene Typen von Chains, die jeweils unterschiedliche Orchestrierungsmuster abbilden:
LLMChain: Die grundlegendste Form. Sie kombiniert ein Prompt-Template mit einem Sprachmodell. Man übergibt Variablen, die Chain rendert den Prompt, ruft das Modell auf und gibt die Antwort zurück. Simpel, aber mächtig – der kleinste gemeinsame Nenner für LLM-basierte Verarbeitung.
SequentialChain und SimpleSequentialChain: Hier
werden mehrere Chains hintereinander geschaltet. Die Ausgabe einer Chain
wird zur Eingabe der nächsten. SimpleSequentialChain nimmt
an, dass jede Chain nur einen Output produziert, der direkt als Input in
die nächste fließt. SequentialChain erlaubt komplexere
Datenflüsse mit benannten Variablen und mehreren Outputs pro
Schritt.
RouterChain: Eine bedingte Verzweigung. Basierend auf der Eingabe entscheidet die Router-Chain, welche von mehreren möglichen Chains ausgeführt werden soll. Das ist nützlich, wenn verschiedene Eingabetypen unterschiedliche Verarbeitungslogik erfordern – etwa wenn eine Nutzeranfrage entweder technischer Support oder Produktberatung betrifft.
TransformChain: Manchmal braucht man keine
LLM-Interaktion, sondern nur eine Datentransformation. Die
TransformChain wickelt eine reine Python-Funktion in das
Chain-Interface, sodass sie sich nahtlos in eine Pipeline einfügt.
Jede dieser Varianten löst ein spezifisches Kompositionsproblem. Zusammen bilden sie ein Toolkit, um Anwendungslogik in verständliche, wiederverwendbare Blöcke zu zerlegen.
Chains bringen weitere Eigenschaften mit, die über reine Ausführung hinausgehen:
Stateful: Chains können mit Memory-Komponenten ausgestattet werden, um Kontext über mehrere Aufrufe hinweg zu behalten. Das ist essenziell für Konversationsanwendungen, bei denen frühere Nutzeräußerungen die Interpretation der aktuellen Eingabe beeinflussen.
Observable: Durch Callbacks kann man in den Lebenszyklus einer Chain eingreifen – Logging, Metriken, Debugging, externe Benachrichtigungen. Die Chain führt die Hauptlogik aus, während Callbacks passiv mitlaufen und Einblick gewähren, ohne die Kernfunktionalität zu verändern.
Composable: Chains sind Lego-Steine. Man kann sie kombinieren, verschachteln, in andere Komponenten einbetten. Das ermöglicht inkrementelles Bauen: Erst eine einfache Chain für einen Teilschritt, dann eine übergeordnete Chain, die mehrere solcher Schritte orchestriert.
Eine Chain ist kein Mysterium. Sie ist ein Ablaufplan mit Eintrittspunkt und Ausgangspunkt.
Theorie ist gut, Praxis ist besser. Schauen wir uns an, wie Chains konkret aussehen – zunächst die einfachste Form, dann eine Komposition.
Das klassische Beispiel: Eine LLMChain, die ein
Prompt-Template mit einem Sprachmodell verbindet. Man übergibt eine
Variable, das Template wird gerendert, das Modell antwortet.
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
# Das Modell: GPT-4 als Backend
llm = ChatOpenAI(model="gpt-4", temperature=0.7)
# Das Prompt-Template: Eine einfache Frage-Antwort-Struktur
prompt = PromptTemplate(
input_variables=["thema"],
template="Erkläre das Konzept '{thema}' in drei prägnanten Sätzen."
)
# Die Chain: Verknüpfung von Prompt und Modell
chain = LLMChain(llm=llm, prompt=prompt)
# Ausführung: Eingabe rein, Ausgabe raus
ergebnis = chain.run(thema="Quantenverschränkung")
print(ergebnis)Was passiert hier? Die Chain nimmt die Variable thema
entgegen, setzt sie in das Template ein, schickt den fertig gerenderten
Prompt an das Modell und gibt die Antwort zurück. Die gesamte
Orchestrierung – Template-Rendering, API-Aufruf, Response-Parsing – ist
in der Chain gekapselt.
Doch Vorsicht: LLMChain ist mittlerweile deprecated.
LangChain bevorzugt heute den Runnable-Ansatz mit der Pipe-Syntax
(|). Das Prinzip bleibt jedoch dasselbe: Eine definierte
Abfolge von Transformationen, gekapselt in einer wiederverwendbaren
Einheit.
Der moderne Weg sähe so aus:
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4", temperature=0.7)
prompt = ChatPromptTemplate.from_template(
"Erkläre das Konzept '{thema}' in drei prägnanten Sätzen."
)
# Chain als Runnable-Komposition
chain = prompt | llm | StrOutputParser()
# Ausführung mit invoke
ergebnis = chain.invoke({"thema": "Quantenverschränkung"})
print(ergebnis)Hier wird die Chain nicht als Klasseninstanz erstellt, sondern als Verkettung von Runnables mit dem Pipe-Operator. Das Ergebnis ist dasselbe: kontrollierte Ausführung, klarer Datenfluss, wiederverwendbar.
Ein einzelner LLM-Aufruf reicht selten aus. Oft braucht man mehrere Schritte: Erst einen Entwurf generieren, dann verfeinern. Erst eine Frage analysieren, dann beantworten. Erst Fakten sammeln, dann zusammenfassen.
SimpleSequentialChain macht genau das: Sie reiht mehrere
Chains hintereinander. Die Ausgabe der ersten Chain wird zur Eingabe der
zweiten.
from langchain.chains import LLMChain, SimpleSequentialChain
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4", temperature=0.7)
# Erste Chain: Generiere eine Produktidee
prompt_idee = PromptTemplate(
input_variables=["kategorie"],
template="Schlage eine innovative Produktidee für die Kategorie '{kategorie}' vor. Beschreibe sie in zwei Sätzen."
)
chain_idee = LLMChain(llm=llm, prompt=prompt_idee)
# Zweite Chain: Erstelle einen Werbespruch für diese Idee
prompt_slogan = PromptTemplate(
input_variables=["produktidee"],
template="Erstelle einen eingängigen Werbespruch für folgende Produktidee:\n\n{produktidee}"
)
chain_slogan = LLMChain(llm=llm, prompt=prompt_slogan)
# Verkettung: Idee → Slogan
gesamtkette = SimpleSequentialChain(chains=[chain_idee, chain_slogan], verbose=True)
# Ausführung: Ein Input, zwei Stufen, ein Output
ergebnis = gesamtkette.run("smarte Küchengeräte")
print(ergebnis)Die erste Chain generiert eine Produktidee basierend auf der Kategorie. Ihr Output ist ein Textblock. Dieser wandert automatisch als Input in die zweite Chain, die daraus einen Slogan formuliert. Das Endergebnis ist der Slogan – nicht die Zwischenstufe.
verbose=True sorgt dafür, dass die Chain ihre
Zwischenergebnisse ausgibt. Man sieht den Datenfluss in Aktion: Eingabe
→ Stufe 1 → Zwischenresultat → Stufe 2 → Finale Ausgabe. Debugging wird
transparent.
Das ist der Kern des Chain-Konzepts: Jeder Schritt ist isoliert testbar, aber zusammen bilden sie eine kohärente Pipeline.
SimpleSequentialChain hat eine Einschränkung: Jede Chain
darf nur einen Output haben, und dieser fließt vollständig in die
nächste. Doch manchmal braucht man mehr Kontrolle. Man möchte mehrere
Variablen zwischen Stufen übergeben, bestimmte Outputs überspringen oder
kombinieren.
Dafür gibt es SequentialChain. Sie erlaubt benannte
Inputs und Outputs und definiert explizit, welche Variablen zwischen den
Chains fließen.
from langchain.chains import LLMChain, SequentialChain
from langchain.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4", temperature=0.7)
# Erste Chain: Analysiere den Tonfall einer Nachricht
prompt_tonfall = PromptTemplate(
input_variables=["nachricht"],
template="Analysiere den Tonfall folgender Nachricht in einem Wort:\n\n{nachricht}"
)
chain_tonfall = LLMChain(
llm=llm,
prompt=prompt_tonfall,
output_key="tonfall"
)
# Zweite Chain: Formuliere eine passende Antwort basierend auf Tonfall und Originaltext
prompt_antwort = PromptTemplate(
input_variables=["nachricht", "tonfall"],
template="Die Nachricht war im Tonfall '{tonfall}'. Formuliere eine angemessene, professionelle Antwort auf:\n\n{nachricht}"
)
chain_antwort = LLMChain(
llm=llm,
prompt=prompt_antwort,
output_key="antwort"
)
# Verkettung: Nachricht → Tonfall → Antwort
gesamtkette = SequentialChain(
chains=[chain_tonfall, chain_antwort],
input_variables=["nachricht"],
output_variables=["tonfall", "antwort"],
verbose=True
)
# Ausführung: Eine Eingabe, zwei Outputs (Tonfall und Antwort)
ergebnis = gesamtkette({"nachricht": "Eure Software ist eine Katastrophe! Ich verlange sofort mein Geld zurück!"})
print(f"Erkannter Tonfall: {ergebnis['tonfall']}")
print(f"Vorgeschlagene Antwort: {ergebnis['antwort']}")Hier wird die ursprüngliche Nachricht in beide Chains weitergereicht.
Die erste Chain produziert den tonfall, die zweite nutzt
sowohl nachricht als auch tonfall, um eine
antwort zu formulieren. Die Gesamtkette gibt beide Outputs
zurück – man hat Zugriff auf Zwischenergebnisse und Endresultat.
Dieser Ansatz ist flexibler, aber auch expliziter. Man deklariert genau, welche Variablen fließen. Das macht den Datenfluss nachvollziehbar – ein Vorteil bei komplexeren Pipelines.
Manchmal ist der Ablauf nicht linear. Je nach Art der Eingabe soll eine andere Chain greifen. Ein Kundenanliegen über Rechnungen braucht andere Verarbeitung als eine technische Support-Anfrage. Eine RouterChain entscheidet dynamisch, welcher Pfad genommen wird.
Die Dokumentation zeigt mehrere Router-Varianten:
LLMRouterChain, EmbeddingRouterChain,
MultiPromptChain. Das Prinzip bleibt gleich: Eine
Routing-Logik analysiert die Eingabe und wählt eine von mehreren
Ziel-Chains aus.
Ein vereinfachtes Konzept:
from langchain.chains.router.base import MultiRouteChain
# Definition mehrerer spezialisierter Chains (hier verkürzt dargestellt)
chain_technik = ... # Chain für technische Anfragen
chain_vertrieb = ... # Chain für Vertriebsanfragen
chain_allgemein = ... # Fallback-Chain
# Router: Entscheidet basierend auf Eingabe
router_chain = ... # Enthält Logik zur Klassifizierung der Anfrage
# Zusammenbau: Router + spezialisierte Chains
gesamtkette = MultiRouteChain(
router_chain=router_chain,
destination_chains={"technik": chain_technik, "vertrieb": chain_vertrieb},
default_chain=chain_allgemein
)
# Ausführung: Die richtige Chain wird automatisch gewählt
ergebnis = gesamtkette.run("Mein Laptop startet nicht mehr. Was soll ich tun?")Der Router analysiert die Anfrage, erkennt das Thema als “technik”
und leitet die Verarbeitung an chain_technik weiter. Der
Aufrufer bekommt davon nichts mit – er sieht nur Input und Output. Die
interne Verzweigung bleibt gekapselt.
Das ist Komposition auf höherer Ebene: Nicht nur sequenziell, sondern auch konditional.
Chains sind keine Blackbox-Magie. Sie sind strukturierte Ablaufpläne. Jede Chain hat einen Eintrittspunkt, eine definierte Verarbeitungslogik und einen Austrittspunkt. Sie nimmt Daten entgegen, transformiert sie schrittweise und gibt ein Ergebnis zurück.
Der Wert liegt nicht nur in der Ausführung, sondern in der Modellierung. Durch Chains wird implizite Logik explizit. Statt verschachtelter Funktionsaufrufe und versteckter Abhängigkeiten entsteht ein nachvollziehbarer Datenfluss. Man sieht auf einen Blick: Was sind die Schritte? In welcher Reihenfolge? Welche Daten fließen?
Gleichzeitig bleiben Chains komponierbar. Eine Chain kann Teil einer größeren Chain sein. Das ermöglicht inkrementelles Design: Zuerst kleine, fokussierte Chains für Teilaufgaben, dann übergeordnete Chains, die diese orchestrieren.
Die Entwicklung von LangChain zeigt eine Verschiebung weg von
klassenbasierten Chains hin zu funktionalen Runnables. Doch das
zugrundeliegende Konzept – kontrollierte, komponierbare Ausführung –
bleibt bestehen. Ob man eine LLMChain instanziiert oder
eine Runnable-Pipeline mit | baut: Man schafft eine
formalisierte Verarbeitungskette.
Chains sind keine Besonderheit von LangChain. Sie sind eine Antwort auf ein fundamentales Problem: Wie organisiere ich komplexe, mehrstufige Verarbeitung so, dass sie verständlich, wartbar und erweiterbar bleibt?
Die Antwort lautet: Durch Abstraktion, Konvention und Komposition. Chains bieten das Gerüst. Den Inhalt füllen wir mit Prompts, Modellen und Logik.