Wie LangGraph-Agenten Neustarts überleben: Checkpoints, Checkpointer, Threads
Erfahren Sie, wie die Persistenz von LangGraph funktioniert: Warum Agenten ohne sie alles verlieren und wie Zustände, Checkpoints, Checkpointer sowie Thread-IDs es ermöglichen, nach einem Absturz mit der Ausführung fortzufahren.
Ein Agent, der alles im Prozessgedächtnis speichert, vergisst alles sofort, sobald der Prozess stoppt: Eine Bereitstellung, ein Absturz, eine unverarbeitete Zeitüberschreitung oder sogar das Ende einer Anfrage reichen aus, um die Konversation sowie alle Zwischenergebnisse zu löschen. LangGraph begegnet diesem Problem mit der Persistenz – einem Mechanismus, der den Zustand des Graphen nach jedem Schritt aufzeichnet, sodass eine Ausführung pausiert, wieder aufgenommen und fortgesetzt werden kann, anstatt immer von vorn zu beginnen. Dieser Leitfaden entwickelt das notwendige Konzept Schritt für Schritt: Was schiefgeht, wenn keine Persistenz vorhanden ist, wie der Zustand von LangGraph mit Checkpoints zusammenhängt, welche Funktion Checkpointer haben, wie Thread-IDs dazu dienen, Konversationen voneinander zu trennen, und wo der im Speicher gespeicherte Checkpointer seinen Platz hat. Am Ende werden Sie in der Lage sein, Persistenz an einen Graphen anzubinden, genau zu verstehen, was und wann gespeichert wird, sowie zu wissen, warum die im Speicher gespeicherte Option nur ein Entwicklungswerkzeug ist.
Persistenz ist außerdem die Grundlage für mehrere Funktionen, die oft getrennt voneinander diskutiert werden: Schritte zur menschlichen Freigabe, Aufrufe von Tools über mehrere Interaktionsschritte hinweg sowie langlaufende Agenten – all das setzt die Fähigkeit voraus, einen Graphen anzuhalten und später wieder aufzunehmen. Wenn man dies zuerst versteht, wirken diese Funktionen weitaus weniger rätselhaft.
Warum Agenten einen Zustand benötigen, der länger anhält als der Prozess
Stellen Sie sich vor, Sie tippen zwei Stunden lang an einem langen Dokument, ohne es zu speichern, und verlieren anschließend den Strom. Die Arbeit ist einfach verschwunden, und Sie müssen von der ersten Seite anfangen. Ein Agent ohne Persistenz befindet sich jedes Mal, wenn er neu gestartet wird, genau in dieser Situation. Er hat keine Aufzeichnung davon, was vor ein paar Sekunden oder erst vor ein paar Tagen passiert ist.
In einem Satz bedeutet Persistenz, den Zustand einer Anwendung an einem langlebigen Ort zu speichern, damit er nach dem Stoppen des Programms wiederhergestellt und fortgesetzt werden kann. Es funktioniert wie eine automatische Speichern-Schaltfläche, die fast nach jedem Schritt ausgelöst wird, ohne dass jemand sie drücken muss.
Dies ist für Agenten wichtiger als für klassischen Request/Response-Code, denn moderne Agenten sind selten nur eine einzige Frage und eine einzige Antwort. Sie tun in der Regel:
- Gespräche über Stunden oder Tage hinweg fortsetzen;
- anhalten und auf die Genehmigung einer Person für eine Aktion warten;
- mehrere Tool-Aufrufe hintereinander ausführen, um eine Aufgabe zu erledigen;
- über einen langen Zeitraum hinweg viele Schritte durchdenken;
- neben Neustarts und Abstürzen weiterarbeiten können, ohne Fortschritte zu verlieren.
RAM ist schnell, aber flüchtig: Er wird gelöscht, wenn der Prozess beendet wird. Alles, was ein Agent über die Lebensdauer eines einzelnen Prozesses hinaus benötigt, muss in dauerhaften Speicher geschrieben und später wieder abgerufen werden. In LangGraph bezieht sich „Persistenz“ auf diesen Speicher sowie die Mechanismen, die ihn verwalten.
Was schiefgeht, wenn ein Agent keine Persistenz hat
Das Problem lässt sich am besten anhand konkreter Ausfallmuster verstehen. Jedes der folgenden Fälle tritt in der Praxis häufig auf.
Ausfall des Stroms mitten in einer langwierigen Aufgabe
Ein Agent fasst einen 200-seitigen Bericht zusammen und ist bei Seite 100 angekommen, als der Rechner den Strom verliert. Ohne gespeicherten Zustand gibt es keine Aufzeichnung davon, dass die Hälfte der Arbeit bereits erledigt ist, weshalb der nächste Versuch bei Seite 1 beginnt.
Regelmäßige Neustart des Servers
Der Agent läuft auf einem Cloud-Server, und ein normales Bereitstellen startet ihn erneut. Jeder Benutzer, der mitten in einem Gespräch war, verliert seine Historie. Der Chatbot kennt nicht einmal mehr den Namen eines Benutzers, der ihm vor zwei Minuten mitgeteilt wurde.
Ausfall mitten in einer mehrstufigen Aufgabe
Im Rahmen einer größeren Aufgabe ruft der Agent eine externe API auf, die abläuft, wodurch der Python-Prozess aufgrund einer unverarbeiteten Ausnahme abstirbt. Der fehlgeschlagene Schritt geht verloren, genauso wie alles, was der Agent zuvor erledigt hat.
Abläufe, die stunden- oder tagelang laufen
Einige Agenten sind absichtlich langsam. Stellen Sie sich einen vor, der Aktienkurse überwacht und nur dann handelt, wenn ein Schwellenwert überschritten wird. Wenn sein gesamter Zustand im Speicher liegt, kann er weder pausiert noch neu bereitgestellt oder auf einen anderen Rechner verschoben werden, ohne von vorn anzufangen.
Menschliche Freigabe, die stundenlang dauert
Ein Agent erstellt ein rechtliches Dokument, das von einem Anwalt genehmigt werden muss, bevor es versandt wird. Der Anwalt darf sechs Stunden lang nicht in die Warteschlange schauen. Ein Prozess so lange einzufrieren und Ressourcen zu blockieren, ist verschwenderisch; falls der Server während des Wartens neu gestartet wird, verschwindet die Aufgabe.
Mehrschrittiges Denken, das spät versagt
Komplexe Agenten teilen die Arbeit oft in einen Zyklus aus Planung, Suchen, Überprüfen und Zusammenfassen auf. Wenn Schritt 7 von 10 fehlschlägt, verschwendet das Erneuten der Schritte 1 bis 6 Zeit, API-Aufrufe und Geld.
Warum „es einfach noch einmal ausführen“ keine Strategie ist
Einen Neustart von vorne klingt akzeptabel – bis man die Kosten betrachtet:
- Geld. Jeder LLM-Aufruf verbraucht Tokens, sodass das Erneuten von sechs erfolgreichen Schritten wegen eines fehlgeschlagenen siebten bedeutet, dass man dafür zweimal bezahlt.
- Zeit. Die Benutzer müssen warten, während die Arbeit, auf die sie bereits gewartet haben, erneut ausgeführt wird.
Dieser letzte Punkt ist der wichtigste. Das Ziel der Persistenz besteht nicht nur darin, Arbeit zu sparen; es geht auch darum, einer Anwendung zu ermöglichen, pausiert, fehlschlagen, neu gestartet oder gewartet zu werden und anschließend genau da weiterzumachen, wo sie aufgehört hat, damit die erledigte Arbeit erhalten bleibt.
Eine präzise Definition der Persistenz
Angesichts dieses Problems ist eine genaueere Definition nützlich: Persistenz ist die Fähigkeit eines Systems, seine internen Daten, seinen Zustand, in eine dauerhafte Speicherung zu schreiben, sodass die Daten länger bestehen als der aktuelle Lauf und erneut geladen werden können, um die Ausführung genau dort fortzusetzen, wo sie aufgehört hat.
Diese Definition umfasst drei unterschiedliche Fähigkeiten:
- Zustandsspeicherung: Das Aufzeichnen dessen, was der Agent derzeit weiß und was er getan hat.
- Aufruf einer früheren Ausführung: Das Zurücklesen dieser Aufzeichnung, auch nach einem Neustart.
- Fortsetzung des Workflows: Die Fortsetzung ab dem wiederhergestellten Punkt anstelle vom Anfang.
Vorübergehende Speicherung versus dauerhafter Speicher
Eine häufige Quelle der Verwirrung ist der Unterschied zwischen dem Halten von Daten und deren Persistenz. Ein reguläres Python-Dictionary, das den Dialog enthält, stellt vorübergehende Speicherung dar: Wenn der Prozess endet, ist das Dictionary verschwunden. Persistenz bedeutet, ein Snapshot dieser Daten anzufertigen und ihn an einem Ort zu speichern, der über den Prozess hinaus bestehen bleibt – in der Regel in einer Datenbank. Das ist die gesamte Idee; der Rest dieses Leitfadens erklärt, wie LangGraph dies umsetzt.
Was Ihnen Persistenz in der Produktion bringt
Neben dem Vermeiden von verlorenem Arbeitsergebnis verändert Persistenz die Art der Systeme, die Sie bauen können.
- Lange laufende Agenten. Agenten, die viele Seiten durchsuchen, große Datensätze verarbeiten oder auf externe Ereignisse warten, können jederzeit und auf jedem Gerät, das auf die Speicherung zugreifen kann, pausiert und wieder aufgenommen werden.
- Fehlertoleranz. Ein fehlertolerantes System verhält sich auch bei Abstürzen, Netzwerkfehlern oder Zeitüberschreitungen weiterhin korrekt. Durch Persistenz kostet ein Fehler nur den gerade ausgeführten Schritt, nicht die gesamte Aufgabe.
- Genehmigungsworkflows. Wenn eine Person etwas überprüfen muss, kann der Agent so lange anhalten, wie nötig, ohne etwas zu verlieren. Das Aufbauen solcher Workflows wird einfach, sobald der Zustand dauerhaft gespeichert ist.
Ein schneller Test, um herauszufinden, ob Sie es benötigen: Würde ein Benutzer unzufrieden sein, wenn der Prozess jetzt sofort neu gestartet würde? Wenn die Antwort ja lautet, benötigt das Diagramm Persistenz.
Zustand: das, was tatsächlich gespeichert wird
Persistenz in LangGraph macht nur dann Sinn, wenn man den Zustand versteht, denn das Speichern eines Diagramms bedeutet im Grunde das Speichern seines Zustands zu gut gewählten Zeitpunkten.
Der Zustand wird in der Regel als typisiertes Dictionary deklariert. Der erste Schritt ist die Importierung:
from typing import TypedDict
Das Schema listet anschließend die Schlüssel auf, mit denen das Graph arbeitet, sowie ihre Typen. Hier überwacht der Zustand eine Nachrichtenliste, den Namen des Benutzers sowie einen Schrittzähler:
class State(TypedDict):
messages: list
user_name: str
step_count: int
Weil State ein TypedDict ist, handelt es sich dabei einfach um ein Dictionary mit einer festgelegten Menge an Schlüsseln und deklarierten Typen. Jeder Knoten erhält den aktuellen Zustand und gibt eine teilweise Aktualisierung zurück, wobei LangGraph diese Aktualisierung in den gemeinsamen Zustand integriert.
Warum alles um den Zustand kreist
Der Zustand ist das Zentrum einer LangGraph-Anwendung:
- Knoten lesen ihn, um zu entscheiden, was sie tun sollen;
- Knoten schreiben ihre Ergebnisse wieder hinein;
- Kanten können je nach Werten darin zu verschiedenen Knoten weitergeleitet werden;
- Die Persistenz speichert und lädt ihn wieder.
Sobald das geklickt wurde, reduziert sich die Persistenz auf einen einzigen Satz: Nach jedem Schritt soll man ein Bild des Zustands machen und dieses an einem sicheren Ort speichern.
Schnappschüsse nach jedem Schritt
Bewahren Sie dieses Modell für den Rest des Leitfadens auf. Jedes Mal, wenn ein Knoten abgeschlossen ist, erfasst LangGraph den aktuellen Zustand und speichert ihn. Falls der Prozess unmittelbar nach Abschluss des zweiten Knotens abstürzt, existiert der Schnappschuss von diesem Moment weiterhin, sodass die Ausführung von dort statt vom ersten Knoten fortgesetzt werden kann. Dieser Schnappschuss hat einen Namen: ein Checkpoint.
Checkpoints: Schnappschüsse des Zustands zu einem bestimmten Zeitpunkt
Ein Checkpoint ist ein Schnappschuss des Zustands des Graphen zu einem bestimmten Zeitpunkt. Der Begriff stammt aus demselben Bereich wie in Videospielen: Es handelt sich um einen sicheren Punkt, zu dem man zurückkehren kann, anstatt das ganze Level erneut durchzuspielen.
Was Checkpoints ermöglichen
Ein Checkpoint beantwortet zuverlässig eine Frage: Wie sah der Zustand unmittelbar nach Abschluss eines bestimmten Schritts aus? Ohne Checkpoints kennt man nur den aktuellen Zustand – und das nur solange das Programm läuft. Mit ihnen kann man jeden früheren Zeitpunkt einer Ausführung überprüfen, und das System kann nach einem Fehler auf den neuesten Checkpoint zurückkehren.
Checkpoints werden automatisch für Sie erstellt
Ein Detail, das viele Anfänger überrascht, ist, dass man Checkpoints niemals manuell speichern muss. Sobald ein Checkpointer dem Graphen zugeordnet wurde, erstellt LangGraph nach jedem Super-Schritt einen neuen Checkpoint. Ein Super-Schritt entspricht einem Tick des Ausführungslaufs des Graphen; in einem einfachen linearen Graphen entspricht er dem Abschluss eines Knotens, während in einem Graphen mit parallelen Ästen alle im selben Tick geplanten Knoten zu einem Super-Schritt gehören. Man schreibt gewöhnlichen Graphenkod, und das Speichern erfolgt gleichzeitig damit.
Was ein Checkpoint enthält
Ein Checkpoint protokolliert in der Regel:
id: eine eindeutige Identifikationsnummer, so geordnet, dass spätere Checkpoints nach früheren sortiert werden;ts: wann der Checkpoint erstellt wurde;channel_values: die Zustandsdaten selbst, wie Nachrichten und andere Variablen zu diesem Zeitpunkt;channel_versions: interne Versionszähler, die LangGraph verwendet, um zu verfolgen, welche Teile des Zustands sich geändert haben;- Metadata: Buchhaltungsinformationen wie der Knoten, der den Checkpoint erzeugt hat, sowie die Schrittnummer.
Es ist nicht notwendig, dieses Layout auswendig zu lernen. Die funktionale Definition reicht aus: Ein Checkpoint ist ein Zeitpunktbild des Zustands zusammen mit einigen Buchhaltungsinformationen, die zu einem bestimmten Zeitpunkt aufgezeichnet werden. Wenn Sie wissen möchten, wie diese Elemente intern gespeichert werden – einschließlich ausstehender Schreibvorgänge und Blobs – finden Sie eine detailliertere Erklärung in Inside LangGraph's InMemorySaver.
Ein Zähler, Schritt für Schritt
Betrachten Sie ein Graphen mit einem Knoten, der eine Zahl erhöht, und stellen Sie sicher, dass dieser Vorgang dreimal hintereinander ausgeführt wird. Nach der ersten Ausführung enthält der gespeicherte Zustand {count: 1}, nach der zweiten {count: 2} und nach der dritten {count: 3}; jede dieser Werte stellt dabei einen eigenen Checkpoint dar. Wenn das Programm unmittelbar nach der Erstellung des zweiten Checkpoints abstürzt, kann ein Neustart von {count: 2} aus fortgesetzt werden, ohne die ersten beiden Erhöhungen wiederholen zu müssen.
Checkpointer: Der Komponente, die speichert und lädt
Wenn ein Checkpoint eine Momentaufnahme darstellt, dann ist der Checkpointer die Komponente, die diese Momentaufnahmen erstellt, speichert und wieder abruft. Er stellt die Persistenzschicht von LangGraph dar.
Eine passende Analogie ist eine Kamera mit einem integrierten Aktenschrank. Immer wenn ein Knoten abgeschlossen ist, macht die Kamera ein Bild vom Zustand und speichert es ab. Der Aktenschrank kann Prozessspeicher, eine lokale Datei oder eine Datenbank sein – je nachdem, welchen Checkpointer man wählt.
Drei Aufgaben eines Checkpointers
Ein Checkpointer ist für Folgendes verantwortlich:
- Zustand speichern: Das neue Snapshot wird nach jedem Schritt in den Speicher geschrieben.
- Zustand laden: Das neueste Snapshot wird wieder gelesen, wenn der Graph für denselben Thread erneut aufgerufen wird (Threads werden im nächsten Abschnitt behandelt).
- Ausführung wiederherstellen: Das Snapshot wird an die Laufzeitumgebung übergeben, damit der Graph von dort weiterläuft, wo er aufgehört hat, anstatt neu zu starten.
Einbindung eines Checkpointers zur Kompilierzeit
Die Persistenz wird aktiviert, wenn der Graph kompiliert wird. Man importiert eine Checkpointer-Klasse sowie den Graph-Builder; die Quelle kennzeichnet diesen Codeausschnitt als JavaScript, doch es handelt sich dabei um Python:
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
Dann erstellt man den Checkpointer und gibt ihn an compile() weiter. Beachten Sie, dass im abgedruckten Codeausschnitt der Kommentar sowie die Zuweisung checkpointer = InMemorySaver() auf einer Zeile zusammengefasst wurden, wodurch die Zuweisung zum Teil des Kommentars würde; im echten Code gehören sie auf getrennte Zeilen:
# ... assume `builder` is a StateGraph you've already defined ...checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
Dieser eine Argument checkpointer=checkpointer aktiviert die Persistenz für den gesamten Graph. Ohne ihn speichert LangGraph nichts, und jeder invoke()-Aufruf beginnt mit einem leeren Zustand.
Der entstehende Zyklus besteht aus Laden, Ausführen und Speichern, der nach jedem Schritt wiederholt wird. Deshalb wirkt die Persistenz automatisch: Man ruft nie selbst die Speichern- oder Ladenfunktionen auf, denn der kompilierte Graph erledigt dies im Rahmen der normalen Ausführung.
Eine Warnung, die leicht übersehen wird: Ein Checkpointer speichert nur die Graphen, an die er weitergeleitet wurde. Wenn derselbe Datei ohne Checkpointer ein zweiter Graph kompiliert wird, hat dieser zweite Graph überhaupt keine Persistenz.
Threads: Gespräche voneinander trennen
Der Wert thread_id kommt im gesamten LangGraph-Code vor und erfordert eine präzise Erklärung. Ein Thread repräsentiert ein kontinuierliches Gespräch oder eine Aufgabe. Jeder Thread hat eine eindeutige ID, und jeder für dieses Gespräch erzeugte Checkpoint wird unter dieser ID gruppiert.
Warum jedes Gespräch seinen eigenen Thread benötigt
Stellen Sie sich einen Support-Chattenbot vor, der gleichzeitig Tausende von Kunden bedient. Ein Kunde fragt nach einer Rückerstattung, während ein anderer sich über eine verspätete Lieferung beschwert. Es handelt sich dabei um getrennte Gespräche, die parallel ablaufen; sie zu vermischen – beispielsweise dem ersten Kunden von der Sendung des zweiten Kunden zu erzählen – wäre ein schwerwiegender Fehler.
Thread-IDs verhindern das, indem sie wie Etiketten auf Ordnern funktionieren. Jeder Zustandspunkt wird unter genau einer Thread-ID abgelegt, sodass sich die Informationen aus verschiedenen Gesprächen niemals vermischen.
Übermittlung einer Thread-ID im Code
Der Thread wird über ein Konfigurationswörterbuch ausgewählt. Die thread_id befindet sich unter dem Schlüssel configurable (dieser und der folgende Codeausschnitt sind in Python, nicht als reiner Text):
config = {"configurable": {"thread_id": "customer-a-session-101"}}
Diese Konfiguration wird bei jedem Aufruf zusammen mit der Eingabe übermittelt:
result = graph.invoke({"messages": [{"role": "user", "content": "Where's my refund?"}]}, config)
Jeder Aufruf von graph.invoke() oder graph.stream() benötigt eine config, die den thread_id enthält. LangGraph verwendet diesen, um:
- bestehende Checkpoints für diesen Thread zu suchen, falls historische Daten geladen werden sollen;
- neue Checkpoints unter derselben ID zu speichern, solange die Ausführung weitergeht.
Falls ein anderer thread_id verwendet wird, entsteht ein neuer, leerer Dialog, als wäre der Zustand zurückgesetzt worden – obwohl das kompilierte Graph und die Checkpoint-Objekte exakt dieselben sind.
Wie Threads mit echten Produkten übereinstimmen
- Eine Chat-Schnittstelle: Jeder geöffnete Chat ist im Grunde ein eigener Thread, und das Wechseln zwischen Chats entspricht dem Wechseln der Thread-IDs. Was man in einem Dialog sagt, dringt nicht in einen anderen ein.
Eine praktische Folge ist, dass Thread-IDs bewusst erzeugt und gespeichert werden sollten – beispielsweise aus einer Ticketnummer oder einer Session-ID in der eigenen Datenbank abgeleitet. Wenn die ID verloren geht, existieren die Zwischenstände zwar weiterhin, doch es gibt nichts, was auf sie hinweist.
Ein kompiliertes Diagramm, viele Threads
Man benötigt keinen Graphen pro Benutzer. Ein einmal kompilierter Graph kann gleichzeitig für beliebig viele Threads genutzt werden; stattdessen ist ein eindeutiger Thread-ID pro Benutzer oder pro Konversation erforderlich. Der Graph definiert das Verhalten, der Thread hingegen bestimmt, auf welchem Zustand dieses Verhalten angewendet wird.
Von Beginn bis zum Absturz und der Wiederaufnahme einer Ausführung
Durch die Kombination von Zuständen, Kontrollpunkten, dem Checkpointer sowie Threads erhält man ein vollständiges Bild davon, was während der Ausführung geschieht. Der untenstehende Flussdiagramm (in Mermaid-Syntax, als Text dargestellt) verfolgt eine Ausführungsschleife, einschließlich eines Absturzes und des anschließenden Rückkehrpfades:
flowchart TD
A[1. Graph starts with invoke] --> B[2. Checkpointer checks thread_id for existing State]
B --> C{State exists for this thread?}
C -->|Yes| D[3a. Load last saved State]
C -->|No| E[3b. Start with fresh empty State]
D --> F[4. Node executes]
E --> F
F --> G[5. State updates in memory]
G --> H[6. Checkpoint saved to storage]
H --> I{More nodes to run?}
I -->|Yes| F
I -->|No| J[7. Return final result to caller]
H -.->|💥 Crash happens here| K[Process restarts]
K --> B
In Prosa lautet die Abfolge wie folgt:
- Der Graph wird gestartet. Man ruft
graph.invoke(input, config)mit einem bestimmtenthread_idauf.
Es ist kein separater Wiederherstellungsmodus zu implementieren. Ein erneuter Aufruf des Graphen im selben Thread reicht aus, damit LangGraph herausfindet, wo mit der Ausführung fortgefahren werden soll.
Ein Detail erfordert Genauigkeit. Um eine unterbrochene oder fehlgeschlagene Ausführung fortzusetzen, rufen Sie das Graph mit None als Eingabe sowie derselben Konfiguration auf – dadurch weist man LangGraph an, von dem gespeicherten Checkpoint aus weiterzumachen anstatt eine neue Ausführung zu starten. Die Übermittlung neuer Eingaben an einen bestehenden Thread startet eine neue Ausführung, die auf dem gespeicherten Zustand aufbaut: Schlüssel mit Reduzierfunktionen, wie beispielsweise eine Nachrichtenliste, werden akkumuliert, während einfache Schlüssel durch den neuen Wert überschrieben werden. Mit graph.get_state(config) können Sie den neuesten Zustandsabriss einsehen und mit graph.get_state_history(config) die vollständige Sequenz.
Derselbe Mechanismus ermöglicht es einem Agenten, absichtlich anzuhalten – beispielsweise um auf eine menschliche Entscheidung zu warten – und Stunden oder Tage später auf einer anderen Maschine fortzusetzen, vorausgesetzt, diese Maschine kann auf denselben persistenten Speicher zugreifen. Um dieses Muster aus der Praxis zu sehen, lesen Sie „Pausing and resuming LangGraph agents with interrupt and Command“.
Der einfachste Backend: InMemorySaver
LangGraph bindet Sie nicht an ein bestimmtes Speichersystem. Es unterstützt mehrere Backends, also Orte, an denen Checkpoints physisch gespeichert werden, und diese unterscheiden sich hauptsächlich in ihrer Langlebigkeit sowie darin, wie viele Prozesse sie teilen können. Der einfachste ist InMemorySaver.
Was es ist und wie es Checkpoints speichert
InMemorySaver ist ein Checkpointer, der jeden Checkpoint im RAM des aktuellen Python-Prozesses in einem gewöhnlichen In-Memory-Dictionary unter Verwendung der Thread-ID speichert. Es gibt weder Dateien noch Datenbanken, nur ein Python-Objekt.
Im ersten Beispiel wird ein typisierter Zustand mit einer einzigen count-Schlüssel verwendet sowie ein Knoten, der diesen Wert um eins erhöht. Der Knoten ist von START bis END verbunden, das Diagramm wird mit einem InMemorySaver kompiliert und auf thread-1 mit einem Anfangswert von 1 aufgerufen, was das Ergebnis 2 ergibt. Die Quelle gibt an, es handele sich um JavaScript, doch in Wirklichkeit ist es Python; außerdem wird angenommen, dass TypedDict, StateGraph, START, END und InMemorySaver bereits zuvor importiert wurden:
class StateInt(TypedDict):
count: int
def add_one(state: StateInt) -> dict:
return {"count": state["count"] + 1}
builder = StateGraph(StateInt)
builder.add_node("add_one", add_one)
builder.add_edge(START, "add_one")
builder.add_edge("add_one", END)
memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)
config = {"configurable": {"thread_id": "thread-1"}}
result = graph.invoke({"count": 1}, config)
print(result) # {'count': 2}
Der folgende Auszug ist lediglich eine Beschriftung, die eine minimale Version des Graphen mit der realistischeren Variante oben vergleicht:
Two ways to write the same graph — minimal vs. real-world.
Die minimale Variante benötigt asyncio zusätzlich zum Checkpointer und zum Graphen-Bauwerk:
import asyncio
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph
Dann wird ein einfacher int als gesamter Zustand verwendet, eine Lambda wird als Knoten registriert, dieser Knoten wird sowohl als Eingangs- als auch als Endpunkt markiert, und der Graph wird asynchron mit ainvoke ausgeführt. Wie abgedruckt wurden mehrere Anweisungen auf derselben Zeile zusammen ausgeführt (zum Beispiel der Aufruf von set_finish_point und die Zuweisung an InMemorySaver()), daher sollten sie vor der Ausführung getrennt werden; der Auszug ist trotz seiner Bezeichnung ebenfalls in Python geschrieben:
builder = StateGraph(int)
builder.add_node("add_one", lambda x: x + 1)
builder.set_entry_point("add_one")
builder.set_finish_point("add_one")memory = InMemorySaver()
graph = builder.compile(checkpointer=memory)config = {"configurable": {"thread_id": "thread-1"}}
result = asyncio.run(graph.ainvoke(1, config))
print(result) # Output: 2
Die wichtigsten Zeilen im Überblick:
InMemorySaver()erstellt einen leeren Checkpoint-Speicher im Speicher.
builder.compile(checkpointer=memory) fügt diesen Speicher dem Graphen hinzu, wodurch die Persistenz aktiviert wird.config = {„configurable“: [0]} verknüpft den Aufruf mit einer bestimmten Konversation.graph.ainvoke(1, config) ist der asynchrone Eingangspunkt; hier wird mit der Zahl 1 als Zustand auf dem Thread „thread-1“ gestartet.Ein erneuter Aufruf mit derselben thread_id arbeitet mit den bereits für diesen Thread gespeicherten Checkpoints statt mit einer leeren Historie. Beachten Sie den Unterschied zur vorherigen Abschnitt: In diesem kleinen Graphen ist die Ausführung bereits abgeschlossen und der Zustand besteht aus einem einzigen überschriebenen Wert; daher ersetzt neuer Eingang diesen einfach, während None als Eingabe eine unvollendete Ausführung fortsetzen würde.
Vorteile
- Keine Einrichtung nötig: Es werden weder eine Datenbank noch ein externer Dienst benötigt.
- Sehr schnell, da es keine Verzögerungen durch Festplatte oder Netzwerk gibt.
- Sehr geeignet für Unit-Tests.
- Praktisch zum Lernen sowie für Experimente in Notebooks.
Einschränkungen
- Alles verschwindet, wenn der Prozess gestoppt wird. Es handelt sich dabei um reine RAM, was genau das Problem ist, das zu Beginn dieses Leitfadens beschrieben wurde.
- Es kann nicht zwischen Prozessen oder Servern geteilt werden, da jeder Prozess sein eigenes Speicherbereich hat.
- Es ist für die Produktion nicht sicher, da ein einfacher Neustart alle Gespräche löscht.
Wann man darauf zurückgreifen sollte
Angemessene Anwendungsbereiche sind:
- lokaler Entwicklungs- und Debugging-Prozess;
- automatisierte Tests, sowohl Unit-Tests als auch CI-Pipelines;
- schnelle Prototypen und Notebooks, bei denen es nicht darauf ankommt, dass sie nach einem Neustart weiterhin funktionieren.
Alles, worauf echte Benutzer angewiesen sind, benötigt einen durch eine Datenbank unterstützten Checkpointer. In der LangGraph-Dokumentation wird ausdrücklich darauf hingewiesen, dass InMemorySaver ausschließlich zum Debuggen und Testen bestimmt ist, und es wird eine zuverlässige Implementierung wie PostgresSaver für die Produktion empfohlen. Die Einrichtung solcher Lösungen sowie weiterer Backends wie SQLite und Redis, benutzerdefinierter Checkpointers und Genehmigungsworkflows, die auf interrupt() und Command basieren, ist der natürliche nächste Schritt; ein für die Produktion geeignetes Beispiel zur Ausführung von LangGraph auf Postgres und Redis wird in Selbsthosting eines LangGraph-Agent-Servers beschrieben.
Haupterkenntnisse
- Durch Persistenz werden der Zustand des Graphen in eine zuverlässige Speicherung geschrieben, sodass ein Lauf auch nach Abstürzen, Neustarts und langen Wartezeiten weiterläuft, anstatt von vorn zu beginnen.
compile().thread_id isoliert ein Gespräch oder eine Aufgabe, sodass ein einziger kompiliertes Graph sicherlich vielen Benutzern dienen kann.None aufzurufen; neue Eingaben an einem bestehenden Thread starten eine neue Ausführung auf der Basis des gespeicherten Zustands.InMemorySaver eignet sich ideal für Tests und Prototypen, doch da er im Prozessspeicher vorhanden ist, benötigen Produktionsysteme einen durch eine Datenbank unterstützten Checkpointer.