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.
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.
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.
| Corto plazo | Largo plazo | |
|---|---|---|
| Qué guarda | El estado del grafo | Hechos sobre el usuario |
| Clave | thread_id | user_id |
| Vida | Una conversación | Todas las conversaciones |
| Quién escribe | LangGraph, automáticamente | Un nodo tuyo, deliberadamente |
| Crece | Sin límite, con el historial | Solo con lo que decidas guardar |
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ú.
| Estrategia | Qué hace | Coste |
|---|---|---|
| Ventana deslizante | Se queda con los últimos N | Ninguno, pero lo que sale, sale |
| Resumen | Un nodo condensa lo viejo y lo sustituye | Una llamada extra por compactación |
| Recuperación | Guardar 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.
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.
Sobre un grafo mínimo — un solo nodo que incremente un contador en el estado
— compila con MemorySaver y ejecuta:
invoke con thread_id="a". ¿El contador va por 2?invoke con thread_id="b". ¿Por cuánto va?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?