Configura memoria con bases de datos vectoriales

Lectura
20 min~13 min lectura

Introducción: La Necesidad de la Memoria Persistente

En el desarrollo de agentes de IA avanzados con LangGraph, la capacidad de recordar interacciones pasadas más allá de una sola sesión de ejecución no es un lujo, sino una necesidad fundamental para aplicaciones útiles. Un agente que olvida todo lo conversado al reiniciarse es como un asistente humano con amnesia severa; cada interacción comienza desde cero, frustrando al usuario y limitando severamente la complejidad de las tareas que puede manejar. La memoria transitoria en RAM, típica de las ejecuciones simples, se evapora cuando el proceso termina, lo que es inaceptable para sistemas desplegados en producción.

Aquí es donde entra la memoria persistente, y específicamente, las bases de datos vectoriales. No se trata solo de guardar texto crónico en un disco. La verdadera potencia reside en almacenar representaciones semánticas de la información, lo que permite al agente recuperar recuerdos relevantes de manera inteligente, no solo por palabras clave exactas, sino por significado y contexto. Esta lección se adentrará en la configuración práctica de esta memoria persistente utilizando bases de datos vectoriales, conectando el estado fluido de tu grafo de LangGraph con un almacenamiento duradero y recuperable. Dominar esto es lo que separa un prototipo académico de un agente listo para el mundo real.

Concepto Clave: Bases de Datos Vectoriales como Memoria Semántica

Para entender el rol de una base de datos vectorial, debemos alejarnos del modelo de base de datos tradicional. Una base de datos SQL o documental almacena y recupera datos basándose en valores exactos o índices predefinidos. En cambio, una base de datos vectorial almacena embeddings: representaciones numéricas densas (vectores de alta dimensión) que capturan el significado semántico de un texto, imagen o dato. La operación clave es la búsqueda por similitud: dado un vector de consulta, el sistema encuentra los vectores almacenados más "cercanos" en el espacio multidimensional, es decir, los conceptualmente más similares.

Una analogía del mundo real útil es la de una biblioteca versus el cerebro de un experto. Una biblioteca tradicional (base de datos SQL) organiza libros por código exacto (ISBN, autor, título). Para encontrar información sobre "técnicas de cultivo sostenible en climas áridos", necesitas saber las palabras clave exactas o el sistema de clasificación. El cerebro del experto (base de datos vectorial), sin embargo, funciona por conceptos. Si le preguntas, relaciona tu consulta con ideas de "conservación de agua", "agricultura de secano", "xeropaisajismo" y "resiliencia climática", recuperando conocimientos relevantes incluso si no usaste esos términos exactos. La base de datos vectorial actúa como este cerebro semántico externo para tu agente, permitiéndole recordar no frases textuales idénticas, sino el significado de interacciones pasadas.

En el contexto de LangGraph, los "recuerdos" (fragmentos de conversación, resultados de herramientas, metadatos de estado) se convierten en embeddings y se almacenan. Cuando el agente necesita contexto para proceder, convierte la situación actual en un vector de consulta y pide a la base de datos: "dame los recuerdos más relevantes a lo que está pasando ahora". Esta recuperación contextual es el núcleo de un agente con memoria verdadera.

Cómo Funciona en la Práctica: Arquitectura y Flujo de Datos

Integrar una base de datos vectorial en un agente LangGraph implica diseñar una arquitectura clara para la lectura y escritura de la memoria. El flujo no es unidireccional; es un ciclo continuo de recuperación, razonamiento y almacenamiento. Primero, debes definir qué se guarda en la memoria. No todo el estado volátil del grafo debe persistirse. Típicamente, se almacenan los mensajes clave de la conversación (intercambios humano-agente), junto con metadatos como timestamps, IDs de sesión y posiblemente resúmenes de sub-diálogos complejos. La decisión de qué es "memorizable" es crucial para el rendimiento y la relevancia.

El proceso paso a paso comienza con la inicialización. Al arrancar el agente para una sesión (e.g., un usuario específico), el primer paso es consultar la base de datos vectorial usando un embedding derivado del ID de usuario y/o los primeros mensajes, para recuperar el historial de conversaciones relevantes previas. Estos recuerdos se inyectan en el estado inicial del grafo, proporcionando contexto desde el primer momento. Luego, durante la ejecución del grafo, en puntos estratégicos (normalmente después de que el agente produce una respuesta o una herramienta devuelve un resultado), se toma el nuevo contenido, se genera su embedding, y se persiste en la base de datos vectorial, asociado a metadatos de la sesión.

Un detalle crítico es la gestión de ventanas de contexto. Los modelos de lenguaje tienen límites de tokens. No puedes recuperar *todos* los recuerdos de una sesión de 6 meses. Por lo tanto, la consulta a la base de datos vectorial debe ser selectiva: "dame los 5 recuerdos más relevantes para la consulta actual". Esto asegura que el agente opere con el contexto más significativo, manteniendo la eficiencia. La base de datos vectorial maneja esta búsqueda semántica de manera optimizada, algo imposible con una consulta SQL a un campo de texto plano.

Código en Acción: Implementación con Chroma y LangChain

Vamos a construir un ejemplo concreto usando Chroma (una base de datos vectorial ligera y persistente) y los integradores de LangChain, que se alinean naturalmente con LangGraph. Configuraremos un sistema donde el agente recuerda hechos sobre un usuario entre sesiones.

# Instalaciones necesarias (correr en terminal):
# pip install langchain langchain-community langchain-chroma langgraph
# pip install sentence-transformers  # Para embeddings locales

import os
from langchain_chroma import Chroma
from langchain_community.embeddings import SentenceTransformerEmbeddings
from langchain.schema import Document
from langgraph.graph import StateGraph, END
from typing import TypedDict, List, Annotated
import operator
from datetime import datetime

# 1. DEFINICIÓN DEL ESTADO CON MEMORIA
class AgentState(TypedDict):
    user_id: str
    input_message: str
    conversation_history: List[str]
    retrieved_memories: List[str]
    agent_response: str

# 2. CONFIGURACIÓN DE LA BASE DE DATOS VECTORIAL PERSISTENTE
# Ruta persistente en disco
persist_directory = "./chroma_memory_db"
# Usamos un modelo de embeddings local para simplicidad
embedding_function = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
# Conexión a la base de datos. Reutiliza si existe, crea si no.
vectorstore = Chroma(
    collection_name="agent_memories",
    persist_directory=persist_directory,
    embedding_function=embedding_function
)

# 3. FUNCIÓN PARA AGREGAR UN RECUERDO A LA MEMORIA
def add_memory_to_store(user_id: str, memory_text: str):
    """Convierte un evento en un embedding y lo guarda en Chroma con metadatos."""
    # Crear un Documento de LangChain con el contenido y metadatos
    doc = Document(
        page_content=memory_text,
        metadata={
            "user_id": user_id,
            "timestamp": datetime.now().isoformat(),
            "type": "conversation_memory"
        }
    )
    # Añadir a la base de datos vectorial
    vectorstore.add_documents([doc])
    # Asegurar persistencia en disco
    vectorstore.persist()
    print(f"[Memoria guardada para {user_id}]: {memory_text[:50]}...")

# 4. FUNCIÓN PARA RECUPERAR MEMORIAS RELEVANTES
def retrieve_memories(user_id: str, query: str, k: int = 3) -> List[str]:
    """Recupera los k recuerdos más relevantes para un usuario y una consulta."""
    # Filtramos por user_id en los metadatos para aislar memorias de este usuario
    results = vectorstore.similarity_search_with_relevance_scores(
        query,
        k=k,
        filter={"user_id": user_id}  # Filtro crítico para privacidad/contexto
    )
    memories = [f"{doc.page_content} (Relevancia: {score:.2f})" for doc, score in results]
    print(f"[Memorias recuperadas para {user_id}]: {len(memories)} resultados.")
    return memories

# 5. DEFINICIÓN DE LOS NODOS DEL GRAFO
def retrieve_node(state: AgentState):
    """Nodo: Recupera memorias relevantes basadas en el mensaje actual."""
    user_id = state["user_id"]
    query = state["input_message"]
    memories = retrieve_memories(user_id, query, k=2)
    return {"retrieved_memories": memories}

def generate_response_node(state: AgentState):
    """Nodo: Genera una respuesta, usando las memorias recuperadas."""
    history = "\n".join(state["conversation_history"][-5:])  # Ventana reciente
    context = "\n".join(state["retrieved_memories"])
    prompt = f"""
    Historial reciente de conversación:
    {history}

    Contexto relevante de memorias pasadas (puede estar vacío):
    {context}

    Mensaje actual del usuario: {state['input_message']}

    Responde de manera útil, considerando el contexto histórico si es relevante.
    Tu respuesta:
    """
    # Simulación de un modelo de lenguaje. En la práctica, usarías LLM.call()
    simulated_response = f"Entiendo. Basándome en nuestro historial y en lo que me dijiste antes sobre '{context[:30] if context else 'nada específico'}', te respondo esto."
    return {"agent_response": simulated_response}

def save_memory_node(state: AgentState):
    """Nodo: Guarda la interacción actual como un nuevo recuerdo."""
    user_id = state["user_id"]
    # Podemos guardar un resumen o el intercambio completo. Aquí guardamos un fragmento.
    memory_to_save = f"Usuario dijo: '{state['input_message']}'. Agente respondió: '{state['agent_response']}'."
    add_memory_to_store(user_id, memory_to_save)
    # También actualizamos el historial de conversación en el estado transitorio
    new_history = state["conversation_history"] + [f"User: {state['input_message']}", f"Agent: {state['agent_response']}"]
    return {"conversation_history": new_history}

# 6. CONSTRUCCIÓN DEL GRAFO
workflow = StateGraph(AgentState)
workflow.add_node("retrieve", retrieve_node)
workflow.add_node("generate", generate_response_node)
workflow.add_node("save", save_memory_node)

workflow.set_entry_point("retrieve")
workflow.add_edge("retrieve", "generate")
workflow.add_edge("generate", "save")
workflow.add_edge("save", END)

app = workflow.compile()

# 7. SIMULACIÓN DE EJECUCIÓN ENTRE DOS SESIONES
print("=== SESIÓN 1 (Usuario 'Alice') ===")
# Estado inicial para Alice. La memoria está vacía al principio.
initial_state_s1 = {
    "user_id": "alice_123",
    "input_message": "¡Me encanta el café de Etiopía!",
    "conversation_history": [],
    "retrieved_memories": [],
    "agent_response": ""
}
result_s1 = app.invoke(initial_state_s1)
print(f"Respuesta del agente: {result_s1['agent_response']}\n")

# Simulamos que el tiempo pasa, la app se reinicia, pero la BD persiste.
print("=== SESIÓN 2 (Usuario 'Alice', más tarde) ===")
# Nuevo estado. El grafo recuperará memorias de la sesión anterior.
initial_state_s2 = {
    "user_id": "alice_123",
    "input_message": "¿Qué café me recomendabas?",
    "conversation_history": [],  # Historial en RAM vacío, pero la memoria persistente tiene datos.
    "retrieved_memories": [],
    "agent_response": ""
}
result_s2 = app.invoke(initial_state_s2)
print(f"Respuesta del agente: {result_s2['agent_response']}")
print(f"Memorias recuperadas en Sesión 2: {result_s2['retrieved_memories']}")

Este código demuestra el ciclo completo. En la Sesión 1, el agente guarda la preferencia de Alice. En la Sesión 2, aunque el historial en RAM (conversation_history) está vacío, la consulta "¿Qué café me recomendabas?" activa una búsqueda en la base de datos vectorial filtrada por user_id="alice_123". El recuerdo sobre el café de Etiopía, al ser semánticamente relevante, es recuperado e inyectado en el estado (retrieved_memories), permitiendo al agente dar una respuesta contextualizada. La memoria ha persistido.

Tip Crítico: La elección del modelo de embeddings es fundamental. Modeles como all-MiniLM-L6-v2 son buenos para equilibrio rendimiento/calidad. Para producción, considera servicios gestionados (OpenAI, Cohere) o modelos más grandes locales (BGE, GTE). Asegúrate de que el modelo usado para guardar y el usado para recuperar sean el mismo, o los vectores no serán comparables.

Gestión Avanzada: Memoria con Resúmenes y Ventanas Híbridas

Para conversaciones muy largas, guardar cada intercambio puede saturar la base de datos y hacer la recuperación menos eficiente. Una estrategia avanzada es implementar una memoria híbrida. En este modelo, se guardan los intercambios recientes de forma detallada, pero periódicamente (por ejemplo, cada 10 mensajes) se usa el LLM para generar un resumen condensado de la conversación hasta ese punto. Este resumen se guarda como un recuerdo más en la base de datos vectorial. Para consultas futuras, el agente puede recuperar tanto fragmentos recientes específicos como estos resúmenes de alto nivel, obteniendo una comprensión tanto granular como general del historial.

# Extensión del ejemplo anterior: Función para crear y guardar resúmenes
from langchain_community.chat_models import ChatOpenAI  # Ejemplo con OpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def create_and_save_summary(user_id: str, recent_conversation: List[str]):
    """Toma un fragmento de conversación y crea un resumen para almacenar como memoria."""
    conversation_text = "\n".join(recent_conversation[-20:])  # Últimos 20 intercambios
    summary_prompt = f"""
    Resume la siguiente conversación entre un usuario y un asistente de IA.
    Extrae los puntos clave, preferencias expresadas por el usuario, hechos importantes y decisiones tomadas.
    El resumen debe ser conciso pero informativo, para ser usado como contexto en futuras interacciones.

    Conversación:
    {conversation_text}

    Resumen:
    """
    summary = llm.invoke(summary_prompt).content
    # Guardar el resumen en la base de datos vectorial
    doc = Document(
        page_content=f"Resumen de conversación: {summary}",
        metadata={
            "user_id": user_id,
            "timestamp": datetime.now().isoformat(),
            "type": "conversation_summary"
        }
    )
    vectorstore.add_documents([doc])
    vectorstore.persist()
    print(f"[Resumen guardado para {user_id}]")
    return summary

# Se podría integrar esta función en un nodo condicional del grafo que se active cada N ciclos.

Errores Comunes y Cómo Evitarlos

1. No Filtrar por Sesión o Usuario: El error más grave y común es almacenar todos los recuerdos de todos los usuarios en un solo espacio vectorial sin metadatos de filtro. Al recuperar, obtendrás una mezcla de contextos ajenos, contaminando la memoria del agente y violando la privacidad. Solución: Siempre incluye un campo como user_id, session_id o tenant_id en los metadatos del documento, y úsalo en el parámetro filter de la consulta de similitud, como se muestra en el código.

2. Ignorar la Persistencia Real en Disco: Algunos clientes de bases de datos vectoriales en memoria (como el modo efímero de Chroma) no guardan los datos automáticamente. Si olvidas llamar a .persist() o configurar el persist_directory, todos los recuerdos se perderán al terminar el proceso. Solución: Verifica la documentación del cliente. Configura explícitamente una ruta de persistencia y llama al método de persistencia después de operaciones de escritura críticas.

3. Embeddings Inconsistentes: Usar diferentes modelos de embeddings para guardar y para recuperar, o incluso diferentes versiones del mismo modelo, hará que los vectores no sean comparables. La búsqueda por similitud devolverá resultados sin sentido. Solución: Establece el modelo de embeddings como una configuración singleton en tu aplicación. Usa exactamente la misma instancia de embedding_function para todas las operaciones. Considera versionar los embeddings si cambias de modelo.

4. Sobrecargar el Contexto con Memorias Irrelevantes: Recuperar demasiados recuerdos (un valor de 'k' muy alto) o no tener una estrategia de limpieza/archivado puede hacer que el prompt final al LLM exceda su límite de tokens o que se diluya la información relevante. Solución: Empieza con un 'k' pequeño (3-5). Implementa una estrategia de "ventana deslizante" para recuerdos recientes y considera archivar o borrar recuerdos muy antiguos y de baja relevancia en procesos de mantenimiento.

5. Tratar la Memoria Vectorial como un Almacén de Hechos Exactos: Esperar que la base de datos vectorial funcione como una base de datos de hechos para recuperación exacta (e.g., "¿Cuál es el teléfono de María?") es un error. Es excelente para relevancia semántica ("María mencionó problemas con su pedido"), pero no para búsquedas de clave-valor. Solución: Para datos estructurados que necesiten búsqueda exacta (IDs, números, fechas), combina la base de datos vectorial con una base de datos tradicional (SQL, key-value). Esta arquitectura híbrida es muy potente.

Checklist de Dominio

Antes de considerar esta lección integrada y pasar a construir agentes más complejos, verifica que puedes realizar o explicar cada uno de estos puntos:

  • Puedo explicar la diferencia entre una base de datos vectorial y una base de datos relacional en el contexto de la memoria de un agente.
  • He configurado exitosamente una instancia persistente de una base de datos vectorial (como Chroma, Weaviate, o Pinecone) y me puedo conectar a ella desde código Python.
  • Sé cómo generar embeddings para un fragmento de texto y almacenarlo como un documento con metadatos relevantes (user_id, timestamp) en la base de datos vectorial.
  • Puedo realizar una consulta de similitud semántica filtrando por metadatos para recuperar los recuerdos más relevantes para un usuario y consulta específicos.
  • He integrado las operaciones de recuperación y almacenamiento de memoria en los nodos de un grafo LangGraph, manejando el flujo de estado adecuadamente.
  • Puedo describir al menos dos estrategias para manejar conversaciones largas (ventana de contexto + resúmenes) usando memoria persistente.
  • Sé identificar y evitar el error crítico de no filtrar las recuperaciones por identificador de usuario o sesión.
  • He probado que mi agente puede recordar información de una sesión anterior después de un reinicio simulado del proceso de la aplicación.

La integración de memoria persistente con bases de datos vectoriales es la piedra angular que transforma a un agente de LangGraph de un script inteligente pero efímero en un compañero digital con historia y contexto. Dominar esta configuración te permite construir aplicaciones que aprenden de cada interacción, ofrecen respuestas personalizadas y, en última instancia, ofrecen una experiencia de usuario profundamente más valiosa y coherente. Ahora tu agente no solo piensa, sino que recuerda.

De lección a portfolio

Convertí esta lección en una habilidad visible para entrevistas.

Guardá el curso, completá los ejercicios y conectá esta habilidad con una ruta de empleo, data, IA, programación o marketing.

Newsletter Cursalo

Recibí rutas y cursos nuevos

Sumate para recibir recursos orientados a empleo y portfolio.

  • Rutas de empleo
  • Cursos prácticos
  • Portfolio y entrevistas

Sin spam. También podés entrar con tu cuenta para guardar progreso. Iniciá sesión