Strona główna / Artykuły / Wewnątrz LangGraph's InMemorySaver: Jak pasują punkty kontrolne, zapisy i Blobs

Wewnątrz LangGraph's InMemorySaver: Jak pasują punkty kontrolne, zapisy i Blobs

Przejdź przez strukturę przechowywania, zapisz słowniki typu „writes” i „blobs” wewnątrz LangGraph’s InMemorySaver oraz prześledź, jak jeden mały graf po zakończeniu obliczeń przekształca się w trzy powiązane punkty kontrolne.

1834 słów

InMemorySaver z LangGraph to zazwyczaj prosty element konfiguracji – przekazuje się go do funkcji compile(), rozmowy nagle pamiętają swój stan, a nikt nie bada tematu dalej. Jednak sposób, w jaki ten mechanizm organizuje dane, wiele wyjaśnia na temat samego LangGraph, w tym to, jak działają funkcje wznowienia pracy, podróży w czasie i tolerancja na błędy, oraz dlaczego narzędzia do tworzenia punktów kontrolnych mają taką strukturę. Analizując minimalny graf przetwarzany przez wewnętrzne słowniki tego narzędzia, będziesz mógł odczytać plik z punktem kontrolnym i dokładnie wiedzieć, co oznacza każda pozycja.

Dlaczego grafy potrzebują punktów kontrolnych

Checkpointer pełni rolę krótkoterminowej pamięci dla grafu: rejestruje jego stan w momencie wykonania. Pomyśl o punktach zapisu w grze w trybie opowieści – bez nich, aby ponownie przejść poziom drugi, trzeba najpierw przejść poziom pierwszy. Punkt zapisu przechowuje postępy gracza, dzięki czemu można kontynuować od tego momentu, nawet po zakończeniu gry. LangGraph robi to samo po każdym kroku, więc wątek może zostać wznowiony lub odtworzony od wcześniejszego punktu.

Minimalny graf do zbadania

Poniższy przykład tworzy najmniejszy użyteczny graf: typowany stan z polami name i address, pojedynczy deterministyczny węzeł, który ustawia oba pola za pomocą Command, oraz krawędzie START, następnie get_address, a na końcu END. Graf jest kompilowany przy użyciu InMemorySaver i InMemoryStore, uruchamiany na wątku "12345", a na koniec wyświetlane są atrybuty checkpointera. InMemoryStore to odrębny komponent służący do przechowywania danych długoterminowych dostępnych pomiędzy wątkami i nie odgrywa żadnej roli w dalszych krokach. Chociaż fragment ten jest oznaczony jako JavaScript, w rzeczywistości jest napisany w Pythonie:

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__

Atrybuty InMemorySaver

Wylistowanie kluczy słownika checkpointera pokazuje pięć atrybutów:

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

serde: serializacja i deserializacja

Dane punktów kontrolnych nie mogą być przechowywane jako żywe obiekty Pythona w bazie danych, a nawet w pamięci są przechowywane w formie zserwowanej. serde to narzędzie seryjalizacji, które przekształca wartości w bajty i z powrotem, oznaczając każdą z nich typem, np. msgpack.

przechowywanie: punkty kontrolne na wątek

storage przechowuje same punkty kontrolne. Każda rozmowa otrzymuje identyfikator wątku, który służy LangGraph do odzyskiwania historii konkretnego wątku. Struktura składa się z zagłębionego słownika: identyfikator wątku, następnie przestrzeń nazw punktu kontrolnego (pusta strona dla grafu najwyższego poziomu; podgrafy mają własne przestrzenie nazw), a na końcu identyfikator punktu kontrolnego:

{
    "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),
         }
    }
}

Każda pozycja zawiera zserializowany punkt kontrolny, jego zserializowane metadane oraz ID rodzicielskiego punktu kontrolnego. Ten wskaźnik rodzica przekształca punkty kontrolne wątku w łączoną historię, co umożliwia cofanie i rozgałęzianie się.

writes: liczba zaległych zapisów na punkt kontrolny

writes rejestruje poszczególne aktualizacje generowane przez zadania. Zamiast nadpisywać stan na miejscu, każda aktualizacja jest zapisywana jako nowa pozycja oznaczona wątkiem, przestrzenią nazw oraz punktem kontrolnym, od którego uruchomione zostało zadanie. Wewnątrz każdy zapis jest identyfikowany przez ID zadania i indeks:

{
    ('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>'))
    }
}

channel_name w tym szkicu jest tylko zastępcą. Gdy węzeł aktualizuje name, kanałem staje się name; gdy aktualizuje address, kanałem staje się address. Węzeł, który aktualizuje oba parametry jednocześnie, tworzy dwa wpisy pod tym samym punktem kontrolnym. Ponieważ zapisy są przechowywane natychmiast po zakończeniu zadania, wykonywanie, które zawodzi w trakcie jakiegoś kroku, nie musi ponownie wykonywać zadań, które już się udały.

blobs: wersjonowane wartości kanałów

blobs przechowuje rzeczywistą wartość każdego kanału w poszczególnych wersjach. Klucz składa się z identyfikatora wątku, przestrzeni nazw, kanału i wersji, dzięki czemu punkt kontrolny może odwoływać się do wartości kanału według wersji zamiast przechowywać jej kopię:

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

stack: zarządzanie kontekstem

Atrybut stack jest czasami opisywany jako kolejka zaległych zadań, ale w implementacji zarządzacza kontekstem jest to stos zarządzacza kontekstem (ExitStack) używany do zarządzania zasobami podczas wchodzenia i wychodzenia z zarządzacza kontekstem. Nie przechowuje on stanu wykonywania grafu. Są to prywatne elementy wewnętrzne, więc sprawdź je pod kątem swojej zainstalowanej wersji.

Śledzenie wykonywania krok po kroku

Jedno wezwanie grafu tworzy trzy punkty kontrolne.

Punkt kontrolny 1: przychodzi dane wejściowe

Pierwszy punkt kontrolny, o identyfikatorze 1f1b054e-b2a5-660a-bfff-7484776ebce0, zawiera dwa obiekty msgpack: sam punkt kontrolny oraz jego metadane.

// 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": {}
}

W tym momencie istnieje tylko kanał __start__. Otrzymał on swoją pierwszą wersję, która jest wymieniona w liście updated_channels, a metadane oznaczają źródło jako input z ustawionym step na -1, co oznacza stan przed wykonaniem jakiegokolwiek kroku w grafie. Ciągi wersji podążają za prostym schematem: zapisany z zerami licznik rosnący monotonicznie, po którym następuje losowa frakcja zapewniająca unikalność wersji.

Punkt kontrolny odnosi się do wartości kanału poprzez jego wersję, a odpowiadający mu plik przechowuje dane. W tym przypadku danymi wejściowymi był pusty słownik, który msgpack koduje jako pojedynczy bajt \x80:

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

Punkt kontrolny 2: kierowanie do węzła

Drugi punkt kontrolny, 1f1b054e-b2a6-6294-8000-96e3a3cb81ac, rejestruje ścieżkę od START do get_address. Chodzi tu o routowanie, a nie jeszcze o uruchamianie węzła:

// 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": {}
}

Dwa kanały przekazują teraz wersję 2. __start__ przechodzi do nowej wersji, ponieważ jego dane wejściowe zostały już wykorzystane, a nowy kanał branch:to:get_address sygnalizuje, że następnie powinien zostać uruchomiony get_address. versions_seen pokazuje, że zadanie __start__ miało już do czynienia z wersją 1 kanału __start__; to właśnie dzięki takiemu rejestrowaniu LangGraph decyduje, które węzły nadal muszą zostać uruchomione. Metadane przechodzą na źródłowy loop z step 0.

Zapis, który spowodował tę zmianę, jest przechowywany pod ID poprzedniego punktu kontrolnego, ponieważ został wygenerowany przez zadanie uruchomione z tego punktu kontrolnego:

('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__')
 }

Tworzone są również dwa nowe bloki. Blok __start__ jest oznaczony jako empty, co odzwierciedla fakt, że kanał został wyczyszczony po jego użyciu, natomiast kanał gałęzi przechowuje wartość null, ponieważ pełni jedynie rolę wyzwalacza:

// 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'')

Punkt kontrolny 3: węzeł aktualizuje stan

Trzeci punkt kontrolny, 1f1b054e-b2a6-6d66-8001-d006da4d6d19, rejestruje wykonywanie funkcji get_address oraz jej aktualizacje wartości name i 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 zawsze zawiera najnowszą wersję każdego kanału, podczas gdy versions_seen rejestruje, jaką wersję widział każdy węzeł podczas swojego uruchomienia. __start__ pozostaje na wersji 2, ponieważ nic więcej go nie modyfikuje. Kanał gałęziowy oraz dwa kanały stanu przechodzą na wersję 3, updated_channels zawiera listę address i name, a licznik kroków osiąga wartość 1.

Węzeł zapisał dwie wartości, więc pod ID drugiego punktu kontrolnego znajdują się dwa zapisy – po jednym dla każdego kanału, które dzielą się tym samym ID zadania:

('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')
}

W końcu nowe bloki przechowują zapisane w formacie msgpack ciągi znaków dla dwóch pól stanu:

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

Dlaczego układ został zaprojektowany w ten sposób

Trzy słowniki dla grafu składającego się z jednego węzła mogą wydawać się przesadą, ale każdy z nich ma swoje uzasadnienie:

  • Punkty kontrolne powiązane z rodzicem zapewniają każdemu wątkowi pełną historię. Można przejrzeć dowolny poprzedni stan, wznowić pracę od niego lub utworzyć nowy gałąź.
  • Bloby wersjonowane przechowują każdą wartość kanału raz na zmianę, dzięki czemu punkty kontrolne pozostają małe, nawet gdy stan jest duży i w większości niezmieniony.
  • Zapisy w oczekiwaniu umożliwiają wznowienie kroków. Jeśli jedno z zadań w danym kroku zawiedzie, zapisy udanych zadań są już zachowane i nie muszą być wykonywane ponownie.

Niezmiennicze narzędzia do tworzenia punktów kontrolnych, takie jak ten w Postgresie, przechowują punkty kontrolne, bloby i zapisy w oddzielnych tabelach, które odzwierciedlają te struktury, dzięki czemu ten sam model myślowy stosuje się do bazy danych.

Główne wnioski

  • InMemorySaver jest przeznaczony do rozwoju i testów; jego dane znikają po zakończeniu procesu.
  • storage przechowuje punkty kontrolne oraz metadane dla każdego wątku i przestrzeni nazw, połączone identyfikatorami rodzicielskimi.
  • writes przechowuje aktualizacje dla poszczególnych zadań, oznaczone identyfikatorem punktu kontrolnego, z którego pochodzą.
  • blobs przechowuje wartości kanałów według wersji, dzięki czemu niezmienione kanały nigdy nie są kopiowane.
  • Literatura pokrewna