Галоўная / Артыкулы / Усередзінне розгляду InMemorySaver ад LangGraph: як падчыты, запісі та блобы адпрацоўваюцься

Усередзінне розгляду InMemorySaver ад LangGraph: як падчыты, запісі та блобы адпрацоўваюцься

Пройдзіце па сховышчы, запісваюце словнікі “writes” і “blobs” у InMemorySaver LangGraph і студзіруйце, як адна маленька графічная структура ператвараецца на тры скасаваныя станы, з’ѐеднаныя між сабой.

1834 слоў

У LangGraph клас InMemorySaver зазвычай ўжоўцяецца як простая настройка: вы перадаеце його функцыі compile(), і розмовы раптам памятаюць свой стан, а ніхто больш не дагледвае. Аднак спосаб, якім ён архівуе даныя, раскрывае багато інфармацыі пра сам LangGraph, укладаючы ў сябе інформацыю пра тое, як функціонуюць воззврат да пярэдніх станоў, „паляцы ў часе“ і толерантнасьць да бядаў, а таксама прычыны, чаму інструменты для стварэння контрольных пунктав выглядаюць так, як выглядаюць. Аналізуючы працэс адработкі мінімальнага графа через внутрашнія словнікі сэўвара, вы зможыце прачытаць копію контрольнага пунктава і точна зразумець, што значыць кожны елемент.

Чаму графым потрэбны контрольные пунктавы

Чэкпойнтар выступае як кашточная памяць для графа: ён фіксуе стан графа на певны момент часу падчас выканання. Падумайце пра точкі зберэння ў гэме у режыме адгулявання: без іх, калі хочаце празыграць другі рэвель, трэба спачатку празыграць першы. Точка зберэння фіксуе працэз дзеяння гэмера, таму можна прыступіць да гры з таго моменту, нават пасля завершэння гры. LangGraph рабіць тое ж пасля кожнага крока, тады поток можа прыступіць да дзеяння або празыграць гру з болей раннега моменту.

Мінімальны граф для адзіроўкі

У прыкладзе нижэй прадстаўляецца самы маленькі корыстны граф: стан з парамі значэнняў name і address, адны дэтэрміністычны вузел, які задае гэтыя значэння за дапамою Command, а таксама рэшткі START, get_address і END. Граф скомпілюецца за дапамою InMemorySaver і InMemoryStore, будзе запусканы на ніцы "12345", а пасля будуць вывучаны атрыбуты чэкпойнтара. InMemoryStore — это аднаэлементны компанент для дадзей, якія трэба выкарыстоўваць у разных ніцах, і ён не плейвае жадной ролі ў далейшым адбыванні задач. Хоць этот фрагмент пазначаецца як JavaScript, на самай працэ ён напісаны на 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__

Атрыбуты InMemorySaver

Спісаванне ключоў слоўніка чэкпойнтара паказвае пяць атрыбутаў:

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

serde: серыяванне і дэсерыяванне

Данные пауз у перагляду не можна зберагаць як жывыя об’екты Python у базе дадзеных, а нават у памяці прыстрой для зберагання заставае іх у серыяванам выглядзе. serde — это прыстрой для серыявання, який перакладае значэнні у байты і назад, прадаючы кожнае з іх атрыбутам типу, напрыклад msgpack.

Збераганне: паузы у перагляду па ниткам

storage зберагае самыя паузы у перагляду. Кожны дыялог атрыбутуецца ідэнтыфікатаром ниткі, і самэй гэтым ідэнтыфікатарам LangGraph адзначае історыю конкрэтной ниткі. Структура ў виглядзе вялікога слоўніка: ідэнтыфікатар ниткі, потым прастор імен пауз у перагляду (пустая строчка для верхніяго рэвю; падрэвю атрыбутуецца своімі прасторамі імен), а потым ідэнтыфікатар паузы у перагляду:

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

Кожны элемент храніць серыяваны стан контрольнай точкі, яе серыяваны метаданныя і ID бацальнай контрольнай точкі. Гэты паказчык бацальнай контрольнай точкі ператварае станы контрольных точак праменя ў супяжаную історыю, што дазволяе вярнуцца назад і ствараць новыя прамені.

Запісы: запісы, якія чакаюць адбыцца па кожнай контрольнай точцы

writes фіксуе адзінаковыя змены, якія ствараюць задачы. У замест на перазапісванне стану на месцы, кожны змена фіксуецца як новы элемент, пазначаны праменем, прасторам імен і контрольнай точкай, з якой запускалася задача. Унутры кожны запіс ідентыфікуецца за дапамою ID задачы і індэкса:

{
    ('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 ўжоўся як месца для падставы. Калі вузел апдэйтуе name, канал стае name; калі ён апдэйтуе address, канал стае address. Вузел, які апдэйтуе і тое, і другое адразу, стварае два элементы пад тым самым пунктом перапалоўкі. Паколькі даныя запішываюцца як толькі завершаецца задача, запуск, який збіваецца на паўпараграфе, не трэба перывыкання задач, якія вже былі завершаны.

blobs: версіяваныя значэння каналаў

blobs храніць рэальнае значэнне кожнага канала ў кожной версіі. Ключ складаецца з назвы вырабні, прастору імен, канала і версіі, таму пункт перапалоўкі можа абыцца значэнням канала па версіі, замест таго, каб хаваць копію:

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

stack: калектывнае кераванне

Атрыбут stack іноды апісваецца як черга завяршэння задач, але ў рэалізацыі сэўвара яны ёсць стак кантэкст-менеджара ( ExitStack ), які викорыстоўваецца для керування рэсурсамі, калі сэўвар запускаецца і завершаецца як кантэкст-менеджар. Ён не зберагае стан выконання графа. Цэлыя гэтыя элементы є прыватнымі внутрашняйма часткамі, таму пераканайцеся, што яны супараднуюцца з вашай установленай версіяю.

Паследовы аналіз крокаў выконання

Адна вызова графа стварае тры пункты контролю.

Пункт контролю 1: прыходзіць вхідны даны

Першы пункт контролю, з ID 1f1b054e-b2a5-660a-bfff-7484776ebce0, містыць два пакеты msgpack: сам пункт контролю і яго метаданы.

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

У гэты момент існуе толькі канал __start__. Ён уже запрацаваў у сваёй першай версіи, яка знаходзится ў списку updated_channels, а метаданы пазначаюць джэрела як input з значеннем step -1, што означае, што гэта стан да запуску будзь-якага крока графа. Строкі версій паўтараюць простую схему: кантэйнер, заполнены нулямі і монатонна зростаючы, за яким следуе вялікая вэртка, якая робіць версіі унікальнымі.

Чэкпоінт аднасаецца да значэння каналу праз яго версію, а адпаведны блоб зберагае даны. У гэтым прыкладзе вхідным дадзенням быў порожній слоўнік, які msgpack кодуе як адзіны байт \x80:

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

Чэкпоінт 2: направленне да вузла

Другі пункт контроля, 1f1b054e-b2a6-6294-8000-96e3a3cb81ac, фіксуе лінію з START на get_address. Це стосуецца маршрутавання, а не запуску вузла:

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

Два каналы зараз перадаюць версію 2. __start__ пераходзіць да новай версіі, таму што яго вхідны даны былі выкарыстаны, а новы канал branch:to:get_address сигналізуе, што наступна мае быць експансія get_address. versions_seen паказвае, што задача __start__ вже бачыла версію 1 канала __start__; гэтыя записі ўтвараюць падставу для таго, каб LangGraph выбіраў, якія вузлы ўсё ўтрэба запусціць. Метаданы пераходзяць да выхіднага loop з step 0.

Запіс, які спрычыніў гэты пераход, зберагаецца пад ID паказвальнага пункта, таму што ён быў створаны задачай, якая запускалася з таго пункта:

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

Таксама ствараюцься два новыя блабы. Блаб __start__ пазначаецца як empty, што адражае тое, што канал быў спачаткуўаны пасля выкарыстоўвання, а канал гілкі зберагае значэнне null, таму што ён служыць толькі як трыгар.

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

Тэплік 3: вузол адначыць стан

Трэці тэплік, 1f1b054e-b2a6-6d66-8001-d006da4d6d19, фіксуе выкананне get_address і яго змены для name і 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 завжоды мае найновейшую версію кожнага каналу, тады як versions_seen фіксуе, калькі версію бачыў кожны вузел пад час свайго запуску. __start__ застаецца на версіі 2, таму што да яго больш ніхто не прыходзіць. Канал разгалужэння і два каналы стану пераходзяць на версію 3, updated_channels супакоўвае address і name, а лічылка крока досягае 1.

Вузел запісаў два значэння, таму пад ID другага контрольнага пункту прыказаны два запісы, по аднаму на канал, якія дзелююцься тым самым ID задання:

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

У канцы новыя блобы маюць строкі, закодаваные у формате msgpack, для двух полей стану:

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

Чаму лейаут спроектаваны так

Тры словнікі для графа з адным вузлам можаць здавацца занадто, але кожны элемент мае свое месца:

  • Пункты контроля, прыязджаныя да родніх элементаў даюць кожнаму потоку цэлую історыю. Вы можете пераглядзець будзь-які паканальны стан, запускаць работу з яго або ствараць новы галузь на його аднойчыні.
  • Версіяваныя блобы зберагаюць кожную значэння каналу аднойчыні праз кожны змянення, таму пункты контроля застаюцца малымі, нават калі стан великі і па большай частцы не змienяецца.
  • Чакаючыя запісы дазволяюць практыкаваць запуск крокаў знову. Якщо адна з задач у кроку не выйшла, запісы успешных задач вялікі ўжо зберагнуты і не патрабуецца ўжо выканаць іх.

Пастаянныя прыстроі стварэння пунктав контроля, такія як той у Postgres, зберагаюць пункты контроля, блобы і запісы ў адзельных табелях, якіе адпаведаюць гэтым словнікам, таму той самы ментальны модэль прыменяецца да вашай базы дадзенаў.

Ключовыя выводы

  • InMemorySaver прызначаны для разработкі і тэстаў; яго даны знікаюць, калі процес завершаецца.
  • storage зберагае пункты перапрыяцоўкі і метаданы па кожнай нітку та прастору імен, выкананыя за дапамою ID-аў абмовак.
  • writes зберагае апошнія змены па кожным заданню, прычалёваныя да пункту перапрыяцоўкі, з якога яны былі створаны.
  • blobs зберагае значэнні каналаў па версіях, таму каналы, якія не зменіліся, ніколі не копіююцца.
  • Супаўзвязаныя матэрыялы