Внутри InMemorySaver от LangGraph: как работают чекпоинты, записи и блобы
Пройдитесь по хранилищу, запишите словари записей и блобов во внутренние структуры LangGraph’s InMemorySaver и проследите, как один небольшой граф в процессе выполнения превращается в три связанных чекпоинта.
InMemorySaver от LangGraph обычно представляет собой простую настройку в одну строку: вы передаёте его в функцию compile(), и диалоги сразу начинают запоминать своё состояние, после чего никто не задумывается об этом дальше. Однако способ, которым он организует хранение данных, многое объясняет о самом LangGraph — включая принципы возобновления работы, «путешествий во времени» и отказоустойчивости, а также причины того, почему механизмы сохранения состояния имеют именно такой вид. Проанализировав минимальную схему, прошедшую через внутренние словари этого средства сохранения, вы сможете прочитать данные из файла чекпоинта и точно понять, что означает каждая запись.
Почему графам нужны чекпоинты
Чекпоинт выполняет функцию краткосрочной памяти для графа: он сохраняет кадр состояния графа по мере его обработки. Представьте себе точки сохранения в игре в режиме истории: без них, чтобы снова пройти второй уровень, необходимо сначала пройти первый. Точка сохранения фиксирует прогресс игрока, что позволяет возобновить игру с этого момента даже после её завершения. LangGraph делает то же самое после каждого шага, так что поток может возобновиться или быть перезапущен с более ранней точки.
Минимальный граф для анализа
В приведённом ниже примере создаётся самая маленькая полезная графика: состояние с полями name и address, единственный детерминистичный узел, который задаёт оба поля с помощью Command, а также рёбра START, затем get_address и в конце END. Графика компилируется с использованием InMemorySaver и InMemoryStore, запускается на потоке "12345", после чего выводятся атрибуты чекпоинтера. Хранилище представляет собой отдельный компонент для долгосрочного хранения данных, общих между потоками, и не участвует в дальнейших операциях. Хотя этот фрагмент помечен как 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 хранит сами контрольные точки. Каждому диалогу присваивается идентификатор потока, и именно по этому ID 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: поступление входных данных
Первая точка контроля с идентификатором 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 определяет, какие узлы еще нуждаются в выполнении. Метаданные переключаются на исходный цикл с шагом 0.
Запись, вызвавшая этот переход, хранится под идентификатором предыдущего чекпоинта, поскольку она была сгенерирована задачей, запущенной из этого чекпоинта:
('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 задачи:
('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')
Почему структура спроектирована именно так
Три словаря для графа с одним узлом кажутся избыточными, но каждый элемент имеет своё обоснование:
- Чекпоинты, связанные с родительским объектом обеспечивают каждой нити полную историю изменений. Вы можете просмотреть любое прошлое состояние, возобновить работу с ним или создать новую ветку от него.
- Версионированные блобы хранят каждое значение канала один раз при каждой изменении, поэтому чекпоинты остаются небольшими даже тогда, когда состояние велико и практически не меняется.
- Временные записи позволяют возобновлять выполнение шагов. Если одна из задач в шаге завершается с ошибкой, записи успешно выполненных задач уже сохранены и не требуют повторной обработки.
Постоянные инструменты создания чекпоинтов, такие как тот, что используется в Postgres, хранят чекпоинты, блобы и записи в отдельных таблицах, которые соответствуют этим структурам данных, так что та же логика применяется и к вашей базе данных.
Основные выводы
InMemorySaverпредназначен для разработки и тестирования; его данные исчезают при завершении процесса.
storage хранит точки контроля и метаданные для каждой нити и пространства имён, связанные между собой через идентификаторы родителей.writes хранит обновления для каждой задачи, отсортированные по точке контроля, из которой они были сгенерированы.blobs хранит значения каналов по версиям, благодаря чему неизменные каналы никогда не копируются.Связанные материалы
- Создание исследовательского агента ReAct в LangGraph: Brain, Hands, Router — Узнайте, как реализовать цикл размышления-действия-наблюдения ReAct в виде подграфа LangGraph с принудительным отражением, лимитами итераций и параллельными методами сбора данных.
- Маршрутизация, разветвление потоков, ReAct, критика и утверждение: пять шаблонов LangGraph — Ознакомьтесь с пятью шаблонами агентных рабочих процессов в LangGraph, начиная с маршрутизаторов и циклов ReAct и заканчивая механизмами оценки и утверждения человеком, а также правилами, необходимыми для их использования в производственных условиях.
- Агенты с механизмом утверждения в LangGraph: функция interrupt(), точки контроля и хранилище — Пошаговая разработка агента в LangGraph: создание явного графа ReAct, процедура утверждения человеком с использованием функции interrupt(), обмен данными между потоками через хранилище, в результате чего формируется помощник-приемник сообщений, который сначала задает вопросы.
- От сырого текста к пайплайнам: парсеры, LCEL, Runnables и работа с памятью в LangChain — Как LangChain преобразует сырой текст модели в структурированные данные, объединяет шаги с помощью LCEL и интерфейса Runnable, а также управляет памятью во время диалога сегодня.