Memoria en Langchain¶

Hasta ahora hemos construido agentes que responden pregunta a pregunta, pero que olvidan todo lo anterior en cuanto termina una invocación. Eso está bien para tareas aisladas, pero resulta inútil para cualquier conversación que dure más de un mensaje.

En este artículo vamos a darle memoria al agente: la capacidad de recordar quién eres, qué preferencias has compartido y qué se ha dicho antes. Para no perder la continuidad con la serie, seguiremos con el ejemplo guía de las conversiones absurdas, pero ahora en formato de diálogo multi-turno.

Objetivos de este notebook:

  1. Observar cómo un agente sin memoria olvida el contexto entre mensajes.
  2. Añadir memoria a un agente con InMemorySaver y thread_id.
  3. Demostrar que la memoria está aislada por hilo de conversación.
  4. Entender el límite de la memoria en memoria y hacia dónde ir cuando necesitemos persistencia real.
In [1]:
# Cargamos variables de entorno. Buscamos el archivo .env hacia arriba desde
# la carpeta donde se ejecuta el notebook.
from dotenv import load_dotenv
import os

load_dotenv()

OPENROUTER_API_KEY = os.environ["OPENROUTER_API_KEY"]
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"

Antes de empezar necesitas:

  • Python 3.10+
  • pip install langchain langchain-openai python-dotenv langgraph
  • Un archivo .env con OPENROUTER_API_KEY=...

1. Agente sin memoria: cada mensaje empieza de cero¶

Creamos un agente básico y le contamos quiénes somos y cuál es nuestra unidad favorita para comparar cosas. Luego le preguntamos si lo recuerda. Sin memoria, no tendrá forma de saberlo.

In [2]:
from langchain_openai import ChatOpenAI
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage

# Usamos el mismo gateway y modelo de los artículos anteriores.
model = ChatOpenAI(
    model="openai/gpt-4o-mini",
    api_key=OPENROUTER_API_KEY,
    base_url=OPENROUTER_BASE_URL
)

# Agente sin checkpointer: no conserva nada entre invocaciones.
agent_sin_memoria = create_agent(model)
In [3]:
# Turno 1: presentamos al usuario y su unidad favorita.
presentacion = HumanMessage(content="Hola, me llamo Sergio y mi unidad favorita para comparar cosas es el plátano.")

respuesta = agent_sin_memoria.invoke({"messages": [presentacion]})
print(respuesta["messages"][-1].content)
¡Hola, Sergio! Usar el plátano como unidad de comparación es una forma divertida y relatable de expresar medidas. ¿Cómo lo utilizas? ¿Tienes algún ejemplo en mente?
In [4]:
# Turno 2: en una conversación humana esto debería ser trivial.
pregunta = HumanMessage(content="¿Cómo me llamo y cuál es mi unidad favorita?")

respuesta = agent_sin_memoria.invoke({"messages": [pregunta]})
print(respuesta["messages"][-1].content)
No tengo acceso a información personal sobre ti, por lo que no puedo saber tu nombre ni tu unidad favorita. Si quieres compartir esa información, estaré encantado de ayudarte con cualquier otra pregunta que tengas relacionada.

Nota didáctica: el agente no está siendo tonto: cada invoke es una petición independiente. El modelo solo ve los mensajes que le pasamos en esa llamada, así que no tiene forma de saber lo que le dijimos antes. La solución no está en el prompt, sino en añadir una capa de estado que conserve el historial.

2. Agente con memoria: InMemorySaver + thread_id¶

LangGraph proporciona checkpointers para guardar el estado de una conversación. InMemorySaver mantiene todo en memoria RAM, lo cual es perfecto para aprender y para prototipos. Para usarlo debemos pasar un thread_id: es el identificador de la conversación.

In [5]:
from langgraph.checkpoint.memory import InMemorySaver

# El checkpointer es el encargado de recordar el estado entre mensajes.
memory = InMemorySaver()

agent_con_memoria = create_agent(
    model,
    checkpointer=memory
)
In [6]:
# El config identifica la conversación. Mientras usemos el mismo thread_id,
# el agente tendrá acceso a todo lo dicho anteriormente.
config_sergio = {"configurable": {"thread_id": "sergio-123"}}

presentacion = HumanMessage(content="Hola, me llamo Sergio y mi unidad favorita para comparar cosas es el plátano.")

respuesta = agent_con_memoria.invoke(
    {"messages": [presentacion]},
    config_sergio
)
print(respuesta["messages"][-1].content)
¡Hola, Sergio! Usar el plátano como unidad de medida es una forma divertida y creativa de comparar cosas. ¿Tienes algún ejemplo de comparación que te guste hacer con plátanos?
In [7]:
# Misma conversación, mismo thread_id. Ahora el agente debería recordar.
pregunta = HumanMessage(content="¿Cómo me llamo y cuál es mi unidad favorita?")

respuesta = agent_con_memoria.invoke(
    {"messages": [pregunta]},
    config_sergio
)
print(respuesta["messages"][-1].content)
Te llamas Sergio y tu unidad favorita para comparar cosas es el plátano. ¡Espero que sigas disfrutando de tus comparaciones!
In [8]:
# Incluso puede usar la preferencia recordada para resolver una nueva pregunta
# sin que se la volvamos a dar.
pregunta = HumanMessage(content="¿Cuántos plátanos mide de largo la Torre Eiffel?")

respuesta = agent_con_memoria.invoke(
    {"messages": [pregunta]},
    config_sergio
)
print(respuesta["messages"][-1].content)
La Torre Eiffel mide aproximadamente 330 metros de altura, incluyendo las antenas en la parte superior. Si tomamos un plátano promedio que mide alrededor de 20 cm de largo, podemos hacer el cálculo:

330 metros = 33000 cm  
33000 cm ÷ 20 cm/plátano = 1650 plátanos

Así que la Torre Eiffel mide aproximadamente 1650 plátanos de largo. ¡Es una forma interesante de imaginar su altura!

3. Aislamiento por hilo: cada thread_id es una conversación diferente¶

La memoria no es global. Si cambiamos de thread_id, el agente accede a otro historial limpio. Esto es esencial en aplicaciones reales donde conviven muchos usuarios o muchas sesiones.

In [9]:
# Otra conversación con otro identificador.
config_ana = {"configurable": {"thread_id": "ana-456"}}

presentacion_ana = HumanMessage(content="Hola, me llamo Ana y mi unidad favorita es el elefante.")

respuesta = agent_con_memoria.invoke(
    {"messages": [presentacion_ana]},
    config_ana
)
print(respuesta["messages"][-1].content)
¡Hola, Ana! Es genial que te gusten los elefantes. Son animales fascinantes e inteligentes. ¿Tienes alguna razón en particular por la que te gustan?
In [10]:
# Preguntamos en el hilo de Ana. No debe confundirse con Sergio.
pregunta = HumanMessage(content="¿Cómo me llamo y cuál es mi unidad favorita?")

respuesta = agent_con_memoria.invoke(
    {"messages": [pregunta]},
    config_ana
)
print(respuesta["messages"][-1].content)
Te llamas Ana y tu unidad favorita es el elefante.

Nota didáctica: el aislamiento por thread_id es lo que permite usar el mismo agente compilado para atender a múltiples usuarios. Cada uno ve solo su propio historial, aunque internamente compartan el mismo objeto agent.

4. Límite de InMemorySaver: la memoria vive solo mientras corre el proceso¶

InMemorySaver es ideal para aprender y prototipar, pero tiene un límite claro: si reiniciamos el kernel de Jupyter o cae el proceso de Python, todo el historial desaparece.

Para producción se usan checkpointers persistentes como:

  • SqliteSaver (base de datos SQLite local).
  • PostgresSaver (base de datos PostgreSQL).
  • Otros backends soportados por LangGraph.

El concepto es el mismo: un thread_id apunta a un estado guardado. Lo único que cambia es dónde se guarda.

Cierre y siguiente paso¶

En este artículo hemos visto que un agente sin memoria es una máquina de responder preguntas aisladas, mientras que un agente con memoria puede mantener una conversación coherente a lo largo del tiempo. La clave ha sido el checkpointer y el thread_id.

Hemos usado InMemorySaver por simplicidad, pero hemos dejado claro cuál es su techo: no sobrevive a un reinicio. Cuando necesites persistencia real, el salto es mecánico: cambias el saver, el resto del código sigue igual.

Honestidad técnica: aunque el agente recuerde el contexto, sigue siendo un modelo de lenguaje. Si le pedimos cifras exactas sin una herramienta, seguirá inventando o redondeando. La memoria no sustituye a una fuente de verdad.

El siguiente artículo ampliará lo que el agente puede procesar: pasaremos de mensajes de texto a multimodal messages, donde el modelo puede recibir y razonar sobre imágenes, audio o incluso documentos.