Von Rohtext zu Pipelines: Parser, LCEL, Ausführbare Objekte und Speicher in LangChain
Wie LangChain Rohtexte von Modellen in strukturierte Daten umwandelt, Schritte mithilfe von LCEL und der Runnable-Schnittstelle zusammenstellt sowie heute die konversationelle Erinnerung verarbeitet.
Quellen: LangChain Explained: Output Parsers, Chains, Runnables, and Memory
Das Aufrufen eines Sprachmodells mit einem gut strukturierten Prompt-Template ist nur der erste Schritt bei einer echten Anwendung. Das Modell gibt weiterhin einen unstrukturierten String zurück, jeder Aufruf vergisst alles, was zuvor geschah, und das manuelle Verbinden von Prompt, Modell und Nachverarbeitung wird schnell zu repetitivem Code. Dieser Leitfaden behandelt die vier LangChain-Konzepte, die diese Probleme lösen: Ausgabeparser, mit LCEL erstellte Ketten, die Runnable-Schnittstelle, die die Kombination ermöglicht, sowie das Speicherkonzept. Danach sollten Sie in der Lage sein, jede LCEL-Pipeline zu verstehen, den richtigen Parser oder Runnable für eine Aufgabe auszuwählen und zu wissen, wo der Gesprächszustand in LangChain gespeichert ist.
Dies setzt voraus, dass Sie bereits wissen, was ein LLM-Wrapper und ein Prompt-Template in LangChain sind, da alles Weitere direkt darauf aufbaut.
Ausgabeparser: Text in Daten umwandeln
Das Problem, das die Parser lösen
Egal, was Sie ein Modell fragen – die Antwort kommt als freier Text. Für einen einfachen Chatbot, der die Antwort auf dem Bildschirm anzeigt, reicht das aus. Sobald Ihr Code mit der Antwort arbeiten muss – beispielsweise indem er sie in eine Datenbank schreibt, an eine andere Funktion weiterleitet oder sie als JSON über eine API zurückgibt – wird ein einfacher String zu einem Problem.
Ein Ausgabe-Parser befindet sich zwischen dem Modell und dem Rest Ihres Programms. Er nimmt den generierten Text entgegen und wandelt ihn in etwas Strukturiertes um: eine Liste, ein Dictionary, ein JSON-Objekt oder eine Instanz Ihrer eigenen Python-Klasse. Eine hilfreiche Vorstellung ist, ihn als Interpreter zu betrachten: Das Modell kommuniziert in lockerer Prosa, und der Parser wandelt diese Prosa in eine Form um, auf die sich Ihr Code verlassen kann.
Warum Sie nicht manuell parsen sollten
- Die Ausgabe des Modells hat kein garantiertes Format; derselbe Prompt kann bei verschiedenen Ausführungen leicht unterschiedliche Strukturen ergeben.
Die integrierte Parser-Familie
LangChain stellt mehrere Parsers bereit, von denen jeder für eine andere Zielstruktur konzipiert ist:
- Beschreibungszeichen-Ausgabe-Parser (
StrOutputParser): die einfachste Option, die den reinen Text aus der Antwortnachricht des Modells extrahiert. Verwenden Sie ihn, wenn Sie einen sauberen String zur Anzeige oder zur Weiterverarbeitung in einem anderen Schritt benötigen. - Liste-Parser: teilt die Antwort in eine Python-Liste auf, was praktisch ist, wenn Sie mehrere Elemente wie Schlüsselwörter oder Vorschläge anfordern.
- JSON-Ausgabeparser: Wandelt die Antwort in ein JSON-Objekt um und ist einer der am häufigsten verwendeten Parser, wenn strukturierte Daten benötigt werden.
- Pydantic-Ausgabeparser: Überträgt die Antwort direkt auf ein Pydantic-Modell, sodass neben der Struktur auch eine Typvalidierung erfolgt. Er ist die richtige Wahl, wenn die Daten einem strengen Schema folgen müssen.
- Datumzeitparser: Erstellt ein echtes Python-
datetime-Objekt anstelle eines als Text geschriebenen Datums. - Enum-Parser: Beschränkt die Ausgabe auf einen Wert aus einer vordefinierten Menge und gibt diesen Wert zurück.
- XML-Parser: Liest Ausgaben, die als XML-Tags geschrieben sind, und wandelt sie in ein Dictionary um.
Die strukturierten Parser bieten eine get_format_instructions()-Methode. Sie gibt einen Textblock zurück, den Sie in Ihre Anfrage einfügen können, um dem Modell genau anzugeben, wie es seine Antwort formatieren soll, damit der Parser sie lesen kann. Der einfache Zeichenketten-Parser hat nichts zu überwachen, weshalb er das einzige Mitglied dieser Familie ist, auf das Formatanweisungen nicht anwendbar sind.
Parser im Vergleich zur nativen strukturierten Ausgabe
Viele aktuelle Modelle können strukturierte Daten nativ zurückgeben, und LangChain stellt dies über with_structured_output() zur Verfügung. Wenn diese Funktion verfügbar ist, ersetzt sie oft einen Parser, da der Modellanbieter das Schema durchsetzt anstelle dessen, dass Ihre Anfrage höflich darum bittet. Parser behalten ihre Relevanz, wenn ein Modell keine native strukturierte Ausgabe bietet und Sie zusätzliche Validierungen oder Transformationen wünschen, die über das hinausgehen, was der Anbieter garantiert.
Ketten und LCEL: Die Schritte miteinander verbinden
Was eine Kette ist
Eine Kette besteht aus einer Reihe von Schritten, wobei die Ausgabe eines Schritts zur Eingabe des nächsten Schritts wird. Anstatt drei separate Codeabschnitte zum Formatieren einer Anfrage, Aufrufen des Modells und Auswerten des Ergebnisses zu schreiben, verbindet man sie zu einem wiederverwendbaren Pipeline.
Das klassische Beispiel umfasst drei Phasen: ein Vorlagen für die Anfrage, anschließend das Modell und schließlich ein Parser für die Ausgabe. Sie liefern rohe Eingaben wie ein Thema oder eine Frage, und die Kette formatiert die Anfrage, ruft das Modell auf und analysiert das zurückgegebene Ergebnis in einem einzigen Ablauf.
Was Ketten Ihnen bieten
- Die Verbindungen zwischen den Schritten werden nur einmal geschrieben, anstatt bei jedem Aufruf wiederholt zu werden.
- Die Anfrage, das Modell und der Parser sind unabhängige Komponenten, sodass Sie jede einzelne ersetzen können, ohne die anderen zu beeinflussen.
Komposition mit dem Pipe-Operator
Die LangChain Expression Language, LCEL, sorgt dafür, dass das Erstellen von Ketten unkompliziert erscheint. Sie verwendet den |-Operator, um Komponenten miteinander zu verbinden – ähnlich wie man Befehle in einer Unix-Shell miteinander verknüpft – anstatt Funktionsaufrufe ineinander zu verschachteln.
Die beiden untenstehenden Zeilen erstellen die oben beschriebene dreistufige Kette und führen sie einmal mit einem Dictionary-Eingang aus. Beachten Sie, dass der Schlüssel des Dictionaries, topic, mit einer Variablen im Prompt-Template übereinstimmen muss.
chain = prompt | model | output_parser
result = chain.invoke({"topic": "black holes"})
Die Eingabe gelangt in den prompt, wodurch ein formatierter Prompt erzeugt wird; dieser geht an das modell, und die Antwort des Modells gelangt anschließend zum output_parser. Jeder Komponente gibt sein Ergebnis direkt an die nächste weiter, und result enthält das, was der Parser zurückgibt.
Jede LCEL-Kette verfügt außerdem über eine Standardmenge an Ausführungsmethoden:
.invoke()führt die Kette einmal für eine einzelne Eingabe aus..batch()führt sie in einem Aufruf für eine Liste von Eingaben aus..stream()liefert die Ausgabe schrittweise, sobald sie erzeugt wird, anstatt dass man auf die vollständige Antwort warten muss. Das Streaming erfolgt nur tokenweise, sofern jeder Schritt in der Kette einen Stream verarbeiten kann; ein Schritt, der zunächst die gesamte Eingabe benötigt, puffert diese vorerst.
Dieser deklarative Stil ist ein großer Grund dafür, warum LCEL komponierbar erscheint. Man beschreibt, was geschehen soll und in welcher Reihenfolge – nicht die Mechanismen dafür, wie jeder Schritt den nächsten aufruft.
Runnables: die zugrundeliegende Schnittstelle
Warum alles gepipelt werden kann
Hinter LCEL ist jedes Komponente, egal ob es sich um einen Prompt, ein Modell, einen Parser oder eine von Ihnen selbst geschriebene Funktion handelt, ein Runnable. Ein Runnable ist eine gemeinsame Schnittstelle, die jede Komponente implementieren kann, und genau dieser gemeinsame Vertrag sorgt dafür, dass | funktioniert. Da jedes Element die gleichen Methoden bereitstellt, können sie unabhängig davon miteinander verbunden werden, was intern vor sich geht.
Dieses gleiche Interface sorgt dafür, dass die Interaktion mit einer Kette einheitlich erscheint. Egal ob ein Komponente ein Modell aufruft, Daten umformt oder mehrere Verarbeitungsschritte gleichzeitig ausführt – Sie steuern dies über die gleichen Methoden .invoke(), .batch() und .stream(). Eine zusammengesetzte Kette ist selbst ein Runnable, sodass Sie Ketten innerhalb größerer Ketten ohne besondere Handhabung verschachteln können.
Die am häufigsten verwendeten Runnables
- RunnableSequence führt die Schritte strikt in der Reihenfolge aus und leitet den Ausgang jedes Schritts an den nächsten weiter. Das Verbinden von Komponenten mit
|erstellt im Hintergrund automatisch eines dieser Objekte. Typische Anwendung: Zuerst Prompt, dann Modell, anschließend Parser.
RunnableParallel verwendet, wenn die ursprüngliche Eingabe zusammen mit den transformierten Werten weitergeleitet werden soll. Typische Anwendung: Zurückgabe der ursprünglichen Frage des Benutzers zusammen mit der erzeugten Antwort.Sobald man erkennt, dass Modelle, Parser und Ketten alle Runnables sind, die eine gemeinsame Schnittstelle teilen, wirkt LangChain nicht mehr wie ein Bündel von Sonderfällen. Es handelt sich dabei um gewöhnliche Komposition: kleine Einheiten mit derselben Struktur, die zu größeren Einheiten mit derselben Struktur kombiniert werden.
Achtung vor Über-Komposition
Weil alles zu einem Runnable werden kann, ist es verlockend, die gesamte Anwendungslogik als verschachtelte Ketten auszudrücken. Für einfache Schritte funktioniert das gut, doch tief verschachtelte Branches und Lambdas können schwieriger zu debuggen sein als im reinen Python. Eine vernünftige Regel ist es, LCEL für den modellorientierten Pipeline zu verwenden und komplexe Kontrollflüsse im normalen Code zu belassen – oder sie in ein Graphenframework zu verlegen, wenn Schleifen und Zustände involviert sind.
Gedächtnis: Kontext zwischen Schritten beibehalten
Was hier unter Gedächtnis verstanden wird
Jeder Aufruf eines Modells ist standardmäßig zustandslos. Es hat keine Kenntnis davon, was Sie gerade gefragt haben, es sei denn, Ihre Anwendung sendet den vorherigen Austausch erneut. In LangChain ist das Gedächtnis die Sammlung von Mechanismen zum Speichern des Konversationskontexts und dessen späteren Abrufen, damit eine Anwendung eine Konversation fortsetzen kann, anstatt jede Nachricht als wäre sie die erste zu behandeln.
Warum konversationelle Anwendungen dies benötigen
- Ohne Speicher verliert ein Chatbot im Moment des Eintreffens einer neuen Nachricht alles: keine Kontinuität, keine Nachfragen, kein gemeinsamer Kontext.
- Eine natürliche Konversation hängt von früheren Gesprächsschritten ab – beispielsweise davon, zu wissen, dass „er“ in einer Nachfrage auf jemanden verweist, der bereits zwei Nachrichten zuvor erwähnt wurde.
- Ein gespeicherter Kontext sorgt dafür, dass ein Assistent kohärent und konversationell wirkt, anstatt auf jede Nachricht isoliert zu reagieren.
- Es handelt sich dabei um eine grundlegende Voraussetzung für Kundenservice-Bots, persönliche Assistenten sowie alle anderen mehrschrittigen Anwendungen.
Klassische Speicherklassen
Der traditionelle Ansatz nutzte Speicherklassen, die innerhalb des laufenden Prozesses existierten. Die einfachste Variante speicherte einen Puffer mit dem gesamten Gespräch; eine andere konsolidierte ältere Nachrichten zu einem laufenden Zusammenfassung, um Tokens zu sparen. Bei jedem neuen Aufruf wurde der gespeicherte Kontext wieder in die Eingabeanweisung eingefügt.
Wo der Speicher heute liegt
Diese in-Prozess-Speicherklassen sind veraltet, da LangChain den Konversationszustand in die Persistenzschicht von LangGraph verlagert hat. In diesem Modell speichern Checkpointer den Konversationszustand pro Sitzung (identifiziert durch einen Thread), während separate Speicherorte langfristigen Speicher beherbergen, der über Sitzungen hinweg erhalten bleiben soll. Das Konzept bleibt unverändert: Der vorherige Kontext muss irgendwo gespeichert werden, damit er wiederverwendet werden kann. Was sich geändert hat, ist die empfohlene Implementierung, die nun widerstandsfähiger und besser für die Produktion geeignet ist, da der Zustand in einer echten Datenbank persistiert werden kann und nicht verschwindet, wenn der Prozess neu gestartet wird. Wenn Sie sehen möchten, wie ein Checkpointer diesen Zustand tatsächlich speichert, erklärt inside LangGraph’s InMemorySaver dessen Funktionsweise im Detail.
Ein sinnvoller Lernweg besteht darin, zunächst die klassische Vorstellung von Gedächtnis zu verstehen, da sie erklärt, warum das Gedächtnis wichtig ist und was in die Anfrage einfließt, und anschließend den auf Persistenz basierenden Ansatz für Anwendungen im Produktivumfeld zu übernehmen. Da sich dieses Gebiet schnell entwickelt hat, sollten Sie vor dem Aufbau die aktuellen Dokumentationen zu LangChain und LangGraph zur Prüfung der genauen APIs konsultieren.
Zusammenfassung
Zusammen mit LLM-Wrappern und Prompt-Vorlagen bilden diese Konzepte das Kernwerkzeugset von LangChain. Jedes Element für sich genommen ist unbedeutend; ihr Wert zeigt sich erst in der Kombination: Eine Vorlage versorgt das Modell mit Daten, ein Parser wandelt den Text des Modells in Strukturen um, jedes Element ist eine ausführbare Einheit, die mithilfe von | zu einer Kette verbunden wird, und eine Persistenzschicht speichert das Gespräch zwischen den Aufrufen.
- Verwenden Sie einen Ausgabeparser oder eine native strukturierte Ausgabe, sofern das Modell dies unterstützt, immer dann, wenn Code die Antwort des Modells verarbeitet.
RunnableSequence; wenden Sie RunnableParallel, RunnablePassthrough, RunnableLambda und RunnableBranch an, wenn der Ablauf nicht linear ist.Sobald diese Bausteine vorhanden sind, sind die nächsten logischen Themen die retrieval-augmented Generation sowie Agenten – beides wird genau aus diesen Komponenten zusammengesetzt.