MÓDULO 5 · LANGGRAPH · LECCIÓN 06 · 40 MIN

Memoria: hilo corto, almacén largo

Un grafo sin checkpointer empieza de cero en cada invoke. El checkpointer guarda el estado de un hilo entre invocaciones: eso es la memoria a corto plazo. Todo lo que deba sobrevivir al hilo — el nombre del usuario, sus preferencias — necesita un almacén aparte, con su propia clave.
01 — El punto de partida
Sin checkpointer no hay memoria
app.invoke({"messages": [HumanMessage("Me llamo Nico")]})
app.invoke({"messages": [HumanMessage("¿Cómo me llamo?")]})

La segunda llamada no sabe nada de la primera. El reducer add_messages acumula dentro de una invocación, entre nodos; no entre invocaciones. Que el historial crezca en un invoke no significa que se guarde al terminar.

// de ahí el patrón de guardar el resultado de invoke en una variable y volver a pasarlo. Funciona, y es justo el trabajo que el checkpointer viene a hacer bien.
02 — Dos líneas y ningún nodo tocado
Checkpointer y thread_id
from langgraph.checkpoint.memory import MemorySaver

app = builder.compile(checkpointer=MemorySaver())

config = {"configurable": {"thread_id": "nico-sesion-1"}}
app.invoke({"messages": [HumanMessage("Me llamo Nico")]}, config)
app.invoke({"messages": [HumanMessage("¿Cómo me llamo?")]}, config)

El checkpointer va en compile(); el thread_id, en la configuración de cada invoke. El thread_id es la clave de la conversación: mismo id, mismo estado recuperado; id distinto, hilo en blanco.

Y como el estado queda guardado paso a paso, puedes leerlo sin ejecutar nada:

app.get_state(config)          # el estado actual del hilo
app.get_state_history(config)  # cada checkpoint, del más reciente al primero

Esa historia es lo que hace posible depurar un ciclo: ves en qué vuelta el estado se torció, no solo el resultado final.

03 — La distinción que decide el diseño
Corto plazo no es largo plazo
Corto plazoLargo plazo
Qué guardaEl estado del grafoHechos sobre el usuario
Clavethread_iduser_id
VidaUna conversaciónTodas las conversaciones
Quién escribeLangGraph, automáticamenteUn nodo tuyo, deliberadamente
CreceSin límite, con el historialSolo con lo que decidas guardar
thread_id = sesion-1
historial de esta charla
lo restaura el checkpointer
user_id = nico
nombre, preferencias, notas
lo escribe un nodo tuyo

La confusión habitual es creer que un checkpointer persistente ya da memoria a largo plazo. No: da el mismo hilo más tarde. Si el usuario abre una conversación nueva, el thread_id cambia y el checkpointer no tiene nada que devolver. Recordar entre hilos es una decisión de producto — qué se extrae, qué se guarda, qué se olvida — y por eso vive en nodos que escribes tú.

04 — El límite del corto plazo
El historial no puede crecer para siempre
EstrategiaQué haceCoste
Ventana deslizanteSe queda con los últimos NNinguno, pero lo que sale, sale
ResumenUn nodo condensa lo viejo y lo sustituyeUna llamada extra por compactación
RecuperaciónGuardar fuera y traer solo lo relevanteÍndice, embeddings, latencia

Y una trampa: si recortas el historial de una conversación con herramientas, cuida de no dejar un ToolMessage sin el AIMessage con tool_calls que lo originó. Muchos proveedores rechazan esa secuencia.

Auditoría

MemorySaver se llama así porque guarda en memoria del proceso. Reinicias el script y no queda nada. Antes de decir que tu chatbot «persiste», pregunta dónde: ¿en RAM, en un diccionario global del módulo, en SQLite?

El enunciado del ejercicio de memoria dual tiene la misma ambigüedad en otro sitio: pide que tras 15 mensajes el historial conserve «los últimos 10», pero cada turno añade el mensaje del usuario y la respuesta del asistente. ¿10 turnos o 10 entradas? Decide cuál implementas y déjalo escrito: el criterio de evaluación no lo aclara.

05 — Ahora tú
Un contador y dos hilos

Sobre un grafo mínimo — un solo nodo que incremente un contador en el estado — compila con MemorySaver y ejecuta:

  1. Dos invoke con thread_id="a". ¿El contador va por 2?
  2. Un tercer invoke con thread_id="b". ¿Por cuánto va?
  3. print(app.get_state({"configurable": {"thread_id": "a"}})). Localiza en esa salida dónde está el valor y dónde el nodo que toca a continuación.

Anota qué pasa si compilas sin checkpointer y pasas igualmente el thread_id: ¿falla, o lo ignora en silencio?