Cómo diseñé la memoria de un agente de IA operativo (los LLM no recuerdan nada)
Hay algo que casi nadie te dice cuando construyes un asistente de IA: el modelo no recuerda nada. Cada llamada a un LLM arranca de cero. La API es stateless: no existe "la conversación" del lado del modelo. Toda la "memoria" que un usuario percibe —que el asistente entienda "¿y ese cuándo vence?"— es una ilusión que el ingeniero construye por fuera del modelo, decidiendo qué guardar, cuánto tiempo, en qué formato se lo reenvías, y —la parte que casi todos ignoran— cuándo olvidarlo.
Este caso es sobre ese diseño.
En TrabaJIA —mi plataforma de gestión de flota y activos retornables— el asistente de IA se llama JIA. Los administradores le hablan por el bot de Telegram de su empresa, por nota de voz o por texto, y JIA responde con datos reales de su operación: saldos de canastillas, últimos movimientos, vehículos, conductores. Todo grounded, sin alucinar (esa es otra historia).
Pero JIA tenía un techo: respondía cada pregunta en frío. Y esta semana, usándola yo mismo en producción —dogfooding sobre la operación de un frigorífico real— choqué con ese techo tres veces seguidas.
Los tres problemas que reporté (usándola yo)
Yo soy mi propio primer usuario, y no me perdono nada. Estas fueron las tres fallas concretas:
1. "¿Tengo vencimiento de documentos?" → "No sé." El contexto que le inyectaba a JIA tenía canastillas, movimientos, vehículos y conductores… pero no los documentos de esos vehículos: SOAT, tecnomecánica, seguros. Para una flota, saber qué se vence esta semana es media operación. Y JIA estaba ciega.
2. Le pregunté por el código de un cliente → tampoco sabía. En el ERP del frigorífico los clientes se manejan por código, no por nombre. El cliente "412" es "DISTRIBUIDORA EL PUERTO" (los nombres, códigos y placas de este artículo están cambiados; las situaciones son reales). Pero mi contexto solo tenía nombres. Así que si el operador preguntaba por el 412, JIA no tenía idea de quién era.
3. Le hice una pregunta de seguimiento → perdió el hilo. Le pregunté algo, me respondió bien, y le repregunté sobre su propia respuesta ("¿y ese cuándo vence?"). JIA no tenía ni idea de qué era "ese". Cero memoria. Cada mensaje era el primero.
El diagnóstico: qué clase de memoria faltaba
Aquí está la diferencia entre parchar y diseñar. Antes de escribir una línea, clasifiqué las fallas, porque en un agente de IA "memoria" no es una sola cosa. Hay al menos tres clases, y cada una se construye con infraestructura distinta:
- Conocimiento semántico: lo que el agente sabe del dominio en todo momento (saldos, vehículos, clientes). Vive en el contexto que le inyectas en cada llamada. Las fallas 1 y 2 eran huecos aquí: le faltaban los vencimientos y el vocabulario del operador (los códigos).
- Memoria de trabajo: el hilo de la conversación en curso — lo que hace falta para resolver "ese", "el otro", "¿y cuándo?". La falla 3 era la ausencia total de esta clase.
- Memoria episódica de largo plazo: lo que pasó en conversaciones anteriores. JIA no la necesita — y esa decisión negativa es tan de diseño como las otras dos.
Con el diagnóstico claro, cada arreglo cae por su propio peso.
Cerrar los huecos semánticos: el contexto debe hablar el idioma del operador
Los códigos. La query que arma el contexto ahora trae el código externo del cliente y lo presenta entre corchetes:
SELECT c.codigo_externo AS codigo,
COALESCE(c.nombre, u.nombre) AS cliente,
ta.nombre_plural AS tipo,
SUM(s.cantidad)::int AS cant
FROM saldos s
...
LEFT JOIN clientes c ON c.id = u.id_cliente
GROUP BY c.codigo_externo, cliente, ta.nombre_plural
ORDER BY cant DESC LIMIT 25
En el prompt, JIA ve líneas como [412] DISTRIBUIDORA EL PUERTO: 850 canastillas, con una instrucción dura: puedes identificar a un cliente por nombre O por código indistintamente, y SIEMPRE que menciones un cliente incluye su código. Respuesta de producción: "DISTRIBUIDORA EL PUERTO (código 412) tiene 850 canastillas y COMERCIAL LA BAHÍA (código 310) tiene 377". Esto es resolución de entidades de manual: la IA no adivina el vocabulario de tu negocio — se lo tienes que dar. Ahora habla el idioma del operador, no el mío.
Los vencimientos. Agregué al contexto una vista tipo semáforo: placa · tipo de documento · fecha de vencimiento · días restantes · estado, ordenada por lo que vence primero. Ahora JIA responde "El SOAT de la ABC123 vence el 12 de febrero de 2027" y, cuando le preguntas en general, "no tienes ningún documento vencido". Le devolví la mitad de la operación que le faltaba.
La memoria de trabajo: una ventana deslizante con política de olvido
Aquí está el diseño que vale el artículo.
El bot mantiene, por cada chat, una ventana deslizante de los últimos 6 intercambios {pregunta, respuesta}, con un TTL de 15 minutos. En cada consulta nueva, manda esa ventana al backend junto con la pregunta.
El backend trata ese historial como lo que es: input no confiable que cruza una frontera de confianza. Viene del lado del usuario, así que lo sanea antes de acercarlo al modelo — máximo 6 turnos, solo strings, recortados a 400 caracteres cada uno. Eso acota dos riesgos a la vez: la superficie de inyección de prompt y el costo en tokens de cada llamada.
Y el detalle que más importa: el historial no se le pega al modelo como un bloque de texto. Se reconstruye como turnos reales de diálogo, con roles:
const mensajes = [];
for (const t of historial) {
mensajes.push({ role: 'user', content: t.q });
mensajes.push({ role: 'assistant', content: t.a });
}
mensajes.push({ role: 'user', content: pregunta });
No es un detalle cosmético. Los LLM están post-entrenados sobre diálogos con estructura de roles: un historial con turnos user/assistant le permite al modelo resolver la anáfora —"ese", "el otro", "¿y cuándo?"— como parte de una conversación. El mismo texto aplanado en un solo bloque es solo contexto suelto, y el modelo lo trata como ruido.
Prueba real, dos notas de voz seguidas: le pregunté por un vehículo, y después solté "¿y ese cuándo vence?". JIA respondió: "La tecnomecánica de ABC123 vence en 302 días." Resolvió "ese" solita, por el hilo.
Por qué NO hay base de datos (ni vectorial): cada clase de memoria tiene su herramienta
Esta es la parte contraintuitiva, la que casi todos harían al revés.
Lo "correcto de manual" sería persistir la conversación en Postgres —cada mensaje con su timestamp, indexado por chat— o, peor, montar una base vectorial "porque es lo que se usa para memoria de IA". No hice ninguna de las dos, a propósito. Porque cada una resuelve una clase de memoria distinta a la que este problema necesita:
- Una base vectorial sirve para recall semántico sobre un corpus grande: buscar el fragmento relevante entre miles. Mi ventana son 6 intercambios de hace minutos — no hay nada que "buscar".
- Persistencia en BD sirve para memoria episódica de largo plazo: auditoría, continuidad entre sesiones, "¿qué me preguntaron el mes pasado?". Una charla operativa se resuelve en minutos y no debe arrastrarse — no quiero que la pregunta de ayer contamine la de hoy.
- La memoria de trabajo — la clase que sí faltaba — pide exactamente lo contrario: acceso inmediato, alcance corto y olvido garantizado.
Bajo esa luz, el olvido no es una limitación: es una feature. El TTL de 15 minutos es la política de retención. Perder la ventana en un reinicio es aceptable porque no son datos de negocio — lo peor que pasa es que la conversación arranca limpia. Y 6 turnos recortados mantienen el presupuesto de tokens de cada llamada bajo control.
Un Map en memoria con un setInterval que barre los chats viejos implementa todo eso en 15 líneas. Usar una clase de infraestructura para otra clase de memoria no es robustez: es un error de categoría que se disfraza de buena práctica.
Bonus: no todo pasa por el modelo
Antes JIA solo escuchaba notas de voz. Ahora, cualquier texto libre de un admin también va a JIA con la misma memoria. Y un detalle de ingeniería de costos que me gusta: si un admin escribe "gracias", el bot responde "Es un gusto." con un simple regex, sin gastar una llamada de IA. Enrutar lo trivial por un camino barato antes de tocar el modelo es parte del diseño, no una trampa.
Un párrafo de seguridad, sin drama
JIA vive en el mismo VPS que el bot, así que le hablo por dentro (localhost), no por internet. Eso me deja cerrar el endpoint a la calle. El truco: nginx pone la cabecera X-Real-IP en todo lo que entra desde internet; una llamada directa a localhost no la trae. Así que rechazo cualquier petición que traiga X-Real-IP.
El gotcha que me costó un rato: no puedes usar x-forwarded-for para esto, porque Next.js la sintetiza para TODAS las peticiones, incluidas las internas. X-Real-IP sí distingue. Sumado a eso: rate limit por empresa (para frenar abuso de costo), un secreto compartido entre bot y backend, y errores genéricos que no filtran tablas ni columnas de Postgres. Y el aislamiento multi-tenant intacto: cada bot solo pregunta por su empresa.
El resultado
Hoy un administrador le habla o le escribe a su bot y JIA le responde con memoria de trabajo ("ese" ya significa algo), con el vocabulario de su ERP (los códigos), con vencimientos de SOAT y tecnomecánica, y hasta con el PDF real de una remisión — todo sobre sus datos, y solo los suyos. Pasó de contestar preguntas sueltas a sostener una conversación operativa.
Lo que aprendí
- Los LLM no tienen memoria: toda memoria es ingeniería de contexto. Diseñarla es decidir qué guardar, cuánto tiempo, en qué formato reenviarlo y cuándo olvidarlo. Eso pasa en tu aplicación, no en el modelo.
- Clasifica la memoria antes de construirla. Memoria de trabajo, episódica y semántica se implementan con infraestructura distinta.
- Dale al modelo el historial como turnos con roles, no como texto pegado. Está post-entrenado sobre diálogo; la estructura es lo que le permite resolver "ese".
- El olvido es una feature. Un TTL corto evita que la conversación de ayer contamine la de hoy. No todo recuerdo mejora al agente.
- El historial es input no confiable. Cruza una frontera de confianza: sanéalo (turnos, tipos, longitud) antes de acercarlo al modelo.
- El contexto debe hablar el idioma del usuario. Si su ERP maneja códigos, tu IA maneja códigos. Un
LEFT JOINcerró una brecha entera.
La pregunta experta no es "¿qué base de datos uso para la memoria de mi IA?". Es "¿qué clase de memoria necesita este problema — y cuándo debe olvidar?".
Preguntas frecuentes
¿Los LLM tienen memoria entre mensajes?
No. Las APIs de los modelos de lenguaje son stateless: cada llamada arranca de cero. Toda "memoria" de un chatbot la implementa la aplicación — guardando los intercambios anteriores y reenviándolos al modelo en cada llamada como parte del contexto. Si un asistente "recuerda", es porque alguien diseñó ese mecanismo por fuera del modelo.
¿Cómo le doy memoria conversacional a un bot de IA?
Mantén una ventana deslizante de los últimos N intercambios (pregunta/respuesta) por cada chat, con un tiempo de expiración, y mándasela al modelo en cada llamada como turnos de diálogo alternados (rol user / rol assistant), no como un bloque de texto plano. La estructura de roles es lo que le permite al modelo resolver referencias como "ese" o "el otro".
¿La memoria de un chatbot debe guardarse en una base de datos o en una base vectorial?
Depende de la clase de memoria. Para memoria de trabajo (el hilo de una conversación que dura minutos), RAM con TTL es suficiente y perderla en un reinicio es aceptable. Una base de datos sirve para memoria episódica de largo plazo (auditoría, continuidad entre sesiones); una vectorial, para recall semántico sobre un corpus grande. Usar la infraestructura de una clase para otra es sobreingeniería.
¿Cuántos turnos de historial conviene enviarle al modelo?
Los suficientes para sostener el hilo sin inflar el costo: en una conversación operativa, unos 6 intercambios recortados (por ejemplo a 400 caracteres cada uno) bastan. Más turnos no siempre ayudan — suben el costo en tokens y agregan ruido que degrada las respuestas. Y sanea siempre ese historial antes de enviarlo: es input que viene del lado del usuario.
Soy Miller Millán, fundador de TrabaJIA, una plataforma multi-empresa de gestión de flotas, activos retornables y logística. Construyo software real para operaciones reales —con IA aplicada donde de verdad suma. ¿Estás resolviendo algo parecido? Escríbeme.