LangGraph — Módulo 10: memoria de largo plazo con Store
Décimo módulo del curso de LangGraph: recordar al usuario entre conversaciones. InMemoryStore, namespaces, put/get/search, acceso al store desde los nodos y búsqueda semántica con embeddings.
☰ Índice del contenido
Décimo módulo del Curso completo de LangGraph. En el Módulo 6 le dimos memoria al agente: dentro de un thread, recuerda todo. Pero abre una conversación nueva y vuelve a ser un desconocido. Hoy resolvemos eso.
La tesis del módulo
El checkpointer recuerda la conversación; el Store recuerda al usuario. Son dos sistemas distintos porque responden preguntas distintas: “¿qué dijimos hace tres mensajes?” y “¿qué sé de esta persona desde siempre?”. Confundirlos es el error clásico.
Las dos memorias, lado a lado
Checkpointer → clave: thread_id → el historial de UNA conversación
Store → clave: namespace → hechos que cruzan TODAS las conversaciones
| Checkpointer | Store | |
|---|---|---|
| Alcance | Un thread | Entre threads |
| Qué guarda | El estado del grafo | Documentos JSON que tú defines |
| Quién escribe | LangGraph, automático | Tú, explícitamente |
| Analogía | La memoria de trabajo | La libreta de notas |
Esa fila de “quién escribe” es la más importante: el Store no se llena solo. Tú decides qué merece recordarse.
El Store en tres operaciones
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
namespace = ("memorias", "usuario_42")
store.put(namespace, "preferencias", {"tono": "directo", "idioma": "es"})
item = store.get(namespace, "preferencias")
print(item.value) # → {'tono': 'directo', 'idioma': 'es'}
resultados = store.search(namespace)
Tres piezas:
namespace— una tupla que organiza la memoria jerárquicamente. Por convención empieza por el ámbito y termina por el dueño:("memorias", user_id). Es tu frontera de aislamiento: un namespace por usuario significa que nunca puedes leer las memorias de otro por accidente.key— el identificador del documento dentro del namespace.value— un diccionario JSON. Lo que quepa ahí, tú lo defines.
InMemoryStore es para desarrollo, igual que InMemorySaver. En producción hay una versión respaldada en Postgres.
Usarlo desde un nodo
Pasas el store al compilar y LangGraph se lo inyecta al nodo que lo declare como parámetro:
from langgraph.store.base import BaseStore
from langgraph.graph import MessagesState
def responder(state: MessagesState, store: BaseStore, config) -> dict:
user_id = config["configurable"]["user_id"]
ns = ("memorias", user_id)
# 1. Recuperar lo que sabemos
recuerdos = store.search(ns)
contexto = "\n".join(f"- {r.value['texto']}" for r in recuerdos)
# 2. Inyectarlo en el prompt
sistema = f"Lo que sabes del usuario:\n{contexto}" if contexto else "Aún no lo conoces."
respuesta = llm.invoke([{"role": "system", "content": sistema}] + state["messages"])
return {"messages": [respuesta]}
graph = builder.compile(checkpointer=checkpointer, store=store)
Y al invocar, el user_id viaja en la config — separado del thread_id, porque un usuario tiene muchas conversaciones:
graph.invoke(
entrada,
{"configurable": {"thread_id": "chat-88", "user_id": "usuario_42"}},
)
usuario_42
├── thread "chat-88" ← checkpointer (historial de hoy)
├── thread "chat-91" ← checkpointer (historial de mañana)
└── namespace ("memorias", "usuario_42")
← store (lo que sabes de él, siempre)
Escribir memorias: el patrón que sí funciona
Recuperar es la parte fácil. Lo difícil es decidir qué guardar, y ahí hay dos enfoques.
En caliente — un nodo evalúa después de cada turno si apareció algo digno de recordar:
from uuid import uuid4
def guardar_memoria(state: MessagesState, store: BaseStore, config) -> dict:
ns = ("memorias", config["configurable"]["user_id"])
extraccion = llm.invoke([
{"role": "system", "content":
"Extrae hechos duraderos sobre el usuario (preferencias, restricciones, "
"contexto personal). Ignora lo circunstancial. Si no hay nada, responde NADA."},
*state["messages"][-2:],
])
if extraccion.content.strip() != "NADA":
store.put(ns, str(uuid4()), {"texto": extraccion.content})
return {}
Simple y funciona. Cuesta una llamada extra al modelo por turno y agrega latencia.
En frío — acumulas la conversación y procesas las memorias después (al cerrar el thread, o en un job programado). El chat va rápido y el costo se mueve fuera del camino crítico. Es lo que usarías en producción con volumen.
El str(uuid4()) como key hace que cada memoria sea un documento nuevo. Si en cambio usas una key fija ("preferencias"), cada put sobrescribe — que es lo que quieres para datos que se actualizan en vez de acumularse.
El prompt de extracción es donde se gana o se pierde. Si no distingues “soy alérgico a los mariscos” (duradero) de “hoy tengo hambre” (circunstancial), en dos semanas tienes cientos de memorias basura y el agente peor que antes.
Cuando hay muchas memorias: búsqueda semántica
store.search(ns) sin más te devuelve todo. Con 500 memorias, eso no cabe en el prompt. La solución es buscar solo lo relevante para el mensaje actual, y para eso el store puede indexar con embeddings:
from langchain.embeddings import init_embeddings
store = InMemoryStore(
index={
"embed": init_embeddings("openai:text-embedding-3-small"),
"dims": 1536,
}
)
store.put(ns, "m1", {"texto": "Es alérgico a los mariscos"})
store.put(ns, "m2", {"texto": "Prefiere respuestas cortas"})
relevantes = store.search(ns, query="¿me recomiendas un restaurante?", limit=3)
# → devuelve "Es alérgico a los mariscos" primero
query activa la búsqueda por significado, no por palabras exactas: “restaurante” recupera la alergia sin que compartan una sola palabra. También puedes combinarla con filter para acotar por metadata.
Este es el mismo mecanismo que hay detrás de un RAG. La diferencia es qué indexas: en RAG, documentos; aquí, hechos sobre el usuario.
Cuándo NO usar Store
- Datos que ya tienes en tu base de datos. El nombre, el plan contratado, el historial de compras: eso se consulta con una tool, no se duplica como “memoria”.
- Contexto de la conversación actual. Eso es el checkpointer.
- Todo, por si acaso. Una memoria que nunca se recupera es solo costo de almacenamiento y ruido en la búsqueda.
Y una advertencia que no es técnica: estás guardando datos personales de forma persistente. Eso implica poder mostrarle al usuario qué recuerdas de él, y poder borrarlo. Diséñalo desde el principio: store.delete(namespace, key) existe, y el namespace por usuario hace que borrar todo sea trivial. Retrofitear esto después es doloroso.
Errores comunes
- Confundir Store con checkpointer. Si guardas el historial del chat en el Store, estás reimplementando mal el Módulo 6.
- Namespace sin el id del usuario. Un namespace global mezcla memorias entre personas: es un incidente de privacidad, no un bug.
- Guardar todo lo que se dice. El Store se llena de ruido y la recuperación empeora.
- Recuperar todo el Store en cada turno. Funciona con 10 memorias, colapsa con 500. Usa
searchconquery. - Olvidar
store=en elcompile(). El nodo no recibe la inyección. - No tener forma de borrar. Un usuario que pide “olvida eso” y no puedes cumplir es un problema legal antes que técnico.
La frase para llevarse
“La memoria de largo plazo no es guardar más: es elegir mejor. Un agente con diez cosas bien elegidas se siente más atento que uno con mil recuerdos irrelevantes.”
Qué viene
En el Módulo 11 llevamos todo esto a producción: ejecución durable, modos de persistencia, reintentos, caché de nodos y time travel para depurar lo que ya pasó.
Material relacionado
- Glosario de LangGraph
- Memoria de largo plazo en agentes — el concepto, sin código
- Módulo 6: persistencia y memoria de corto plazo
Preguntas sobre este curso
Regístrate gratis para hacer preguntas y guardar tu progreso.
✉ Entérate de lo nuevo
Recibe un aviso cuando publique nuevos cursos, workshops o investigaciones. Sin spam.