Dentro de InMemorySaver de LangGraph: cómo encajan los puntos de control, las escrituras y los Blobs
Recorra el almacenamiento, escriba los diccionarios de escrituras y blobs dentro de InMemorySaver de LangGraph y trace cómo una única ejecución de un gráfico pequeño se convierte en tres puntos de control vinculados.
El InMemorySaver de LangGraph suele ser un detalle de configuración de una sola línea: se pasa a compile(), las conversaciones recuerdan de repente su estado y nadie investiga más allá. Sin embargo, la forma en que organiza los datos explica mucho sobre LangGraph mismo, incluyendo cómo funcionan la reanudación, los viajes en el tiempo y la tolerancia a fallos, así como por qué los mecanismos de punto de control persistente tienen esa apariencia. Al seguir el flujo de un grafo mínimo a través de los diccionarios internos del salvador, podrás leer un archivo de punto de control y saber exactamente qué significa cada entrada.
Por qué los grafos necesitan puntos de control
Un checkpointer funciona como memoria a corto plazo para un grafo: captura una instantánea del estado del grafo a medida que avanza la ejecución. Piense en los puntos de guardado de un juego en modo historia: sin ellos, para reproducir el nivel dos sería necesario volver a jugar primero el nivel uno. Un punto de guardado registra los avances del jugador para que se pueda continuar desde ese momento, incluso después de terminar el juego. LangGraph hace lo mismo después de cada paso, de modo que un hilo puede continuar o reproducirse desde un punto anterior.
Un grafo mínimo para inspeccionar
El ejemplo a continuación crea el grafo más pequeño útil: un estado con tipo que incluye name y address, un único nodo determinista que establece ambos campos mediante una Command, y aristas START, luego get_address, y finalmente END. Compila el grafo con un InMemorySaver y un InMemoryStore, lo ejecuta en el hilo "12345", y por último muestra los atributos del checkpointer. El almacén es un componente separado para datos a largo plazo compartidos entre hilos y no desempeña ningún papel en lo que sigue. Aunque el fragmento está etiquetado como JavaScript, en realidad es 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__
Los atributos de InMemorySaver
Al listar las claves del diccionario del checkpointer se muestran cinco atributos:
app.checkpointer.__dict__.keys()
# dict_keys(['serde', 'storage', 'writes', 'blobs', 'stack'])
serde: serialización y deserialización
Los datos de los puntos de control no pueden almacenarse como objetos Python en tiempo real en una base de datos, e incluso en memoria el salvador los mantiene en un formato serializado. serde es el serializador que convierte los valores a bytes y viceversa, etiquetando cada uno con un tipo como msgpack.
almacenamiento: puntos de control por hilo
storage almacena los puntos de control en sí. Cada conversación recibe un ID de hilo, y ese ID es el medio mediante el cual LangGraph obtiene el historial de un hilo específico. La estructura es un diccionario anidado: ID del hilo, luego el espacio de nombres del punto de control (una cadena vacía para el grafo de nivel superior; los subgrafos tienen sus propios espacios de nombres), y finalmente el ID del punto de control:
{
"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),
}
}
}
Cada entrada contiene el punto de control serializado, sus metadatos serializados y el ID del punto de control padre. Ese puntero padre convierte los puntos de control de un hilo en una historia enlazada, lo que permite rebobinar y crear ramas.
escrituras: escrituras pendientes por punto de control
writes registra las actualizaciones individuales que generan las tareas. En lugar de sobrescribir el estado directamente, cada actualización se registra como una nueva entrada identificada por el hilo, el espacio de nombres y el punto de control desde el cual ejecutó la tarea. Dentro de ella, cada escritura se identifica mediante un ID de tarea e un índice:
{
('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>'))
}
}
El channel_name en este esquema es un marcador de posición. Cuando un nodo actualiza name, el canal es name; cuando actualiza address, el canal es address. Un nodo que actualiza ambos al mismo tiempo genera dos entradas bajo el mismo punto de control. Dado que las escrituras se almacenan tan pronto como termina una tarea, una ejecución que falla a mitad de un paso no necesita volver a ejecutar las tareas que ya tuvieron éxito.
blobs: valores de canal versionados
blobs almacena el valor real de cada canal en cada versión. La clave combina hilo, espacio de nombres, canal y versión, por lo que un punto de control puede hacer referencia al valor de un canal por versión en lugar de incluir una copia:
{
('thread_id', 'namespace', 'channel_name', 'version') : ('mssgpack', '<binary data>')
}
stack: gestión de contexto
El atributo stack a veces se describe como una cola de tareas pendientes, pero en la implementación del salvador es una pila de gestores de contexto (un ExitStack) que se utiliza para administrar recursos al entrar y salir del salvador como gestor de contexto. No almacena el estado de ejecución del grafo. Se trata de componentes internos privados, por lo que verifíquelos según su versión instalada.
Rastreando la ejecución paso a paso
Cada llamada al grafo genera tres puntos de control.
Punto de control 1: llega la entrada
El primer punto de control, con ID 1f1b054e-b2a5-660a-bfff-7484776ebce0, contiene dos cargas útiles de msgpack: el punto de control y sus metadatos.
// 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": {}
}
En este momento solo existe el canal __start__. Ha recibido su primera versión, la cual se enumera en updated_channels, y los metadatos marcan la fuente como input con step establecido en -1, lo que indica que se trata del estado anterior a que se ejecutara cualquier paso del grafo. Las cadenas de versión siguen un esquema sencillo: un contador con relleno de ceros que aumenta de forma monótona, seguido por una fracción aleatoria que garantiza la unicidad de las versiones.
El punto de control hace referencia al valor del canal a través de su versión, y el blob correspondiente contiene los datos. En este caso, la entrada era un diccionario vacío, que msgpack codifica como el único byte \x80:
// this msgpack basically {}
('12345', '', '__start__', '00000000000000000000000000000001.0.267464090313665'): ('msgpack', b'\x80')
Punto de control 2: enrutamiento hacia el nodo
El segundo punto de control, 1f1b054e-b2a6-6294-8000-96e3a3cb81ac, registra la conexión desde START hasta get_address. Se trata de enrutamiento, no aún de ejecutar el nodo:
// 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": {}
}
Actualmente dos canales transmiten la versión 2. __start__ pasa a una nueva versión porque su entrada ya se ha consumido, y un nuevo canal, branch:to:get_address, indica que get_address debe ejecutarse a continuación. versions_seen muestra que la tarea __start__ ha visto la versión 1 del canal __start__; este registro es el método mediante el cual LangGraph decide qué nodos aún necesitan ejecutarse. Los metadatos cambian al origen loop con step 0.
La escritura que desencadenó esta transición se almacena bajo el ID del punto de control anterior, ya que fue generada por la tarea que se ejecutó a partir de ese punto de control:
('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__')
}
También se crean dos nuevos blobs. El blob __start__ está etiquetado como empty, lo que refleja que el canal fue vaciado después de ser consumido, y el canal de ramas almacena un valor null porque solo funciona como desencadenante:
// 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'')
Punto de control 3: el nodo actualiza su estado
El tercer punto de control, 1f1b054e-b2a6-6d66-8001-d006da4d6d19, captura la ejecución de get_address y sus actualizaciones en name y 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 siempre contiene la versión más reciente de cada canal, mientras que versions_seen registra lo que vio cada nodo al ejecutarse. __start__ permanece en la versión 2 porque nada más lo modifica. El canal de ramificación y los dos canales de estado pasan a la versión 3, updated_channels enumera address y name, y el contador de pasos alcanza el 1.
El nodo escribió dos valores, por lo que aparecen dos escrituras bajo el ID del segundo punto de control, una por canal, compartiendo el mismo ID de tarea:
('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')
}
Finalmente, los nuevos blobs albergan las cadenas codificadas en msgpack para los dos campos de estado:
('12345', '', 'name', '00000000000000000000000000000003.0.07103778333502464'): ('msgpack', b'\xacpavaneeshwar'),
('12345', '', 'address', '00000000000000000000000000000003.0.07103778333502464'): ('msgpack', b'\xb3Hyderabad residency')
Por qué el diseño es así
Tres diccionarios para un grafo de un solo nodo parecen excesivos, pero cada elemento tiene su razón de ser:
- Puntos de control vinculados a los padres proporcionan a cada hilo un historial completo. Puedes inspeccionar cualquier estado anterior, reanudar desde él o crear una nueva rama a partir de él.
- Blobs versionados almacenan cada valor del canal una sola vez por cambio, de modo que los puntos de control permanecen pequeños incluso cuando el estado es grande y en su mayor parte no ha cambiado.
- Escribimientos pendientes permiten reanudar las acciones. Si una tarea dentro de un paso falla, los escritos de las tareas que tuvieron éxito ya están guardados y no es necesario ejecutarlos nuevamente.
Los generadores de puntos de control persistentes, como el de Postgres, conservan los puntos de control, los blobs y los escritos en tablas separadas que reflejan estos diccionarios, de modo que el mismo modelo mental se aplica a tu base de datos.
Puntos clave
InMemorySaverestá diseñado para el desarrollo y las pruebas; sus datos desaparecen cuando finaliza el proceso.
storage almacena puntos de control y metadatos por hilo y espacio de nombres, vinculados mediante IDs de padre.writes almacena actualizaciones por tarea, identificadas por el punto de control del cual provienen.blobs almacena valores de canal por versión, de modo que los canales sin cambios nunca se copian.Lecturas relacionadas
- Construyendo un agente de investigación ReAct en LangGraph: Cerebro, Manos, Router — Aprenda cómo implementar el bucle de razonamiento-acción-observación ReAct como un sub-gráfico de LangGraph, con reflexión forzada, presupuestos de iteración e investigación paralela de tipo scatter-gather.
- Enrutamiento, Fan-Out, ReAct, Crítica y Aprobación: Cinco Patrones de LangGraph — Conozca cinco patrones de flujos de trabajo basados en agentes en LangGraph, desde routers y bucles ReAct hasta puertas de evaluación y aprobación humana, incluyendo las medidas de seguridad que cada uno necesita en entornos de producción.
- Agentes con Aprobación en LangGraph: interrupt(), Puntos de Interrupción y un Almacén — Construya un agente de LangGraph paso a paso: un grafo ReAct explícito, aprobación humana mediante interrupt(), y memoria entre hilos mediante un almacén, terminando con un asistente de bandeja de entrada que pregunta primero.
- De texto bruto a tuberías: analizadores, LCEL, ejecutables y memoria en LangChain — Cómo LangChain convierte el texto bruto del modelo en datos estructurados, compone pasos con LCEL y la interfaz Runnable, y gestiona la memoria en las conversaciones actualmente.