Startseite / Artikel / Im Inneren von LangGraphs InMemorySaver: Wie Checkpoints, Schreibvorgänge und Blobs zusammenpassen

Im Inneren von LangGraphs InMemorySaver: Wie Checkpoints, Schreibvorgänge und Blobs zusammenpassen

Gehen Sie durch den Speicher, schreiben Sie Wörterbücher für Schriftzeichen und Blob-Daten in LangGraphs InMemorySaver und verfolgen Sie, wie eine einzige kleine Graphenverarbeitung in drei miteinander verbundene Checkpoints umgewandelt wird.

1834 Wörter

LangGraphs InMemorySaver ist in der Regel eine Einzeileneinstellung: Man gibt ihn an compile() weiter, die Konversationen erinnern sich plötzlich an ihren Zustand, und niemand schaut weiter darauf. Doch die Art und Weise, wie er Daten strukturiert, verrät viel über LangGraph selbst – einschließlich der Funktionsweise von Wiederaufnahmen, Zeitreisen und Fehlertoleranz sowie darüber, warum persistente Checkpointer so gestaltet sind. Indem man einen minimalen Graphen durch die internen Wörterbücher des Savers verfolgt, kann man einen Checkpoint-Export lesen und genau wissen, was jeder Eintrag bedeutet.

Warum Graphen Checkpoints benötigen

Ein Checkpointer fungiert als Kurzzeitgedächtnis für ein Graph: Er erfasst im Laufe der Ausführung einen Zeitpunktbild des Zustands des Graphs. Stellen Sie sich Save-Points in einem Story-Modus-Spiel vor: Ohne sie bedeutet das Wiederspielen des zweiten Levels, zuerst das erste Level noch einmal spielen zu müssen. Ein Save speichert den Fortschritt des Spielers, sodass man von diesem Moment aus fortfahren kann – auch nach Beendigung des Spiels. LangGraph tut dasselbe nach jedem Schritt, damit ein Thread von einem früheren Punkt aus fortgesetzt oder wiedergespielt werden kann.

Ein minimaler Graph zum Inspezieren

Das untenstehende Beispiel erstellt das kleinste nützliche Diagramm: einen typisierten Zustand mit name und address, einen einzigen deterministischen Knoten, der beide Felder über eine Command setzt, sowie Kanten START, anschließend get_address und schließlich END. Es kompiliert das Diagramm mit einem InMemorySaver und einem InMemoryStore, ruft es auf dem Thread "12345" auf und gibt schließlich die Attribute des Checkpointers aus. Der Store ist ein separates Komponenten für langfristig gespeicherte Daten, die zwischen Threads geteilt werden, und spielt bei den folgenden Schritten keine Rolle. Obwohl der Codeausschnitt als JavaScript gekennzeichnet ist, handelt es sich dabei um Python:

from langgraph.checkpoint.memory import InMemorySaver
from langgraph.store.memory import InMemoryStore
from langgraph.graph import StateGraph
from typing import TypedDict, Literal
from langgraph.types import Command
from langgraph.graph.state import START, END

# we create a checkpointer, for now testing purposes we use inmemory
checkpointer = InMemorySaver()

# we will talk about this in our next blog
store = InMemoryStore()


# how you want to store your graph state which is persisted across chats
class GraphState(TypedDict):
    name: str
    address: str

# this is a determinsitic node that is present as a node
def get_address(state: GraphState) -> Command[Literal[END]]:
    return Command(update={
        "name": "pavaneeshwar",
        "address": "Hyderabad residency"
    })

# intialize graph
graph = StateGraph(GraphState)

# add this node to the graph
graph.add_node("get_address", get_address)

# by default START and END defines the START execution and end execution
graph.add_edge(START, "get_address")
graph.add_edge("get_address", END)

# the above graph we created is START => get_address => END

# we load the entire graph, this returns an object which we can run
app = graph.compile(checkpointer=checkpointer, store=store)

app.invoke({}, config={"configurable": {"thread_id": "12345"}})

# we are interested here how langgraph stores checkpointer
app.checkpointer.__dict__

Die Attribute von InMemorySaver

Die Auflistung der Schlüssel des Dictionarys des Checkpointers zeigt fünf Attribute:

app.checkpointer.__dict__.keys()
# dict_keys(['serde', 'storage', 'writes', 'blobs', 'stack'])

serde: Serialisierung und Deserialisierung

Checkpoint-Daten können nicht als lebende Python-Objekte in einer Datenbank gespeichert werden, und selbst im Speicher wird das Seriellisierungsverfahren verwendet, um sie in einer serialisierten Form zu speichern. serde ist das Seriellisierungsverfahren, das Werte in Bytes umwandelt und wieder zurück, wobei jeder Wert mit einem Typ wie msgpack gekennzeichnet wird.

Speicherung: Checkpoints pro Thread

storage enthält die Checkpoints selbst. Jede Konversation erhält eine Thread-ID, und anhand dieser ID holt LangGraph den Historieverlauf eines bestimmten Threads ab. Die Struktur besteht aus einem verschachtelten Dictionary: zunächst die Thread-ID, anschließend der Namespace des Checkpoints (eine leere Zeichenkette für das oberste Niveau des Graphen; Untergraphen erhalten ihre eigenen Namespaces) und schließlich die ID des Checkpoints:

{
    "thread_id": {
         "namespace" : {
            "checkpoint_uuid_0": (msgpack, <binary_data>),
            "checkpoint_uuid_1": (msgpack,<binary_data>, checkpoint_uuid_0),
            "checkpoint_uuid_2": (msgpack,<binary_data>, checkpoint_uuid_1),
         }
    }
}

Jeder Eintrag enthält den serialisierten Checkpoint, seine serialisierten Metadaten sowie die ID des übergeordneten Checkpoints. Dieser Übergeordnungszeiger verwandelt die Checkpoints eines Threads in eine verknüpfte Historie, wodurch das Zurückspulen und Aufteilen möglich wird.

Schreibvorgänge: ausstehende Schreibvorgänge pro Checkpoint

writes protokolliert die einzelnen Aktualisierungen, die von Aufgaben erzeugt werden. Anstatt den Zustand direkt zu überschreiben, wird jede Aktualisierung als neuer Eintrag mit Schlüsseln aus Thread, Namespace und dem Checkpoint protokolliert, von dem aus die Aufgabe ausgeführt wurde. Innerhalb dieses Eintrags wird jede Schreibvorgang durch eine Aufgaben-ID und einen Index identifiziert:

{
    ('thread_id', 'namespace', 'checkpoint_uuid_1') : {
        ('operation_uuid_1', 0) : ('operation_uuid_1', 'channel_name', ('msgpack', '<binary data>')),
        ('operation_uuid_2', 1) : ('operation_uuid_2', 'channel_name', ('msgpack', '<binary data>'))
    }
}

Der channel_name in diesem Sketch ist ein Platzhalter. Wenn ein Knoten name aktualisiert, lautet der Channel-Name name; wenn er address aktualisiert, lautet der Channel-Name address. Ein Knoten, der beides gleichzeitig aktualisiert, erzeugt zwei Einträge unter derselben Checkpoint. Da Schreibvorgänge sofort gespeichert werden, sobald eine Aufgabe abgeschlossen ist, muss ein Lauf, der mitten in einem Schritt fehlschlägt, die bereits erfolgreich abgeschlossenen Aufgaben nicht erneut ausführen.

blobs: versionierte Channel-Werte

blobs speichert den tatsächlichen Wert jedes Channels in jeder Version. Der Schlüssel kombiniert Thread, Namespace, Channel und Version, sodass ein Checkpoint auf einen Channel-Wert per Version verweisen kann, anstatt eine Kopie einzubetten:

{
    ('thread_id', 'namespace', 'channel_name', 'version') : ('mssgpack', '<binary data>')
}

stack: Kontextverwaltung

Das Attribut stack wird manchmal als Warteschlange für ausstehende Aufgaben beschrieben, doch in der Implementierung des Savers handelt es sich um einen Stack für Kontextmanager (einen ExitStack), der dazu dient, Ressourcen zu verwalten, wenn der Saver als Kontextmanager betreten und verlassen wird. Er speichert nicht den Ausführungszustand des Graphen. Dies sind interne Strukturen, die nur für den eigenen Gebrauch bestimmt sind; überprüfen Sie sie daher anhand Ihrer installierten Version.

Schritt für Schritt den Ablauf nachverfolgen

Jede Aufrufung des Graphen erzeugt drei Checkpoints.

Checkpoint 1: Die Eingabe trifft ein

Der erste Checkpoint mit der ID 1f1b054e-b2a5-660a-bfff-7484776ebce0 enthält zwei MsgPack-Datenpakete: den Checkpoint selbst sowie seine Metadaten.

// First Message pack
{
  "v": 4,
  "ts": "2026-09-14T15:56:59.435773+00:00",
  "id": "1f1b054e-b2a5-660a-bfff-7484776ebce0",
  "channel_versions": {
    "__start__": "00000000000000000000000000000001.0.267464090313665"
  },
  "versions_seen": {
    "__input__": {}
  },
  "updated_channels": [
    "__start__"
  ]
}

// Second Message Pack, this is just meta data

{
  "source": "input",
  "step": -1,
  "parents": {}
}

Zu diesem Zeitpunkt existiert nur der Kanal __start__. Er hat bereits seine erste Version erhalten, diese wird in updated_channels aufgelistet, und die Metadaten kennzeichnen die Quelle als input mit einem auf -1 gesetzten step, was bedeutet, dass dies der Zustand vor dem Ausführen irgendeines Graphenschritts ist. Die Versionsstrings folgen einem einfachen Schema: ein mit Nullen aufgefüllter, monoton steigender Zähler, gefolgt von einer zufälligen Zahl, die dafür sorgt, dass die Versionen eindeutig sind.

Der Checkpoint bezieht sich auf den Wert des Kanals über seine Version, und der entsprechende Blob enthält die Daten. Hier war die Eingabe ein leeres Dictionary, das von msgpack als das einzige Byte \x80 kodiert wird:

// this msgpack basically {}
('12345', '', '__start__', '00000000000000000000000000000001.0.267464090313665'): ('msgpack', b'\x80')

Checkpoint 2: Routing zum Knoten

Die zweite Kontrollstelle, 1f1b054e-b2a6-6294-8000-96e3a3cb81ac, protokolliert den Pfad von START zu get_address. Es geht dabei um die Routenplanung, noch nicht um den Ausführungsprozess des Nodes:

// first message pack
{
  "v": 4,
  "ts": "2026-09-14T15:56:59.436094+00:00",
  "id": "1f1b054e-b2a6-6294-8000-96e3a3cb81ac",
  "channel_versions": {
    "__start__": "00000000000000000000000000000002.0.27282425125643517",
    "branch:to:get_address": "00000000000000000000000000000002.0.27282425125643517"
  },
  "versions_seen": {
    "__input__": {},
    "__start__": {
      "__start__": "00000000000000000000000000000001.0.267464090313665"
    }
  },
  "updated_channels": [
    "branch:to:get_address"
  ]
}

// second message pack
{
  "source": "loop",
  "step": 0,
  "parents": {}
}

Zwei Kanäle transportieren nun Version 2. __start__ wechselt zu einer neuen Version, da seine Eingabe verbraucht wurde, und ein neuer Kanal, branch:to:get_address, signalisiert, dass get_address als Nächstes ausgeführt werden soll. versions_seen zeigt an, dass die Aufgabe __start__ bereits Version 1 des __start__-Kanals gesehen hat; durch diese Protokollierung entscheidet LangGraph, welche Nodes noch ausgeführt werden müssen. Die Metadaten wechseln auf die Quelle loop mit step 0.

Die Schreiboperation, die diesen Übergang auslöste, wird unter der ID der vorherigen Kontrollstelle gespeichert, da sie von der Aufgabe erzeugt wurde, die von dieser Kontrollstelle aus ausgeführt wurde:

('12345', '', '1f1b054e-b2a5-660a-bfff-7484776ebce0'): {
        ('4efa087d-283c-eb5c-478a-97c592eb3802', 0): ('4efa087d-283c-eb5c-478a-97c592eb3802', 'branch:to:get_address', ('null', b''), '~__pregel_pull, __start__')
 }

Zwei neue Blob-Objekte werden ebenfalls erstellt. Der __start__-Blob wird mit dem Tag empty versehen, was darauf hindeutet, dass der Kanal nach seiner Verwendung geleert wurde, und der Branch-Kanal speichert einen null-Wert, da er lediglich als Auslöser fungiert:

// one created for progressing start
('12345', '', '__start__', '00000000000000000000000000000002.0.27282425125643517'): ('empty', b''),

// one for creating branch
('12345', '', 'branch:to:get_address', '00000000000000000000000000000002.0.27282425125643517'): ('null', b'')

Checkpoint 3: Der Knoten aktualisiert den Zustand

Der dritte Checkpoint, 1f1b054e-b2a6-6d66-8001-d006da4d6d19, erfasst die Ausführung von get_address sowie dessen Aktualisierungen von name und address:

// first message pack
{
  "v": 4,
  "ts": "2026-09-14T15:56:59.436372+00:00",
  "id": "1f1b054e-b2a6-6d66-8001-d006da4d6d19",
  "channel_versions": {
    "__start__": "00000000000000000000000000000002.0.27282425125643517",
    "branch:to:get_address": "00000000000000000000000000000003.0.07103778333502464",
    "name": "00000000000000000000000000000003.0.07103778333502464",
    "address": "00000000000000000000000000000003.0.07103778333502464"
  },
  "versions_seen": {
    "__input__": {},
    "__start__": {
      "__start__": "00000000000000000000000000000001.0.267464090313665"
    },
    "get_address": {
      "branch:to:get_address": "00000000000000000000000000000002.0.27282425125643517"
    }
  },
  "updated_channels": [
    "address",
    "name"
  ]
}

// second message pack
{
  "source": "loop",
  "step": 1,
  "parents": {}
}

channel_versions enthält stets die neueste Version jedes Channels, während versions_seen festhält, welche Versionen jeder Knoten beim Ausführen gesehen hat. __start__ bleibt bei Version 2, da er nicht mehr verändert wird. Der Branch-Channel sowie die beiden State-Channels wechseln auf Version 3, updated_channels listet address und name auf, und der Schrittzähler erreicht 1.

Der Knoten hat zwei Werte geschrieben, weshalb unter der ID des zweiten Checkpoints zwei Einträge zu sehen sind – jeweils einer pro Channel – die denselben Task-ID teilen:

('12345', '', '1f1b054e-b2a6-6294-8000-96e3a3cb81ac'): {
        ('a6b6f3e8-32e4-88a4-559d-cd6d409c7910', 0): ('a6b6f3e8-32e4-88a4-559d-cd6d409c7910', 'name', ('msgpack', b'\xacpavaneeshwar'), '~__pregel_pull, get_address'),
        ('a6b6f3e8-32e4-88a4-559d-cd6d409c7910', 1): ('a6b6f3e8-32e4-88a4-559d-cd6d409c7910', 'address', ('msgpack', b'\xb3Hyderabad residency'), '~__pregel_pull, get_address')
}

Schließlich enthalten die neuen Blob-Dateien die in msgpack kodierten Zeichenketten für die beiden State-Felder:

('12345', '', 'name', '00000000000000000000000000000003.0.07103778333502464'): ('msgpack', b'\xacpavaneeshwar'),
('12345', '', 'address', '00000000000000000000000000000003.0.07103778333502464'): ('msgpack', b'\xb3Hyderabad residency')

Warum die Struktur so gestaltet ist

Drei Wörterbücher für ein Graphen mit einem Knoten erscheinen übertrieben, doch jeder Bestandteil hat seinen Sinn:

  • Eltern-bezogene Checkpoints liefern jedem Thread eine vollständige Historie. Sie können jeden früheren Zustand überprüfen, von dort aus fortfahren oder einen neuen Zweig davon erstellen.
  • Versionierte Blob-Daten speichern jeden Kanalwert einmal pro Änderung, sodass die Checkpoints klein bleiben, selbst wenn der Zustand groß und größtenteils unverändert ist.
  • Ausstehende Schreibvorgänge ermöglichen das Wiederaufnehmen von Schritten. Wenn eine Aufgabe in einem Schritt fehlschlägt, wurden die Schreibvorgänge der erfolgreichen Aufgaben bereits gespeichert und müssen nicht erneut ausgeführt werden.

Persistente Checkpointer wie der von Postgres speichern Checkpoints, Blob-Daten und Schreibvorgänge in separaten Tabellen, die diesen Strukturen entsprechen, sodass dasselbe Konzept auch auf Ihre Datenbank angewandt werden kann.

Haupterkenntnisse

  • InMemorySaver ist für Entwicklung und Tests gedacht; seine Daten verschwinden, wenn der Prozess beendet wird.
  • storage enthält Checkpoints sowie Metadaten pro Thread und Namespace, die über Eltern-IDs miteinander verknüpft sind.
  • writes enthält Updates pro Aufgabe, die nach dem jeweiligen Checkpoint geordnet sind, aus dem sie stammen.
  • blobs speichert Kanalwerte nach Version, sodass unveränderte Kanäle niemals kopiert werden.
  • Zusätzliche Literatur