Módulo 8: Memoria y Persistencia
Checkpointing con MemorySaver
Descripción de la cápsula
Tu Research Agent procesa 5 fuentes de investigación. Descompone la consulta, busca en la web, analiza papers, extrae datos de noticias, y sintetiza un reporte. El proceso toma 5 minutos y consume tokens de API en cada paso. Después de procesar 3 fuentes — crash. El proceso se cae. Sin checkpointing: repites las 5 fuentes desde cero. 5 minutos más. Doble costo de API. El usuario espera de nuevo.
Con checkpointing: el agente resume desde la fuente 4. 1 minuto. Sin repetir trabajo. Sin costo duplicado.
Checkpointing guarda el estado completo de tu grafo después de cada nodo. Es un snapshot automático: qué nodos se ejecutaron, qué estado tenían, qué mensajes se intercambiaron. Si el proceso se interrumpe, no empiezas de cero — retomas desde el último checkpoint.
En la cápsula anterior viste short-term memory: cómo el grafo mantiene el historial de mensajes durante una conversación. Eso resuelve la continuidad dentro de una sesión. Checkpointing resuelve algo diferente: la durabilidad de esa sesión. Si el proceso muere, la memoria muere con él — a menos que tengas checkpointing.
MemorySaver es el checkpointer más simple de LangGraph: guarda los checkpoints en memoria RAM. Es perfecto para desarrollo y testing. No necesitas base de datos, no necesitas configuración. Una línea de código y tu grafo ya tiene checkpointing. En la siguiente cápsula migrarás a PostgresSaver para producción — y vas a ver que el cambio es literalmente una línea.
El problema: estado que se pierde
Sin checkpointing, cada invocación de tu grafo es independiente. El grafo no sabe que ya corrió antes. No tiene contexto previo. Cada graph.invoke() empieza con un estado vacío.
from langchain.chat_models import init_chat_model
from langgraph.graph import StateGraph, MessagesState, START, END
def chatbot(state: MessagesState) -> dict:
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke(state["messages"])
return {"messages": [response]}
graph_builder = StateGraph(MessagesState)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
graph = graph_builder.compile()
result1 = graph.invoke({"messages": [("user", "¿Qué es RAG?")]})
print(result1["messages"][-1].content)
# Output: "RAG (Retrieval-Augmented Generation) es una técnica que..."
result2 = graph.invoke({"messages": [("user", "Dame ejemplos")]})
print(result2["messages"][-1].content)
# Output: "¿Ejemplos de qué? No tengo contexto previo..."
El segundo invoke no sabe nada del primero. "Dame ejemplos" no tiene referente — el agente no sabe que hablaste de RAG hace 2 segundos. Cada invocación es una conversación nueva desde cero.
MemorySaver: checkpointing en una línea
MemorySaver guarda el estado de tu grafo en memoria después de cada nodo. Lo activas en dos pasos: crear el checkpointer y pasarlo al compilar.
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END
def chatbot(state: MessagesState) -> dict:
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke(state["messages"])
return {"messages": [response]}
graph_builder = StateGraph(MessagesState)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
checkpointer = MemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user_001"}}
result1 = graph.invoke({"messages": [("user", "¿Qué es RAG?")]}, config)
print(result1["messages"][-1].content)
# Output: "RAG (Retrieval-Augmented Generation) es una técnica que..."
result2 = graph.invoke({"messages": [("user", "Dame ejemplos")]}, config)
print(result2["messages"][-1].content)
# Output: "Aquí tienes ejemplos de RAG: 1) Un chatbot que consulta
# documentación interna... 2) Un asistente legal que busca
# jurisprudencia relevante..."
Tres cambios respecto al código sin checkpointing:
checkpointer = MemorySaver()— creas el checkpointergraph_builder.compile(checkpointer=checkpointer)— lo pasas al compilarconfig = {"configurable": {"thread_id": "user_001"}}— identificas el thread
Ahora el segundo invoke sabe que "ejemplos" se refiere a RAG. El checkpointer guardó el estado completo después del primer turno (mensajes del usuario + respuesta del modelo), y el segundo turno lo recibió automáticamente.
Cómo funciona internamente
Cuando compilas con checkpointer, LangGraph intercepta la ejecución de cada nodo y guarda un snapshot del estado:
Ejecución con checkpointing:
[START] → Estado inicial guardado como Checkpoint 0
↓
[chatbot] ejecuta → Estado actualizado guardado como Checkpoint 1
↓
[END] → Estado final guardado como Checkpoint 2
Cada checkpoint contiene:
- El estado completo del grafo en ese punto
- El nombre del nodo que produjo ese estado
- Un timestamp
- El ID del checkpoint padre (para navegar la historia)
Lo importante: el checkpoint guarda el estado completo, no un diff. Checkpoint 2 tiene todos los mensajes, no solo el último. Esto significa que puedes retomar desde cualquier checkpoint sin necesidad de los anteriores.
Flujo de dos turnos
Turno 1: "¿Qué es RAG?"
1. Checkpointer busca checkpoint para thread "user_001" → no hay
2. Nodo "chatbot" ejecuta → respuesta del modelo
3. Checkpointer guarda: [user_msg, ai_msg]
Turno 2: "Dame ejemplos"
1. Checkpointer busca checkpoint → ENCUENTRA [user_msg_1, ai_msg_1]
2. MERGE: estado previo + nuevo input = [user_msg_1, ai_msg_1, user_msg_2]
3. Nodo "chatbot" ejecuta con contexto completo → respuesta coherente
4. Checkpointer guarda: [user_msg_1, ai_msg_1, user_msg_2, ai_msg_2]
El paso 1-2 del turno 2 es la magia: el checkpointer recupera el estado previo y lo combina con el nuevo input. El nodo chatbot recibe toda la conversación, no solo el último mensaje.
thread_id: aislamiento entre conversaciones
El thread_id es obligatorio cuando usas checkpointer. Es el identificador que separa las conversaciones. Sin él, LangGraph no sabe dónde guardar ni de dónde recuperar el checkpoint.
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END
def chatbot(state: MessagesState) -> dict:
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke(state["messages"])
return {"messages": [response]}
graph_builder = StateGraph(MessagesState)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
checkpointer = MemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
config_alice = {"configurable": {"thread_id": "alice_session_001"}}
config_bob = {"configurable": {"thread_id": "bob_session_001"}}
graph.invoke({"messages": [("user", "Soy Alice. Investigo sobre RAG.")]}, config_alice)
graph.invoke({"messages": [("user", "Soy Bob. Investigo sobre fine-tuning.")]}, config_bob)
result_alice = graph.invoke({"messages": [("user", "¿Qué estoy investigando?")]}, config_alice)
print(f"Alice: {result_alice['messages'][-1].content}")
# Output: "Estás investigando sobre RAG..."
result_bob = graph.invoke({"messages": [("user", "¿Qué estoy investigando?")]}, config_bob)
print(f"Bob: {result_bob['messages'][-1].content}")
# Output: "Estás investigando sobre fine-tuning..."
Cada thread_id tiene su propia línea de checkpoints independiente. Alice y Bob nunca se mezclan. Esto es la base del soporte multi-usuario: en producción, cada usuario (o cada sesión de un usuario) tiene su propio thread_id.
Patrones comunes para thread_id
| Patrón | Ejemplo | Cuándo usarlo |
|---|---|---|
| Por usuario | "user_12345" | Una conversación continua por usuario |
| Por sesión | "user_12345_session_abc" | Múltiples conversaciones por usuario |
| Por tarea | "research_task_789" | Identificar una investigación específica |
| UUID | "550e8400-e29b-41d4-a716-446655440000" | Cuando necesitas unicidad garantizada |
Inspeccionar checkpoints: ver el estado del grafo
Los checkpoints no son solo para continuidad — son una herramienta de debugging. Puedes inspeccionar el estado exacto del grafo en cualquier punto de su ejecución.
get_state: ver el checkpoint actual
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END
def chatbot(state: MessagesState) -> dict:
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke(state["messages"])
return {"messages": [response]}
graph_builder = StateGraph(MessagesState)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
checkpointer = MemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "debug_session"}}
graph.invoke({"messages": [("user", "¿Qué es prompt engineering?")]}, config)
graph.invoke({"messages": [("user", "Dame 3 técnicas")]}, config)
state_snapshot = graph.get_state(config)
print(f"Número de mensajes: {len(state_snapshot.values['messages'])}")
print(f"Último nodo ejecutado: {state_snapshot.next}")
print(f"Config: {state_snapshot.config}")
print(f"Checkpoint ID: {state_snapshot.config['configurable']['checkpoint_id']}")
for msg in state_snapshot.values["messages"]:
role = "User" if msg.type == "human" else "AI"
preview = msg.content[:80] + "..." if len(msg.content) > 80 else msg.content
print(f" [{role}] {preview}")
# Output esperado:
# Número de mensajes: 4
# Último nodo ejecutado: ()
# Config: {'configurable': {'thread_id': 'debug_session', 'checkpoint_id': '...'}}
# Checkpoint ID: 1ef8a...
# [User] ¿Qué es prompt engineering?
# [AI] Prompt engineering es la disciplina de diseñar instrucciones efectivas...
# [User] Dame 3 técnicas
# [AI] 1) Zero-shot prompting: dar la instrucción sin ejemplos...
get_state() retorna un StateSnapshot con:
values: el estado completo del grafonext: tupla de nodos pendientes (vacía si terminó)config: la configuración incluyendo elcheckpoint_idmetadata: información adicional del checkpointparent_config: referencia al checkpoint anterior
get_state_history: navegar el historial completo
history = list(graph.get_state_history(config))
print(f"Total de checkpoints: {len(history)}")
for i, snapshot in enumerate(history):
n_msgs = len(snapshot.values.get("messages", []))
created = snapshot.metadata.get("created_by", "unknown")
step = snapshot.metadata.get("step", "?")
print(f" Checkpoint {i}: step={step}, mensajes={n_msgs}, created_by={created}")
# Output esperado:
# Total de checkpoints: 5
# Checkpoint 0: step=3, mensajes=4, created_by=loop
# Checkpoint 1: step=2, mensajes=3, created_by=loop
# Checkpoint 2: step=1, mensajes=2, created_by=loop
# Checkpoint 3: step=0, mensajes=1, created_by=loop
# Checkpoint 4: step=-1, mensajes=0, created_by=system
El historial está en orden cronológico inverso: el más reciente primero. Cada checkpoint corresponde a un paso en la ejecución del grafo. Checkpoint 4 (step=-1) es el estado inicial antes de que cualquier nodo ejecute.
Resumir desde un checkpoint específico (time-travel)
No solo puedes retomar desde el último checkpoint — puedes retomar desde cualquiera. Obtén el checkpoint deseado de get_state_history(), extrae su .config, y pasa ese config a graph.invoke():
history = list(graph.get_state_history(config))
old_checkpoint = history[-3] # checkpoint después del turno 1
result = graph.invoke(
{"messages": [("user", "Nueva pregunta")]},
old_checkpoint.config # retoma desde ese punto
)
Esto es time-travel debugging: retrocedes a un punto específico y creas una bifurcación. El historial original sigue intacto. El nuevo invoke desde el checkpoint antiguo crea una rama nueva. Los ejercicios 4 y 5 de esta cápsula lo implementan en detalle.
Checkpointing en grafos multi-nodo
El valor del checkpointing se amplifica en grafos con múltiples nodos. Cada nodo genera un checkpoint, así que si el proceso se interrumpe entre nodos, no pierdes el trabajo de los nodos anteriores.
from typing import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
class ResearchState(TypedDict):
query: str
sources_searched: list[str]
findings: list[str]
report: str
status: str
def decompose_query(state: ResearchState) -> dict:
print("[1/4] Descomponiendo query...")
return {"status": "decomposed"}
def search_sources(state: ResearchState) -> dict:
sources = ["web", "papers", "news"]
findings = []
for source in sources:
print(f"[2/4] Buscando en {source}...")
findings.append(f"Hallazgo de {source} sobre '{state['query']}'")
return {
"sources_searched": sources,
"findings": findings,
"status": "searched"
}
def analyze_findings(state: ResearchState) -> dict:
print(f"[3/4] Analizando {len(state['findings'])} hallazgos...")
return {"status": "analyzed"}
def generate_report(state: ResearchState) -> dict:
print("[4/4] Generando reporte...")
report = f"Reporte: {len(state['findings'])} hallazgos de {len(state['sources_searched'])} fuentes"
return {"report": report, "status": "completed"}
builder = StateGraph(ResearchState)
builder.add_node("decompose", decompose_query)
builder.add_node("search", search_sources)
builder.add_node("analyze", analyze_findings)
builder.add_node("report", generate_report)
builder.add_edge(START, "decompose")
builder.add_edge("decompose", "search")
builder.add_edge("search", "analyze")
builder.add_edge("analyze", "report")
builder.add_edge("report", END)
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "research_001"}}
result = graph.invoke(
{"query": "Estado del arte en RAG", "sources_searched": [], "findings": [], "report": "", "status": ""},
config
)
# Output:
# [1/4] Descomponiendo query...
# [2/4] Buscando en web...
# [2/4] Buscando en papers...
# [2/4] Buscando en news...
# [3/4] Analizando 3 hallazgos...
# [4/4] Generando reporte...
history = list(graph.get_state_history(config))
print(f"Checkpoints creados: {len(history)}")
# Output esperado: Checkpoints creados: 6
# (1 por nodo + 1 input + 1 sistema)
6 checkpoints para 4 nodos. Si el proceso se hubiera caído entre search y analyze, podrías retomar desde el checkpoint con status=searched — sin repetir la descomposición ni las búsquedas.
Qué contiene un checkpoint
Cada checkpoint almacena información completa para reconstruir el estado exacto del grafo:
| Campo | Contenido | Para qué sirve |
|---|---|---|
values | Estado completo del grafo | Retomar ejecución, debugging |
config | Thread ID + Checkpoint ID | Identificar este checkpoint exacto |
next | Nodos pendientes por ejecutar | Saber si el grafo terminó o no |
metadata | Source, writes, step, timestamp | Saber qué nodo produjo este estado |
parent_config | Config del checkpoint anterior | Navegar la cadena de checkpoints |
La limitación crítica: MemorySaver es volátil
MemorySaver almacena todo en la RAM del proceso. Esto significa:
Proceso Python activo:
✅ Checkpoints disponibles
✅ Puedes resumir conversaciones
✅ Historial completo accesible
Proceso Python se reinicia (deploy, crash, Ctrl+C):
❌ TODOS los checkpoints se pierden
❌ Todas las conversaciones empiezan de cero
❌ El historial completo desaparece
Esto NO es un defecto — es una decisión de diseño. MemorySaver está diseñado para:
| Escenario | ¿MemorySaver? | ¿Por qué? |
|---|---|---|
| Desarrollo local | ✅ Perfecto | Sin setup, sin dependencias |
| Tests automatizados | ✅ Ideal | Rápido, aislado, sin cleanup |
| Notebooks / prototipos | ✅ Excelente | Iteración inmediata |
| Producción | ❌ Nunca | Un restart = todo perdido |
| Multi-instancia (k8s) | ❌ Imposible | Cada instancia tiene su propia RAM |
Para producción necesitas un checkpointer durable. En la siguiente cápsula implementarás PostgresSaver — y vas a ver que el cambio es una sola línea de código. Todo lo que aprendiste aquí sobre thread_id, get_state(), get_state_history(), y el flujo de checkpointing funciona exactamente igual. Solo cambia dónde se guardan los datos.
Troubleshooting
Problema 1: "RunnableConfigurableFieldsSpec" o error al invocar sin config
Síntoma: Error críptico al hacer graph.invoke({"messages": [...]}) sin pasar config.
Causa: Cuando compilas con checkpointer, el thread_id es obligatorio.
Solución: Siempre pasa config = {"configurable": {"thread_id": "algún_id"}} como segundo argumento a invoke().
Problema 2: El agente no recuerda turnos anteriores
Síntoma: Cada invoke parece una conversación nueva.
Causa 1: Estás usando un thread_id diferente en cada invoke.
Causa 2: El checkpointer no se pasó al compilar.
Solución: Verifica que (1) el mismo thread_id se use en ambos invokes y (2) graph_builder.compile(checkpointer=checkpointer) incluya el checkpointer.
Problema 3: Checkpoints desaparecen después de reiniciar el script
Síntoma: Ejecutas el script, funciona. Lo ejecutas de nuevo, el agente no recuerda nada. Causa: MemorySaver es in-memory. Cuando el proceso de Python termina, los checkpoints se pierden. Solución: Esto es comportamiento esperado de MemorySaver. Para persistencia entre reinicios, usa PostgresSaver (cápsula 04).
Problema 4: Mezcla de conversaciones entre usuarios
Síntoma: Un usuario recibe contexto de otro usuario.
Causa: Estás reutilizando el mismo thread_id para diferentes usuarios.
Solución: Usa un thread_id único por usuario o por sesión. Un patrón seguro: f"user_{user_id}_session_{session_id}".
Problema 5: El historial crece sin límite y el modelo falla
Síntoma: Después de muchos turnos, el modelo lanza error de context window.
Causa: El checkpointer guarda todos los mensajes. Después de 50+ turnos, la lista de mensajes excede el límite del modelo.
Solución: Implementa message trimming antes de llamar al modelo. La cápsula 02 de este módulo cubre trim_messages() para esto.
Ejercicios
Ejercicio 1: Chatbot con memoria básica (Fácil)
Crea un grafo con un solo nodo chatbot que use MessagesState y MemorySaver. Haz 3 invocaciones con el mismo thread_id: (1) "Me llamo Carlos", (2) "¿Cuál es mi nombre?", (3) "¿Cuántos mensajes llevamos?". Verifica que el modelo recuerda el nombre y puede contar los turnos.
Ver solución
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END
def chatbot(state: MessagesState) -> dict:
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke(state["messages"])
return {"messages": [response]}
graph_builder = StateGraph(MessagesState)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
checkpointer = MemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "carlos_001"}}
r1 = graph.invoke({"messages": [("user", "Me llamo Carlos")]}, config)
print(f"Turno 1: {r1['messages'][-1].content}")
r2 = graph.invoke({"messages": [("user", "¿Cuál es mi nombre?")]}, config)
print(f"Turno 2: {r2['messages'][-1].content}")
r3 = graph.invoke({"messages": [("user", "¿Cuántos mensajes llevamos en esta conversación?")]}, config)
print(f"Turno 3: {r3['messages'][-1].content}")
state = graph.get_state(config)
print(f"\nMensajes totales en el checkpoint: {len(state.values['messages'])}")
assert len(state.values["messages"]) == 6, "Debe haber 6 mensajes (3 user + 3 AI)"
print("✅ Checkpoint contiene toda la conversación")
# Output esperado:
# Turno 1: ¡Hola Carlos! Encantado de conocerte...
# Turno 2: Tu nombre es Carlos...
# Turno 3: Llevamos 3 intercambios (6 mensajes en total)...
# Mensajes totales en el checkpoint: 6
# ✅ Checkpoint contiene toda la conversación
Explicación: Cada invoke agrega un mensaje del usuario y la respuesta del modelo al checkpoint. Después de 3 turnos hay 6 mensajes. El modelo tiene acceso a toda la historia gracias al checkpointer.
Ejercicio 2: Aislamiento multi-usuario (Fácil)
Crea un chatbot con MemorySaver. Usa dos thread_id diferentes ("user_A" y "user_B"). En thread A, dile al modelo "Mi lenguaje favorito es Python". En thread B, dile "Mi lenguaje favorito es Rust". Luego pregunta en cada thread "¿Cuál es mi lenguaje favorito?" y verifica que las respuestas son correctas y no se mezclan.
Ver solución
from langchain.chat_models import init_chat_model
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, MessagesState, START, END
def chatbot(state: MessagesState) -> dict:
model = init_chat_model("openai:gpt-4.1-mini")
response = model.invoke(state["messages"])
return {"messages": [response]}
graph_builder = StateGraph(MessagesState)
graph_builder.add_node("chatbot", chatbot)
graph_builder.add_edge(START, "chatbot")
graph_builder.add_edge("chatbot", END)
checkpointer = MemorySaver()
graph = graph_builder.compile(checkpointer=checkpointer)
config_a = {"configurable": {"thread_id": "user_A"}}
config_b = {"configurable": {"thread_id": "user_B"}}
graph.invoke({"messages": [("user", "Mi lenguaje favorito es Python")]}, config_a)
graph.invoke({"messages": [("user", "Mi lenguaje favorito es Rust")]}, config_b)
result_a = graph.invoke({"messages": [("user", "¿Cuál es mi lenguaje favorito?")]}, config_a)
result_b = graph.invoke({"messages": [("user", "¿Cuál es mi lenguaje favorito?")]}, config_b)
print(f"User A: {result_a['messages'][-1].content}")
print(f"User B: {result_b['messages'][-1].content}")
assert "python" in result_a["messages"][-1].content.lower()
assert "rust" in result_b["messages"][-1].content.lower()
print("\n✅ Las conversaciones están correctamente aisladas")
# Output esperado:
# User A: Tu lenguaje favorito es Python...
# User B: Tu lenguaje favorito es Rust...
# ✅ Las conversaciones están correctamente aisladas
Explicación: Cada thread_id mantiene su propia cadena de checkpoints. El checkpointer nunca mezcla estados entre threads diferentes. Esto es la base del soporte multi-usuario.
Ejercicio 3: Inspeccionar el historial de checkpoints (Medio)
Construye un grafo con 3 nodos secuenciales (analyze → enrich → summarize), cada uno agrega un campo al estado. Usa get_state_history() para verificar que cada checkpoint tiene progresivamente más datos.
Ver solución
from typing import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
class PipelineState(TypedDict):
input_text: str
analysis: str
enrichment: str
summary: str
def analyze(state: PipelineState) -> dict:
return {"analysis": f"Análisis de: {state['input_text'][:30]}"}
def enrich(state: PipelineState) -> dict:
return {"enrichment": f"Datos para: {state['analysis'][:30]}"}
def summarize(state: PipelineState) -> dict:
return {"summary": f"Resumen: {state['analysis'][:20]} + {state['enrichment'][:20]}"}
builder = StateGraph(PipelineState)
builder.add_node("analyze", analyze)
builder.add_node("enrich", enrich)
builder.add_node("summarize", summarize)
builder.add_edge(START, "analyze")
builder.add_edge("analyze", "enrich")
builder.add_edge("enrich", "summarize")
builder.add_edge("summarize", END)
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "pipeline_001"}}
graph.invoke({"input_text": "LangGraph para agentes", "analysis": "", "enrichment": "", "summary": ""}, config)
history = list(graph.get_state_history(config))
print(f"Total checkpoints: {len(history)}\n")
for i, snap in enumerate(history):
filled = [k for k, v in snap.values.items() if v]
print(f" Checkpoint {i}: campos={filled}")
# Output esperado:
# Total checkpoints: 5
# Checkpoint 0: campos=['input_text', 'analysis', 'enrichment', 'summary']
# Checkpoint 1: campos=['input_text', 'analysis', 'enrichment']
# ...
assert len(history) == 5
print("✅ Progresión de checkpoints verificada")
Explicación: Cada nodo agrega un campo. El historial muestra la progresión: el más antiguo solo tiene input_text, el más reciente tiene todos los campos.
Ejercicio 4: Time-travel — bifurcar desde un checkpoint anterior (Medio)
Usa el grafo del Ejercicio 3. Obtén el checkpoint después de analyze (antes de enrich). Invoca desde ese checkpoint con graph.invoke(None, checkpoint.config). Verifica que enrich y summarize se re-ejecutan pero analysis se preserva.
Ver solución
from typing import TypedDict
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
class PipelineState(TypedDict):
input_text: str
analysis: str
enrichment: str
summary: str
def analyze(state: PipelineState) -> dict:
return {"analysis": f"Análisis de: {state['input_text'][:30]}"}
def enrich(state: PipelineState) -> dict:
return {"enrichment": f"Enriquecido: {state['analysis'][:30]}"}
def summarize(state: PipelineState) -> dict:
return {"summary": f"Resumen: {state['analysis'][:20]} + {state['enrichment'][:20]}"}
builder = StateGraph(PipelineState)
for name, fn in [("analyze", analyze), ("enrich", enrich), ("summarize", summarize)]:
builder.add_node(name, fn)
builder.add_edge(START, "analyze")
builder.add_edge("analyze", "enrich")
builder.add_edge("enrich", "summarize")
builder.add_edge("summarize", END)
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "time_travel_ex"}}
original = graph.invoke(
{"input_text": "LangGraph para agentes", "analysis": "", "enrichment": "", "summary": ""}, config
)
history = list(graph.get_state_history(config))
after_analyze = next(s for s in history if s.values.get("analysis") and not s.values.get("enrichment"))
branched = graph.invoke(None, after_analyze.config)
assert branched["analysis"] == original["analysis"], "Analysis debe preservarse"
assert branched["summary"] != "", "Summary debe regenerarse"
print(f"Analysis preservado: {branched['analysis']}")
print(f"Enrichment regenerado: {branched['enrichment']}")
print("✅ Time-travel bifurcation exitosa")
# Output esperado:
# Analysis preservado: Análisis de: LangGraph para agentes
# Enrichment regenerado: Enriquecido: Análisis de: LangGraph para ag
# ✅ Time-travel bifurcation exitosa
Explicación: graph.invoke(None, after_analyze.config) retoma desde el checkpoint sin agregar input nuevo. El pipeline re-ejecuta enrich y summarize desde ese punto, preservando el analysis original.
Ejercicio 5: Simular crash y recovery (Avanzado)
Crea un grafo con 4 nodos secuenciales (step_1 → step_2 → step_3 → step_4) donde cada nodo agrega su nombre a una lista steps_completed en el estado. Ejecuta el grafo normalmente y guarda el resultado. Luego simula un "crash" obteniendo el checkpoint después de step_2 y reanudando desde ahí. Verifica que la ejecución reanudada completa step_3 y step_4 sin repetir step_1 y step_2.
Ver solución
import operator
from typing import TypedDict, Annotated
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph, START, END
class CrashState(TypedDict):
task: str
steps_completed: Annotated[list[str], operator.add]
output: str
def step_1(state: CrashState) -> dict:
print("[step_1] Ejecutando...")
return {"steps_completed": ["step_1"]}
def step_2(state: CrashState) -> dict:
print("[step_2] Ejecutando...")
return {"steps_completed": ["step_2"]}
def step_3(state: CrashState) -> dict:
print("[step_3] Ejecutando...")
return {"steps_completed": ["step_3"]}
def step_4(state: CrashState) -> dict:
print("[step_4] Ejecutando...")
all_steps = state["steps_completed"] + ["step_4"]
return {"steps_completed": ["step_4"], "output": f"Completado: {', '.join(all_steps)}"}
builder = StateGraph(CrashState)
for name, fn in [("step_1", step_1), ("step_2", step_2), ("step_3", step_3), ("step_4", step_4)]:
builder.add_node(name, fn)
builder.add_edge(START, "step_1")
builder.add_edge("step_1", "step_2")
builder.add_edge("step_2", "step_3")
builder.add_edge("step_3", "step_4")
builder.add_edge("step_4", END)
checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "crash_sim"}}
full_result = graph.invoke({"task": "test", "steps_completed": [], "output": ""}, config)
print(f"\nFull run: {full_result['steps_completed']}")
history = list(graph.get_state_history(config))
after_step_2 = None
for snap in history:
completed = snap.values.get("steps_completed", [])
if "step_2" in completed and "step_3" not in completed:
after_step_2 = snap
break
assert after_step_2 is not None, "No se encontró checkpoint post-step_2"
print(f"\n--- Simulando crash y recovery desde step_2 ---")
print(f"Estado en checkpoint: steps_completed={after_step_2.values['steps_completed']}")
recovered = graph.invoke(None, after_step_2.config)
print(f"\nRecovered steps: {recovered['steps_completed']}")
print(f"Output: {recovered['output']}")
assert "step_1" in recovered["steps_completed"]
assert "step_2" in recovered["steps_completed"]
assert "step_3" in recovered["steps_completed"]
assert "step_4" in recovered["steps_completed"]
print("\n✅ Recovery exitoso: step_3 y step_4 ejecutados sin repetir step_1 y step_2")
# Output esperado:
# [step_1] Ejecutando...
# [step_2] Ejecutando...
# [step_3] Ejecutando...
# [step_4] Ejecutando...
#
# Full run: ['step_1', 'step_2', 'step_3', 'step_4']
#
# --- Simulando crash y recovery desde step_2 ---
# Estado en checkpoint: steps_completed=['step_1', 'step_2']
# [step_3] Ejecutando...
# [step_4] Ejecutando...
#
# Recovered steps: ['step_1', 'step_2', 'step_3', 'step_4']
# Output: Completado: step_1, step_2, step_3, step_4
#
# ✅ Recovery exitoso: step_3 y step_4 ejecutados sin repetir step_1 y step_2
Explicación: El reducer Annotated[list[str], operator.add] acumula los pasos completados. Después de recuperar el checkpoint post-step_2, graph.invoke(None, config) continúa desde ese punto. Solo se ejecutan step_3 y step_4 — los prints lo confirman. El estado final contiene los 4 pasos, 2 del checkpoint original y 2 de la ejecución reanudada.
Resumen
En esta cápsula aprendiste:
- Checkpointing guarda el estado completo del grafo después de cada nodo. No es un diff, es un snapshot. Si el proceso se interrumpe, puedes retomar desde el último checkpoint sin perder trabajo previo
- MemorySaver es el checkpointer para desarrollo. Una línea (
checkpointer = MemorySaver()), sin dependencias externas, sin configuración. Perfecto para prototipar y testear thread_ides obligatorio y es el mecanismo de aislamiento. Cada thread tiene su propia cadena de checkpoints. Múltiples usuarios = múltiples thread_ids = historiales completamente separadosget_state()yget_state_history()son herramientas de debugging. Puedes ver el estado exacto del grafo en cualquier punto de su ejecución — qué mensajes había, qué nodos se ejecutaron, qué datos tenía- Time-travel: puedes retomar desde cualquier checkpoint, no solo el último. Esto crea bifurcaciones en la historia — útil para debugging y para explorar caminos alternativos
- MemorySaver es volátil: si el proceso se reinicia, todos los checkpoints se pierden. Esto es esperado y aceptable para desarrollo. Para producción, necesitas persistencia durable
Próxima cápsula: Persistencia con PostgresSaver y Redis — migrar de MemorySaver a PostgresSaver es cambiar una línea de código. Todo lo demás (thread_id, get_state, get_state_history) funciona exactamente igual. El valor: tus checkpoints sobreviven a crashes, restarts, y deploys.
Recursos adicionales
- LangGraph — Persistence — Documentación oficial de checkpointing en LangGraph. Cubre MemorySaver, PostgresSaver, y la arquitectura general del sistema de persistencia
- LangGraph — How to add thread-level persistence — Guía paso a paso para implementar checkpointing con thread_id
- LangGraph — How to manage conversation history — Estrategias para mantener el historial de mensajes bajo control cuando usas checkpointing
- LangGraph — Time travel — Guía oficial de time-travel debugging: navegar checkpoints, hacer replay, y bifurcar ejecuciones
- LangGraph — MemorySaver Reference — API reference del MemorySaver con todos los métodos disponibles
Módulo 8 — LangChain & LangGraph: From Chains to Agents