Quién les habla

José Miguel Amaya — construyendo software con IA

José Miguel Amaya
  • CTO y socio fundador de Xprende Tech. Full Stack Engineer en Efilm Online.
  • Fundador de Piura AI, Tallanix, Python Piura y VICUX.
  • Especialista en Python, Django/FastAPI e IA (LangChain, LangGraph, RAG).
  • Construí este bot de Telegram con memoria persistente que verán hoy.
comunidad
empresa
Levantemos la mano

“¿Quién ha usado un chatbot que olvida todo lo que le acabas de decir?”

Sin memoria
“¿Cómo te llamas?”

Pregunta lo mismo una y otra vez. El usuario repite sus datos en cada mensaje.

Con poca memoria
“Pierde el hilo”

Recuerda el último mensaje, pero olvida el contexto de la conversación.

Con memoria persistente
“Te recuerda”

Retoma la charla y conserva datos entre sesiones. De eso trata esta charla.

Hoy veremos cómo darle a un bot de Telegram memoria de corto y largo plazo.

Hoja de ruta

Qué veremos hoy

  • Por qué un chatbot necesita memoria~4 min
  • El flujo: Telegram → FastAPI → grafo → MongoDB~4 min
  • LangChain: prompts y chains~6 min
  • LangGraph: estado, nodos y checkpointer~6 min
  • MongoDB Atlas: checkpoints y búsqueda vectorial~8 min
  • El grafo en acción y sus costos~6 min
  • Resultados, gotchas y cierre~5 min

Total ~40 min + preguntas. El código está en la rama develop del repo.

Introducción

Chatbots y la importancia de la memoria

Los chatbots deben dar respuestas coherentes y útiles. Sin memoria, un chatbot “olvida” lo que el usuario le dijo antes y lo obliga a repetir datos en cada mensaje.

1

Respuestas coherentes

Recordar el contexto y los detalles permite respuestas más relevantes.

2

La memoria es una función cognitiva

Ayuda al bot a adaptarse y a ofrecer respuestas más útiles.

3

Corto + largo plazo

Incorporaremos ambas en un chatbot de Telegram con LangChain, LangGraph y MongoDB.

Flujo de trabajo

De un mensaje en Telegram a una respuesta con memoria

🧑 Usuarioescribe en Telegram
▼
💬 Telegram Botwebhook FastAPI · POST /webhook/{token}
▼
📦 GraphBot (orquestador)memoria corto + largo plazo · LangGraph + LangChain
▼
📩 Respuesta al usuariogenerada con contexto y memoria

La memoria vive en MongoDB: tanto la de corto como la de largo plazo.

Telegram

¿Por qué Telegram?

Telegram es una app de mensajería que permite crear bots conversacionales fácilmente.

Usamos un webhook en FastAPI que recibe las actualizaciones de Telegram en tiempo real.

Usuario → Telegram → Webhook (POST /webhook/{token}) │ ▼ Bot (procesa) │ ▼ Respuesta al usuario por Telegram
Flujo del webhook de Telegram con FastAPI
LangChain

¿Qué es LangChain?

LangChain es una herramienta para construir aplicaciones que usan LLMs (grandes modelos de lenguaje). Hace fácil conectar el modelo con otras cosas:

Datos
🔗 Bases de datos

Conecta el modelo con fuentes externas.

Contexto
🧠 Memoria

Gestiona la memoria de conversación.

Conocimiento
📄 Documentos

Lee y usa documentos como contexto.

Acciones
🧰 Herramientas

APIs, buscadores y más.

LangChain

Piensa en LangChain como “cerebro + conectores”

Tú defines
Qué información darle al modelo

El contexto correcto en el momento correcto.

Tú defines
Qué hacer con su respuesta

Guardarla, mostrarla, encadenarla con otro paso.

📦 Ejemplos de lo que puedes crear: chatbots, agentes autónomos y asistentes que leen PDFs.

El modelo es el cerebro. LangChain son los cables.

LangChain

¿Qué es un Prompt?

Un prompt es el texto que le das al modelo para guiar su respuesta. Piensa en él como la personalidad + instrucciones del bot.

CHARACTER_PROMPT = """
Estás a punto de jugar a un juego de prueba de Turing como parte de un experimento.
Te emparejarán con un humano en una conversación de chat.
Tu tarea es interpretar al siguiente personaje, convenciendo al humano de que eres un humano real.

## Biografía de Lucía

## Personalidad de Lucía

## Antecedentes del usuario

Esto es lo que sabes sobre el usuario gracias a conversaciones anteriores:

{memory_context}

Proporciona respuestas de texto simple, sin indicadores de formato ni metacomentarios.
"""
Fíjate en {memory_context}: ahí inyectaremos los recuerdos recuperados de MongoDB.
LangChain

Prompt de memoria

Usamos otro prompt para extraer los datos relevantes del usuario y guardarlos en la memoria de largo plazo.

MEMORY_ANALYSIS_PROMPT = """Extraiga y formatee datos personales importantes del usuario a partir de su mensaje.
Céntrese en la información real, no en metacomentarios ni solicitudes.

Los datos importantes incluyen:
- Datos personales (nombre, edad, ubicación)
- Información profesional (trabajo, formación, habilidades)
- Preferencias (gustos, disgustos, favoritos)
- Circunstancias vitales (familia, relaciones)
- Experiencias o logros significativos
- Metas o aspiraciones personales

Examples:
Input: "Oye, ¿podrías recordar que me encanta Star Wars?"
Output: {{
    "is_important": true,
    "formatted_memory": "Le encanta Star Wars"
}}
"""
Salida estructurada: el modelo decide is_important y devuelve un recuerdo limpio y corto.
LangChain

¿Qué es una cadena (chain)?

Las chains son secuencias de operaciones que procesan y transforman entradas para producir salidas. Son como tuberías que conectan componentes.

Memory chain
Extrae lo importante

Analiza cada mensaje y decide qué vale la pena recordar.

Character chain
Responde en personaje

Genera la respuesta final usando el contexto de memoria.

LangChain

Chains en código

Memory chain · extracción estructurada
def get_memory_chain():
    model = ChatOpenAI(model="gpt-4o", temperature=0).with_structured_output(MemoryAnalysis)
    prompt = ChatPromptTemplate.from_template(MEMORY_ANALYSIS_PROMPT)
    return prompt | model
Character chain · responde en personaje
def get_character_chain():
    model = ChatOpenAI(model="gpt-4o", temperature=0.5)
    system_message = CHARACTER_PROMPT
    prompt = ChatPromptTemplate.from_messages(
        [
            ("system", system_message),
            MessagesPlaceholder(variable_name="messages"),
        ]
    )
    return prompt | model
Hoy cambiaríamos gpt-4o por el modelo que prefieras: LangChain es agnóstico del proveedor.
LangGraph

¿Qué es LangGraph?

LangGraph es una extensión de LangChain para crear agentes con lógica compleja. Usa grafos para definir flujos de decisión entre pasos.

Nodo
Llama al modelo

Genera texto o decide.

Nodo
Consulta memoria

Busca en MongoDB.

Nodo
Toma decisiones

Elige el siguiente paso.

Nodo
Guarda estado

Persiste el avance del grafo.

LangGraph

Un diagrama de flujo inteligente

El modelo elige el camino según el contexto.

Ventaja
Memoria de largo plazo

Integrada en el propio flujo del agente.

Ventaja
Fácil de depurar

Se puede visualizar como un grafo.

Ventaja
Múltiples habilidades

Ideal para bots con varias tareas.

Puedes dibujar tu agente antes de programarlo.

Grafo

Flujo definido por nodos y aristas

START
▼
memory_extraction_nodeaprende
▼
memory_injection_noderecuerda
▼
generate_responseresponde
▼
END
1 · Nodos
def create_workflow_graph():
    g = StateGraph(StateBot, context_schema=Context)
    g.add_node("memory_extraction_node", memory_extraction_node)
    g.add_node("memory_injection_node", memory_injection_node)
    g.add_node("generate_response", generate_response)
2 · Aristas y retorno
    g.add_edge(START, "memory_extraction_node")
    g.add_edge("memory_extraction_node", "memory_injection_node")
    g.add_edge("memory_injection_node", "generate_response")
    g.add_edge("generate_response", END)
    return g
Estado

El estado de la conversación

El estado hereda de MessagesState de LangGraph y almacena:

  • El historial de mensajes (heredado).
  • El contexto de memoria personalizado (memory_context).

El Context (schema) inyecta el user_id en cada nodo para separar la memoria por usuario.

from dataclasses import dataclass
from langgraph.graph import MessagesState


@dataclass
class Context:
    user_id: str


class StateBot(MessagesState):
    memory_context: str
Nodos

Los tres nodos del grafo

1 · Memory extraction
Aprende

Analiza el último mensaje. Si es importante, lo guarda en el store (que vectoriza solo).

2 · Memory injection
Recuerda

Toma el último mensaje, busca por similitud semántica y construye el contexto de memoria.

3 · Generate response
Responde

Usa el character chain, incorpora la memoria y mantiene la consistencia.

Aprender → recordar → responder. Simple y ordenado.

Memoria

Corto plazo vs. largo plazo

Corto plazoLargo plazo
AlcanceUn solo hiloEntre conversaciones
Qué guardaEstado del grafo (mensajes)Hechos y preferencias del usuario
MecanismoCheckpointerStore
VidaLa conversaciónPermanente
Accesothread_id en el confignamespace (user_id, "memories")
Para quéContinuidad, human-in-the-loopPreferencias, conocimiento compartido
Short term

Memoria a corto plazo

Es el contexto y los mensajes recientes de una sola conversación.

LangGraph la trata como parte del estado interno del agente, persistido con un checkpointer por cada hilo de conversación.

En desarrollo: InMemorySaver (antes MemorySaver). En producción: MongoDBSaver de langgraph.checkpoint.mongodb.
Memoria de corto plazo persistida por hilo
Short term

La ventana de contexto no es infinita

Con memoria de corto plazo activada, las conversaciones largas superan la ventana del LLM. La doc oficial recomienda gestionarla:

Recortar
trim_messages

Conservar solo los últimos N tokens antes de llamar al modelo.

Resumir
SummarizationNode

Resumir los mensajes antiguos y reemplazarlos por el resumen.

Borrar
RemoveMessage

Eliminar mensajes del estado de forma permanente.

Y del lado de la persistencia: prune(...) y TTL en el checkpointer evitan que los checkpoints crezcan sin control.
Long term

Memoria a largo plazo

Son conocimientos que se comparten entre conversaciones distintas: datos permanentes como el perfil del usuario o hechos guardados.

LangGraph provee la API de Store (put, get, search), organizada por namespace. Con index_config, el search es semántico.

from langgraph.store.mongodb import MongoDBStore, create_vector_index_config

store = MongoDBStore.from_conn_string(
    settings.MONGO_DB_URL, collection_name="memories",
    index_config=create_vector_index_config(embed=embeddings, dims=1536),
)
ns = (user_id, "memories")
store.put(ns, memory_id, {"data": "Le encanta Star Wars"})
store.search(ns, query="¿qué le gusta?", limit=5)
Memoria de largo plazo en un store externo Recuperación semántica de recuerdos
Long term

Búsqueda semántica

Buscar por significado, no por palabras exactas. En vez de comparar texto literal, convertimos frases en vectores (embeddings) y encontramos los que se parecen en sentido.

Búsqueda normal
❌ “auto” ≠ “carro”

Coincidencia literal de palabras.

Búsqueda semántica
✅ “auto” ≈ “carro”

Coincidencia por significado.

Por eso el usuario puede decir “mi nombre” o “cómo me llamo” y aun así lo encontramos.

Memoria

¿Cómo modela LangGraph la memoria?

Memoria corta: checkpointer que guarda el estado al ejecutar el grafo.

Memoria larga: Store de larga duración que el agente consulta cuando hace falta.

Implementaciones: InMemorySaver/MongoDBSaver para el hilo y MongoDBStore para los hechos.
Checkpointers de LangGraph Store de largo plazo de LangGraph
Memoria · teoría

No toda la memoria es igual

La doc de LangGraph clasifica la memoria de largo plazo según qué recuerda y cuándo se escribe.

Semántica
Hechos

“Al usuario le encanta Star Wars”. Es la que implementamos.

Episódica
Experiencias

Acciones pasadas del agente; suele ir a few-shot examples.

Procedimental
Reglas

El prompt/sistema, refinable con feedback.

Escritura · hot path
En línea

El agente decide recordar antes de responder. Inmediato, pero suma latencia. Es lo que usamos.

Escritura · background
Asíncrona

Un proceso aparte genera memorias: no afecta la respuesta, pero llega con retraso.

Nuestro diseño es semántica + hot path: hechos sobre el usuario, escritos al vuelo.

MongoDB

MongoDB, la base de datos

NoSQL
Documentos

Orientada a documentos, guarda información en formato JSON flexible (BSON).

Flexible
Sin esquemas fijos

No necesita relaciones tradicionales ni migraciones rígidas.

Escalable
Grandes volúmenes

Rápida para mucha data e ideal para memoria persistente.

Soporta búsqueda de texto (Search) y similitud vectorial con Atlas Vector Search.

MongoDB

Compilar el grafo una sola vez

El checkpointer y el store se crean al arrancar la app. El grafo se compila una vez con ambos y se reutiliza en cada mensaje.

1 · Setup (una vez, en el lifespan)
from langgraph.checkpoint.mongodb import MongoDBSaver

with MongoDBSaver.from_conn_string(
    settings.MONGO_DB_URL, db_name=settings.MONGO_DB_NAME,
) as checkpointer:
    graph = create_workflow_graph().compile(
        checkpointer=checkpointer,
        store=store,
    )
2 · Por mensaje
async def reply(self, chat_id, user_id, text):
    config = {"configurable": {"thread_id": str(chat_id)}}
    await self.graph.ainvoke(
        {"messages": [HumanMessage(content=text)]},
        config,
        context=Context(user_id=str(user_id)),
    )
    state = await self.graph.aget_state(config)
    return state.values["messages"][-1].content
MongoDB

¿Qué se guarda?

Al usar checkpointer=, LangGraph genera y administra sus propias colecciones. No escribimos el esquema a mano.

ColecciónQué guarda
checkpointsSnapshot del estado por hilo (mensajes y canales)
checkpoint_writesEscrituras pendientes de cada nodo
El hilo de la conversación y el estado del grafo se guardan automáticamente. Nosotros solo definimos el estado.
MongoDB

Atlas Vector Search

Es el motor de búsqueda vectorial de MongoDB Atlas: indexa embeddings y hace KNN por similitud con el stage $vectorSearch. No confundir con Atlas Search, que es búsqueda de texto (BM25).

Vectorial
🧠 $vectorSearch

KNN aproximado (HNSW) sobre el campo de embeddings.

Filtros
⚙️ Campos del índice

Filtra por metadatos declarados, como user_id.

Híbrida
🔠 Texto + vector

Combina BM25 y similitud con Reciprocal Rank Fusion.

MongoDB

Vector Search como memoria de largo plazo

1 · Configurar
MongoDBStore

Definimos colección e index_config (modelo de embeddings y dimensiones).

2 · Guardar
store.put

Guardamos el hecho; el store calcula y almacena el embedding.

3 · Recuperar
store.search

Buscamos por similitud con query= y limitamos con limit=.

La dimensión depende del modelo: 1536 con text-embedding-3-small, 3072 con large. Alternativas 2026: Voyage AI (p. ej. voyage-4-lite, 1024 dims) y Automated Embeddings de Atlas (server-side).
Store

Configurar MongoDBStore

El store oficial crea el índice vectorial al abrir la conexión (no hay setup() aparte). Definimos colección, index_config y, opcionalmente, TTL.

1 · Store con búsqueda semántica
from langgraph.store.mongodb import MongoDBStore, create_vector_index_config

store = MongoDBStore.from_conn_string(
    settings.MONGO_DB_URL,
    collection_name="memories",
    index_config=create_vector_index_config(embed=embeddings, dims=1536),
)
2 · TTL opcional: expira memorias viejas
ttl_config = {"default_ttl": 43200, "refresh_on_read": True}
Una sola instancia, creada al arrancar. Igual que el checkpointer, no se recrea por mensaje.
Long term · calidad

Mejorar el recall: híbrida + reranking

La similitud vectorial sola no siempre basta. Atlas y LangChain ofrecen dos mejoras.

Búsqueda híbrida
BM25 + vector

Combina coincidencia de texto y semántica con Reciprocal Rank Fusion.

Reranking
Voyage AI

Un reranker reordena los candidatos por relevancia real.

store = MongoDBStore(
    collection,
    index_config=create_vector_index_config(embed=embeddings, dims=1536),
    rerank_config=RerankConfig(model="rerank-2.5-lite", top_k=3),
)

El reranking nativo requiere Atlas 8.3+ y Native Reranking habilitado.

Extracción

Nodo de extracción de memoria

Guardamos el hecho en el store con aput. El store calcula el embedding solo; nosotros damos namespace, key y valor.

1 · Analizar el mensaje
async def memory_extraction_node(state, runtime: Runtime[Context]):
    response = await get_memory_chain().ainvoke(
        {"message": state["messages"][-1]}
    )
    if response.is_important:
2 · Guardar en el store
        ns = (runtime.context.user_id, "memories")
        try:
            await runtime.store.aput(
                ns, str(uuid.uuid4()),
                {"data": response.formatted_memory},
            )
        except Exception as e:
            print(f"Error almacenando memoria: {e}")
    return {}
En el repo se implementó un vector store custom equivalente; aquí mostramos el patrón oficial con MongoDBStore.
Inyección

Nodo de inyección de memoria

Buscamos por similitud semántica con asearch; el namespace por usuario garantiza que cada quien recupere solo sus recuerdos.

1 · Namespace y último mensaje
async def memory_injection_node(state, runtime: Runtime[Context]):
    ns = (runtime.context.user_id, "memories")
    last_message = state["messages"][-1].content
2 · Buscar y construir el contexto
    try:
        items = await runtime.store.asearch(
            ns, query=last_message, limit=5,
        )
        memory_context = "\n".join(i.value["data"] for i in items)
        return {"memory_context": memory_context}
    except Exception as e:
        print(f"Error recuperando memorias: {e}")
        return {"memory_context": ""}
Documents

La colección memories

El store guarda cada recuerdo como un documento JSON con namespace, key y value. Con index_config, agrega el embedding para la búsqueda semántica.

namespace + key + value (+ embedding) = un recuerdo recuperable.
{
  "namespace": ["68412097", "memories"],
  "key": "b1f2c3d4-...",
  "value": { "data": "Le encanta Star Wars" },
  "created_at": "2026-09-30T...",
  "updated_at": "2026-09-30T..."
}
Generación

Nodo de generación de respuesta

La memoria de largo plazo se inserta junto con los mensajes del estado (memoria de corto plazo) para entregar una respuesta coherente.

1 · Recuperar memoria y chain
async def generate_response(state: StateBot, config: RunnableConfig):
    memory_context = state.get('memory_context')
    chain = get_character_chain()
2 · Invocar con ambas memorias
    response = await chain.ainvoke(
        {
            "messages": state['messages'],
            "memory_context": memory_context,
        },
        config,
    )
    return {"messages": response}
Corto plazo (mensajes) + largo plazo (memory_context) = respuesta con contexto.
Lo que nadie te cuenta

El costo de recordar

Cada mensaje del usuario dispara trabajo extra además de la respuesta.

+1 llamada al LLM
Memory chain

Analiza si el mensaje merece guardarse. Es una llamada adicional por mensaje.

+1 embedding
Vectorizar

El store lo calcula al hacer put; es una llamada al modelo de embeddings.

+1 búsqueda vectorial
limit=5

Recuperar los recuerdos relevantes antes de responder.

Mitigaciones: extraer solo si is_important, cachear embeddings, acotar el limit y usar TTL/prune. La memoria no es gratis, pero se paga sola.
Resultados

El bot recuerda tu nombre

Le pregunté mi nombre sin habérselo dado en esa conversación; cuando se lo di, lo recordó perfectamente.

El bot no sabe el nombre al inicio El bot recuerda el nombre en otro mensaje
Resultados

Sobrevive al borrado de la memoria corta

Borré las tablas que guardan la memoria de corto plazo y dejé intactas las de largo plazo. El bot siguió recordando.

Tablas de corto plazo eliminadas El bot conserva la memoria de largo plazo
Lo que aprendí

Gotchas de la memoria persistente

Privacidad
PII en la DB

Estás guardando datos personales. Define qué se guarda, por cuánto y cómo se borra.

Duplicados
Sin dedup

El mismo hecho puede guardarse muchas veces. Normaliza o deduplica.

Aislamiento
user_id / namespace

Sin namespace por usuario, los recuerdos se mezclan entre conversaciones.

Checkpoints
Crecen sin parar

MongoDB soporta ttl y prune nativos: úsalos en lugar de limpiar a mano.

Embeddings
Cambiar = reconstruir

Otro modelo o dimensión invalida el índice vectorial existente.

Errores silenciosos
try/except

Devolver vacío ante un fallo evita romper, pero oculta el problema: loguea.

Para llevarse

Lo que aprendimos hoy

01

Dos memorias

Corto plazo con un checkpointer; largo plazo con un Store (MongoDBStore). No son lo mismo ni se guardan igual.

02

Guardar y buscar

La memoria de largo plazo es escribir embeddings y recuperarlos por similitud semántica.

03

Arquitectura > modelo

La estructura del grafo y del almacenamiento importa más que qué LLM uses.

Un bot sin memoria repite; un bot con memoria entiende.

Gracias

¿Preguntas?

Un bot sin memoria repite; un bot con memoria entiende.

Ing. José Miguel Amaya Camacho linkedin.com/in/miguelamaya99 · github.com/joseamaya