Cómo Construir Agentes de IA que Realmente Recuerdan: Arquitectura de Memoria para Apps LLM en Producción
Tu agente de IA acaba de olvidar todo.
El usuario lleva veinte minutos explicando la arquitectura de su codebase, sus restricciones de deployment, el estilo de código del equipo. Entonces hace una pregunta de seguimiento — y el agente responde como si nunca se hubieran conocido. La ventana de contexto se desbordó. Todo antes del mensaje #47 se esfumó. El usuario arranca de cero, frustrado.
Si construiste algo con LLMs más allá de un demo de juguete, chocaste con este muro. Las ventanas de contexto no son memoria. Una ventana de 128K tokens parece enorme hasta que un solo escaneo de codebase la llena en segundos. Una ventana de 1M tokens suena infinita hasta que calculás el costo de API de llenarla en cada request. Y aun con las ventanas más grandes, no hay persistencia — cerrás la sesión y tu agente tiene amnesia.
Este es el desafío fundamental de las aplicaciones de IA en producción en 2026: ¿Cómo construís agentes que realmente recuerdan?
No "recordar los próximos 5 mensajes." Recordar entre sesiones. Recordar las preferencias del usuario de hace tres semanas. Recordar que la migración de base de datos falló el martes pasado y el workaround sigue activo. Recordar como lo haría un colega humano.
Esta guía cubre los patrones de arquitectura de memoria que los equipos de producción usan de verdad — desde ventanas deslizantes simples hasta sistemas de memoria jerárquica y almacenes de conocimiento basados en grafos. Construiremos implementaciones reales, compararemos los frameworks principales (Mem0, LangChain Memory, Letta), y te mostramos dónde falla cada patrón.
Por qué las ventanas de contexto no son memoria
Antes de ir a las soluciones, seamos precisos con el problema.
La ilusión de la ventana de contexto
Todo LLM tiene una ventana de contexto — el máximo de tokens que puede procesar en un solo request. En 2026, estas crecieron significativamente:
| Modelo | Ventana de Contexto | Costo Aprox. (Input) |
|---|---|---|
| GPT-4.1 | 1M tokens | ~$2.00/M tokens |
| Claude Opus 4 | 200K tokens | ~$5.00/M tokens |
| Gemini 2.5 Pro | 1M tokens | ~$1.25/M tokens |
| Llama 4 Scout | 10M tokens | Self-hosted |
"Simplemente usá una ventana más grande" parece la solución obvia. No lo es, y acá van las razones:
1. El costo escala linealmente (o peor). Meter 500K tokens en cada llamada a la API cuando solo necesitás 2K de contexto relevante es quemar plata. A 1. Si tu agente maneja 1,000 conversaciones por día, son $1,000 diarios — y el 99% es contexto irrelevante.
2. El rendimiento baja con el ruido. Las investigaciones muestran consistentemente que los LLMs rinden peor con grandes cantidades de contexto irrelevante. El problema de "buscar la aguja en el pajar" es real: los modelos luchan para encontrar y usar información específica enterrada en ventanas masivas. Más contexto ≠ mejores respuestas.
3. La latencia crece con el tamaño del contexto. El time-to-first-token escala con la longitud del input. Un input de 500K tokens tarda notablemente más que uno de 5K, destruyendo la sensación de tiempo real en aplicaciones de chat.
4. No hay persistencia entre sesiones. Las ventanas de contexto son efímeras. Cerrás la pestaña y todo desaparece. No hay mecanismo para llevar información de una sesión a otra sin almacenamiento externo.
5. No hay olvido selectivo. Los humanos no recuerdan todo — recuerdan lo importante. Una ventana de contexto no tiene concepto de importancia; es un buffer FIFO que descarta los tokens más viejos sin importar si contienen información crítica o charla trivial.
Cómo se ve la memoria real
La memoria humana no es un buffer único. Es un sistema por capas:
- Memoria de trabajo (contexto inmediato): Lo que estás pensando ahora mismo. Poca capacidad, acceso rápido.
- Memoria a corto plazo (eventos recientes): Lo que pasó en los últimos minutos/horas. Capacidad media, acceso medio.
- Memoria a largo plazo (conocimiento persistente): Hechos, habilidades y experiencias acumuladas. Capacidad masiva, acceso más lento.
La memoria efectiva de un agente de IA replica esta arquitectura. Construyámosla.
Patrón 1: Ventana deslizante con truncado inteligente
El patrón de memoria más simple — y muchas veces suficiente para chatbots básicos.
Cómo funciona
Mantené los N mensajes más recientes en la ventana de contexto. Cuando la conversación excede el límite, descartá los mensajes más viejos.
interface Message { role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; tokenCount: number; } class SlidingWindowMemory { private messages: Message[] = []; private maxTokens: number; private systemPrompt: Message; constructor(maxTokens: number, systemPrompt: string) { this.maxTokens = maxTokens; this.systemPrompt = { role: 'system', content: systemPrompt, timestamp: Date.now(), tokenCount: this.estimateTokens(systemPrompt), }; } addMessage(role: 'user' | 'assistant', content: string): void { this.messages.push({ role, content, timestamp: Date.now(), tokenCount: this.estimateTokens(content), }); this.trim(); } getContext(): Message[] { return [this.systemPrompt, ...this.messages]; } private trim(): void { let totalTokens = this.systemPrompt.tokenCount + this.messages.reduce((sum, m) => sum + m.tokenCount, 0); while (totalTokens > this.maxTokens && this.messages.length > 2) { const removed = this.messages.shift()!; totalTokens -= removed.tokenCount; } } private estimateTokens(text: string): number { return Math.ceil(text.length / 4); } }
Cuándo usarlo
- Chatbots Q&A simples donde el historial no importa mucho
- Bots de soporte al cliente con conversaciones cortas y enfocadas
- Prototipos y MVPs
Dónde falla
- El contexto temprano importante se pierde. Si el usuario explicó sus requerimientos en el primer mensaje, eso es lo primero que desaparece.
- Sin persistencia entre sesiones. Cada sesión nueva arranca en blanco.
- Sin criterio de qué conservar. Una tangente confusa tiene la misma prioridad que especificaciones críticas.
Patrón 2: Resumen de conversación
El primer paso hacia memoria inteligente — comprimí el contexto viejo en vez de descartarlo.
Cómo funciona
Cuando la conversación crece demasiado, resumí la parte más vieja y reemplazala por el resumen. El agente trabaja con: [System Prompt] + [Resumen de mensajes viejos] + [Mensajes recientes].
class SummarizingMemory { private messages: Message[] = []; private summary: string = ''; private maxTokens: number; private recentWindowSize: number; private llm: LLMClient; constructor(maxTokens: number, recentWindowSize: number, llm: LLMClient) { this.maxTokens = maxTokens; this.recentWindowSize = recentWindowSize; this.llm = llm; } async addMessage(role: 'user' | 'assistant', content: string): Promise<void> { this.messages.push({ role, content, timestamp: Date.now(), tokenCount: this.estimateTokens(content), }); if (this.shouldSummarize()) { await this.compactHistory(); } } async getContext(): Promise<Message[]> { const context: Message[] = []; if (this.summary) { context.push({ role: 'system', content: `Resumen de conversación previa:\n${this.summary}`, timestamp: 0, tokenCount: this.estimateTokens(this.summary), }); } context.push(...this.messages.slice(-this.recentWindowSize)); return context; } private shouldSummarize(): boolean { const totalTokens = this.messages.reduce((sum, m) => sum + m.tokenCount, 0); return totalTokens > this.maxTokens * 0.8; } private async compactHistory(): Promise<void> { const messagesToSummarize = this.messages.slice( 0, this.messages.length - this.recentWindowSize ); if (messagesToSummarize.length === 0) return; const conversationText = messagesToSummarize .map(m => `${m.role}: ${m.content}`) .join('\n'); const existingSummary = this.summary ? `Resumen existente:\n${this.summary}\n\n` : ''; this.summary = await this.llm.complete({ prompt: `${existingSummary}Nueva conversación a incorporar:\n${conversationText}\n\nCreá un resumen completo que preserve:\n1. Decisiones clave y conclusiones\n2. Preferencias y requerimientos del usuario\n3. Especificaciones técnicas mencionadas\n4. Tareas pendientes\n5. Contexto importante para referencia futura\n\nSé conciso sin perder detalles críticos.`, maxTokens: 500, }); this.messages = this.messages.slice(-this.recentWindowSize); } private estimateTokens(text: string): number { return Math.ceil(text.length / 4); } }
El truco del resumen progresivo
En vez de un resumen plano, usá resúmenes jerárquicos:
class HierarchicalSummarizingMemory { private detailedSummary: string = ''; // Últimos ~30 mensajes private broadSummary: string = ''; // Todo lo anterior private recentMessages: Message[] = []; // Últimos ~10 mensajes async compactHistory(): Promise<void> { // Paso 1: Fusionar el resumen detallado actual al resumen amplio if (this.detailedSummary) { this.broadSummary = await this.llm.complete({ prompt: `Resumen de alto nivel existente:\n${this.broadSummary}\n\nResumen detallado a incorporar:\n${this.detailedSummary}\n\nCreá un resumen de alto nivel preservando solo los hechos, decisiones y preferencias más importantes. Máximo 200 palabras.`, maxTokens: 300, }); } // Paso 2: Resumir el overflow reciente al resumen detallado const overflow = this.recentMessages.slice(0, -10); this.detailedSummary = await this.llm.complete({ prompt: `Segmento de conversación:\n${overflow.map(m => `${m.role}: ${m.content}`).join('\n')}\n\nCreá un resumen detallado preservando detalles técnicos específicos, código mencionado y requerimientos exactos. Máximo 500 palabras.`, maxTokens: 600, }); this.recentMessages = this.recentMessages.slice(-10); } async getContext(): Promise<string> { let ctx = ''; if (this.broadSummary) { ctx += `## Contexto General\n${this.broadSummary}\n\n`; } if (this.detailedSummary) { ctx += `## Contexto Reciente (Detallado)\n${this.detailedSummary}\n\n`; } ctx += `## Conversación Actual\n`; ctx += this.recentMessages.map(m => `${m.role}: ${m.content}`).join('\n'); return ctx; } }
Cuándo usarlo
- Aplicaciones de chat de larga duración
- Soporte al cliente con conversaciones complejas y multi-turno
- Asistentes de código que necesitan contexto del proyecto
Dónde falla
- El resumen pierde detalle. El resumen dice "el usuario quiere una REST API" pero se olvida de que pidió respuestas HATEOAS-compliant con headers ETag.
- Errores de resumen compuestos. Resumir un resumen de un resumen introduce pérdida de información progresiva. Después de 5 ciclos de compactación, el matiz original desapareció.
- Costo del resumen. Cada compactación requiere una llamada al LLM, sumando latencia y costo.
Patrón 3: Extracción de entidades y hechos
En vez de resumir conversaciones enteras, extraé y almacená hechos estructurados.
Cómo funciona
Después de cada interacción, extraé entidades y hechos clave a un almacén persistente. Antes de cada respuesta, recuperá los hechos relevantes según la consulta actual.
interface Fact { id: string; subject: string; predicate: string; object: string; confidence: number; source: string; timestamp: number; supersedes?: string; } class EntityMemory { private facts: Map<string, Fact[]> = new Map(); private llm: LLMClient; private vectorStore: VectorStore; async extractFacts(messages: Message[]): Promise<Fact[]> { const conversation = messages .map(m => `${m.role}: ${m.content}`) .join('\n'); const response = await this.llm.complete({ prompt: `Extraé hechos clave de esta conversación como datos estructurados. Conversación: ${conversation} Devolvé como JSON array: [{ "subject": "nombre entidad", "predicate": "relación o atributo", "object": "valor o entidad relacionada", "confidence": 0.0-1.0 }] Enfocate en: - Preferencias y requerimientos del usuario - Decisiones técnicas tomadas - Especificaciones del proyecto - Deadlines y restricciones - Personas y roles mencionados`, responseFormat: 'json', }); return JSON.parse(response).map((f: any) => ({ ...f, id: crypto.randomUUID(), source: 'current_session', timestamp: Date.now(), })); } async storeFacts(facts: Fact[]): Promise<void> { for (const fact of facts) { const key = `${fact.subject}::${fact.predicate}`; const existing = this.facts.get(key) || []; const contradicting = existing.find( e => e.object !== fact.object && e.confidence < fact.confidence ); if (contradicting) { fact.supersedes = contradicting.id; } if (!this.facts.has(key)) { this.facts.set(key, []); } this.facts.get(key)!.push(fact); await this.vectorStore.upsert({ id: fact.id, text: `${fact.subject} ${fact.predicate} ${fact.object}`, metadata: fact, }); } } async recallRelevant(query: string, limit: number = 20): Promise<Fact[]> { const results = await this.vectorStore.search(query, limit); return results .map(r => r.metadata as Fact) .filter(f => !f.supersedes) .sort((a, b) => b.confidence - a.confidence); } async buildContext(query: string, recentMessages: Message[]): Promise<string> { const relevantFacts = await this.recallRelevant(query); let context = ''; if (relevantFacts.length > 0) { context += '## Hechos Conocidos del Usuario/Proyecto\n'; for (const fact of relevantFacts) { context += `- ${fact.subject} ${fact.predicate}: ${fact.object}\n`; } context += '\n'; } context += '## Conversación Actual\n'; context += recentMessages.map(m => `${m.role}: ${m.content}`).join('\n'); return context; } }
Cuándo usarlo
- Asistentes de IA personales que aprenden del usuario con el tiempo
- Bots de gestión de proyectos que rastrean decisiones a lo largo de muchas reuniones
- Cualquier aplicación donde los hechos concretos importan más que el flujo conversacional
Dónde falla
- La extracción no es perfecta. Los LLMs pierden matices y a veces alucinan hechos que no fueron dichos.
- Las contradicciones son difíciles. "El deadline es el viernes" seguido de "en realidad, pasémoslo al lunes" requiere que el sistema detecte y resuelva conflictos.
- Los hechos envejecen. Sin expiración explícita, los hechos desactualizados contaminan el contexto. El usuario cambió su framework preferido hace dos meses, pero el dato viejo sigue ahí.
Patrón 4: Arquitectura de memoria jerárquica
Esto es lo que usan los sistemas de producción reales. Combina múltiples patrones en un sistema por capas que replica la memoria humana.
Modelo de tres niveles
┌─────────────────────────────────────────────┐
│ Nivel 1: Memoria de Trabajo │
│ (Ventana de contexto actual, ~10 msgs) │
│ Acceso: Instantáneo | Capacidad: Baja │
├─────────────────────────────────────────────┤
│ Nivel 2: Memoria a Corto Plazo │
│ (Resúmenes de sesión, hechos recientes) │
│ Acceso: Retrieval rápido | Capacidad: Media │
├─────────────────────────────────────────────┤
│ Nivel 3: Memoria a Largo Plazo │
│ (Grafo de conocimiento, perfil, historial) │
│ Acceso: Búsqueda semántica | Capacidad: Alta│
└─────────────────────────────────────────────┘
Implementación
class HierarchicalMemory { private workingMemory: Message[] = []; private shortTermMemory: ShortTermStore; private longTermMemory: LongTermStore; private llm: LLMClient; constructor(config: MemoryConfig) { this.shortTermMemory = new ShortTermStore(config.shortTermTTL); this.longTermMemory = new LongTermStore(config.vectorStore, config.graphDB); this.llm = config.llm; } async processMessage(message: Message): Promise<void> { // 1. Agregar a memoria de trabajo this.workingMemory.push(message); // 2. Extraer hechos para almacenamiento a corto plazo if (this.workingMemory.length % 5 === 0) { const recentFacts = await this.extractFacts( this.workingMemory.slice(-5) ); await this.shortTermMemory.store(recentFacts); } // 3. Promover hechos importantes a memoria a largo plazo if (this.workingMemory.length % 20 === 0) { await this.consolidate(); } // 4. Recortar memoria de trabajo si es necesario if (this.getWorkingMemoryTokens() > 8000) { await this.compactWorkingMemory(); } } async buildContext(query: string): Promise<ContextBundle> { // Recuperar de los tres niveles const [shortTermResults, longTermResults] = await Promise.all([ this.shortTermMemory.search(query, 10), this.longTermMemory.search(query, 15), ]); // Deduplicar y rankear por relevancia const allFacts = this.deduplicateAndRank([ ...shortTermResults, ...longTermResults, ]); return { systemContext: this.buildSystemContext(allFacts), workingMemory: this.workingMemory.slice(-10), relevantFacts: allFacts.slice(0, 20), tokenBudget: { system: 2000, facts: 3000, working: 8000, response: 4000, }, }; } private async consolidate(): Promise<void> { const shortTermFacts = await this.shortTermMemory.getAll(); // Usar LLM para identificar qué hechos vale la pena guardar a largo plazo const assessment = await this.llm.complete({ prompt: `Revisá estos hechos y determiná cuáles deberían almacenarse a largo plazo. Hechos: ${shortTermFacts.map(f => `- ${f.subject} ${f.predicate}: ${f.object} (confianza: ${f.confidence})`).join('\n')} Para cada hecho, respondé con: - KEEP: Importante para interacciones futuras (preferencias, decisiones clave, specs del proyecto) - DISCARD: Temporal o conversacional (saludos, confirmaciones, estados transitorios) - MERGE: Se puede combinar con otro hecho Devolvé como JSON array con objetos {id, action, mergeWith?}.`, responseFormat: 'json', }); const actions = JSON.parse(assessment); for (const action of actions) { if (action.action === 'KEEP') { const fact = shortTermFacts.find(f => f.id === action.id); if (fact) { await this.longTermMemory.store(fact); } } } await this.shortTermMemory.prunePromoted( actions.filter((a: any) => a.action === 'KEEP').map((a: any) => a.id) ); } private async compactWorkingMemory(): Promise<void> { const overflow = this.workingMemory.slice(0, -10); const summary = await this.llm.complete({ prompt: `Resumí este segmento de conversación preservando detalles técnicos:\n${overflow.map(m => `${m.role}: ${m.content}`).join('\n')}`, maxTokens: 300, }); const facts = await this.extractFacts(overflow); await this.shortTermMemory.store(facts); this.workingMemory = [ { role: 'system', content: `[Resumen de conversación previa: ${summary}]`, timestamp: Date.now(), tokenCount: this.estimateTokens(summary), }, ...this.workingMemory.slice(-10), ]; } }
Cuándo usarlo
- Asistentes de IA en producción con interacciones multi-sesión
- Copilots empresariales que necesitan recordar contexto del proyecto por semanas
- Cualquier aplicación donde la personalización a largo plazo es crítica
Patrón 5: Memoria basada en grafos (GraphRAG)
La vanguardia de la memoria de agentes. En vez de almacenar hechos como texto plano, representá el conocimiento como un grafo de relaciones.
Por qué los grafos le ganan a los vectores
La búsqueda por similitud vectorial (la columna vertebral del RAG tradicional) tiene una limitación fundamental: encuentra cosas que suenan parecido pero pierde cosas que están estructuralmente relacionadas.
Ejemplo: "Alice gestiona el equipo de pagos" y "El equipo de pagos es dueño del microservicio de checkout" no son semánticamente similares. Pero en un grafo podés recorrer: Alice → gestiona → equipo de pagos → es dueño → checkout. Cuando alguien pregunta "¿Con quién hablo por bugs del checkout?", un grafo puede responder "Alice", mientras que un vector store no.
class GraphMemory { private graph: GraphDatabase; // Neo4j u otra async addKnowledge( subject: string, predicate: string, object: string, metadata: Record<string, any> ): Promise<void> { await this.graph.query(` MERGE (s:Entity {name: $subject}) MERGE (o:Entity {name: $object}) MERGE (s)-[r:${predicate.toUpperCase().replace(/\s/g, '_')}]->(o) SET r += $metadata, r.updatedAt = timestamp() `, { subject, object, metadata }); } async query(question: string): Promise<GraphResult[]> { // Paso 1: Extraer entidades de la pregunta const entities = await this.extractEntities(question); // Paso 2: Encontrar subgrafo relevante alrededor de esas entidades const subgraph = await this.graph.query(` MATCH (e:Entity)-[r*1..3]-(connected:Entity) WHERE e.name IN $entities RETURN e, r, connected LIMIT 50 `, { entities }); // Paso 3: Formatear subgrafo como contexto return this.formatSubgraph(subgraph); } async traverseForContext( startEntity: string, maxDepth: number = 3 ): Promise<string> { const result = await this.graph.query(` MATCH path = (start:Entity {name: $startEntity})-[*1..${maxDepth}]-(end:Entity) RETURN path ORDER BY length(path) LIMIT 30 `, { startEntity }); return result.paths .map(p => this.pathToSentence(p)) .join('\n'); } private pathToSentence(path: GraphPath): string { return path.segments .map(s => `${s.start.name} ${s.relationship.type.toLowerCase().replace(/_/g, ' ')} ${s.end.name}`) .join(', que '); } }
Enfoque híbrido: Vectores + Grafos
Los sistemas de producción más efectivos combinan ambos:
class HybridMemory { private vectorStore: VectorStore; // Para similitud semántica private graphStore: GraphMemory; // Para relaciones estructurales private llm: LLMClient; async recall(query: string): Promise<MemoryResult> { const [vectorResults, graphResults] = await Promise.all([ this.vectorStore.search(query, 10), this.graphStore.query(query), ]); // Fusionar y deduplicar const merged = this.mergeResults(vectorResults, graphResults); // Re-rankear con LLM const ranked = await this.llm.complete({ prompt: `Dada la consulta: "${query}" Calificá la relevancia de cada item de memoria (0-10): ${merged.map((m, i) => `${i}: ${m.text}`).join('\n')} Devolvé como JSON: [{index, score, reason}]`, responseFormat: 'json', }); return { memories: this.applyRanking(merged, JSON.parse(ranked)), sources: { vector: vectorResults.length, graph: graphResults.length }, }; } }
Comparación de frameworks: Mem0 vs LangChain Memory vs Letta
Comparemos los tres frameworks de memoria más populares para aplicaciones LLM.
Mem0
Mem0 provee una capa de memoria gestionada para aplicaciones de IA con una arquitectura multi-store (KV store + vector store + capa de grafos).
import { MemoryClient } from 'mem0ai'; const memory = new MemoryClient({ apiKey: process.env.MEM0_API_KEY }); // Agregar memorias de la conversación await memory.add( "I prefer Python over JavaScript for backend work", { user_id: "alice", metadata: { category: "preferences" } } ); // Buscar memorias const results = await memory.search( "What programming language does Alice prefer?", { user_id: "alice" } ); // Devuelve: [{memory: "Prefers Python over JavaScript for backend", score: 0.95}] // Obtener todas las memorias de un usuario const allMemories = await memory.getAll({ user_id: "alice" });
Fortalezas:
- API ultra simple — add/search/get en tres líneas
- Infraestructura gestionada (no necesitás configurar vector DB)
- Deduplicación automática y resolución de conflictos
- Funciona entre sesiones por defecto
- Opción self-hosted disponible (mem0 OSS)
Debilidades:
- Control limitado sobre la representación de memoria
- Algoritmo de ranking opaco
- Dependencia del cloud para la versión gestionada
- La capa de grafos es más nueva y menos probada que el vector store
LangChain Memory
LangChain provee múltiples implementaciones de memoria out-of-the-box:
import { BufferWindowMemory } from 'langchain/memory'; import { ConversationSummaryMemory } from 'langchain/memory'; import { VectorStoreRetrieverMemory } from 'langchain/memory'; import { CombinedMemory } from 'langchain/memory'; // Opción 1: Buffer simple const bufferMemory = new BufferWindowMemory({ k: 10 }); // Opción 2: Resumen const summaryMemory = new ConversationSummaryMemory({ llm: chatModel, returnMessages: true, }); // Opción 3: Retrieval basado en vectores const vectorMemory = new VectorStoreRetrieverMemory({ vectorStoreRetriever: vectorStore.asRetriever(5), memoryKey: 'relevant_history', }); // Opción 4: Combinar múltiples tipos de memoria const combinedMemory = new CombinedMemory({ memories: [bufferMemory, summaryMemory, vectorMemory], });
Fortalezas:
- Flexibilidad máxima — combiná y mezclá tipos de memoria
- Integración profunda con el ecosistema LangChain (agentes, cadenas, herramientas)
- Backends comunitarios (Redis, PostgreSQL, MongoDB)
- Open-source y self-hosted
- Buena documentación con muchos ejemplos
Debilidades:
- Requiere más setup y decisiones de infraestructura
- Puede ser over-engineering para casos simples
- Los tipos de memoria no siempre componen limpiamente
- Depende del framework LangChain más amplio
Letta (ex MemGPT)
Letta toma un enfoque fundamentalmente distinto — trata la gestión de memoria como un problema de sistema operativo.
import { Letta } from 'letta'; const client = new Letta({ apiKey: process.env.LETTA_API_KEY }); // Crear un agente con gestión de memoria tipo OS const agent = await client.createAgent({ name: 'project-assistant', memory: { coreMemory: { // Siempre en contexto — como tu system prompt persona: 'You are a senior software engineer...', human: '', // Se llena automáticamente de las conversaciones }, archivalMemory: true, // Almacenamiento vectorial a largo plazo recallMemory: true, // Búsqueda en historial de conversaciones }, model: 'gpt-4.1', tools: ['archival_memory_insert', 'archival_memory_search', 'core_memory_replace', 'core_memory_append'], }); // El agente gestiona su propia memoria vía tool calls const response = await agent.sendMessage( "I'm working on a Next.js project with PostgreSQL and Drizzle ORM" ); // El agente internamente llama: // core_memory_append(section="human", content="Works with Next.js, PostgreSQL, Drizzle ORM") // archival_memory_insert(content="User's current project stack: Next.js + PostgreSQL + Drizzle ORM")
Fortalezas:
- Memoria auto-gestionada — el agente decide qué recordar
- Arquitectura inspirada en OS (core/archival/recall)
- Persistente por defecto — la memoria sobrevive entre sesiones
- El agente puede razonar explícitamente sobre qué guardar y recuperar
- Opciones cloud y self-hosted
Debilidades:
- Requiere llamadas LLM extra para gestión de memoria (overhead de costo/latencia)
- Arquitectura opinionada que puede no encajar en todos los casos
- Ecosistema más joven comparado con LangChain
- Las actualizaciones de core memory pueden ser impredecibles
Matriz de decisión
| Criterio | Mem0 | LangChain Memory | Letta |
|---|---|---|---|
| Complejidad de setup | ⭐ Baja | ⭐⭐⭐ Alta | ⭐⭐ Media |
| Flexibilidad | ⭐⭐ Media | ⭐⭐⭐ Alta | ⭐⭐ Media |
| Memoria entre sesiones | ✅ Built-in | ⚙️ Requiere config | ✅ Built-in |
| Auto-gestionada | ❌ | ❌ | ✅ |
| Self-hosted | ✅ | ✅ | ✅ |
| Production readiness | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Ideal para | Integración rápida | Arquitecturas custom | Agentes autónomos |
Patrones y anti-patrones de producción
Patrón: Prompt engineering consciente de memoria
La optimización más impactante muchas veces no es el sistema de memoria sino cómo presentás las memorias recuperadas al modelo.
// ❌ Mal: Tirar todas las memorias como texto plano const badPrompt = ` Acá hay cosas que sabés: ${memories.map(m => m.text).join('\n')} Usuario: ${query} `; // ✅ Bien: Estructurado, priorizado, con señales de frescura const goodPrompt = ` ## Lo que Sabés de Este Usuario ${highConfidenceMemories.map(m => `- ${m.text} (última confirmación: ${formatRelativeTime(m.updatedAt)})` ).join('\n')} ## Contexto del Proyecto Relevante ${projectMemories.map(m => `- ${m.text}`).join('\n')} ## Potencialmente Desactualizado (verificar antes de usar) ${staleMemories.map(m => `- ${m.text} (de ${formatDate(m.createdAt)}, puede haber cambiado)` ).join('\n')} ## Conversación Actual ${recentMessages.map(m => `${m.role}: ${m.content}`).join('\n')} `;
Anti-patrón: Memoria sin olvido
Tan importante como recordar es saber cuándo olvidar.
class MemoryManager { // Implementar decay — las memorias que nunca se recuperan se debilitan async applyDecay(): Promise<void> { const allMemories = await this.store.getAll(); for (const memory of allMemories) { const daysSinceAccess = (Date.now() - memory.lastAccessedAt) / (1000 * 60 * 60 * 24); // Fórmula de decay: reducir confianza con el tiempo si no se accede const decayFactor = Math.exp(-0.01 * daysSinceAccess); const newConfidence = memory.confidence * decayFactor; if (newConfidence < 0.2) { await this.store.archive(memory.id); // No borrar, archivar } else { await this.store.updateConfidence(memory.id, newConfidence); } } } // Implementar detección de contradicciones async addWithContradictionCheck(newFact: Fact): Promise<void> { const existing = await this.store.search( `${newFact.subject} ${newFact.predicate}`, 5 ); const contradictions = existing.filter(e => e.subject === newFact.subject && e.predicate === newFact.predicate && e.object !== newFact.object ); if (contradictions.length > 0) { for (const old of contradictions) { await this.store.markSuperseded(old.id, newFact.id); } } await this.store.add(newFact); } }
Anti-patrón: Over-engineering para casos simples
No toda app necesita un sistema de memoria jerárquica de tres niveles con GraphRAG y consolidación automática. Seguí este árbol de decisión:
1. ¿La conversación es de <20 mensajes?
→ Ventana deslizante basta. Listo.
2. ¿El usuario necesita volver a la misma conversación después?
→ Sumá resumen de conversación. Tal vez ya alcanza.
3. ¿El agente necesita recordar hechos entre diferentes conversaciones?
→ Sumá extracción de entidades + vector store.
4. ¿El agente necesita entender relaciones entre entidades?
→ Sumá memoria basada en grafos.
5. ¿El agente necesita gestionar autónomamente qué recuerda?
→ Considerá el enfoque auto-gestionado de Letta.
Benchmarking de tu sistema de memoria
¿Cómo sabés si tu sistema de memoria funciona de verdad? Definí estas métricas:
// Test: ¿El agente puede recordar hechos de N mensajes atrás? async function testRecallAccuracy( agent: Agent, testFacts: { fact: string; queryAfterNMessages: number }[] ): Promise<number> { let correct = 0; for (const test of testFacts) { await agent.processMessage({ role: 'user', content: test.fact }); for (let i = 0; i < test.queryAfterNMessages; i++) { await agent.processMessage({ role: 'user', content: `Mensaje relleno ${i}: Contame sobre ${randomTopic()}`, }); } const response = await agent.processMessage({ role: 'user', content: `¿Qué te dije sobre ${extractSubject(test.fact)}?`, }); if (responseContainsFact(response, test.fact)) { correct++; } } return correct / testFacts.length; }
Métricas clave
| Métrica | Qué Mide | Objetivo |
|---|---|---|
| Recall@N | ¿Puede el agente recordar un hecho después de N mensajes? | >90% en N=50 |
| Tasa de contradicciones | ¿Con qué frecuencia usa info desactualizada? | <5% |
| Latencia de memoria | Tiempo de recuperación de memorias relevantes | <200ms |
| Eficiencia de tokens | Ratio de tokens relevantes vs totales en contexto | >60% |
| Recall entre sesiones | ¿Recuerda hechos de sesiones anteriores? | >80% |
Conclusión
Construir agentes de IA que recuerdan no se trata de encontrar la ventana de contexto más grande. Se trata de diseñar una arquitectura de memoria que refleje cómo la información realmente necesita fluir en tu aplicación.
La hoja de ruta práctica:
-
Arrancá simple. Ventana deslizante + resumen de conversación cubre el 80% de los casos. No sobre-diseñes desde el día uno.
-
Sumá persistencia cuando los usuarios lo exijan. Cuando esperan que tu agente los recuerde entre sesiones, necesitás extracción de entidades y un almacén persistente. Mem0 es el camino más rápido.
-
Sumá estructura cuando la memoria plana no alcanza. Cuando tu agente necesita entender relaciones — organigramas, grafos de dependencias, arquitecturas de sistemas — ahí paga la memoria basada en grafos.
-
Dejá que el agente se gestione solo cuando el problema sea lo suficientemente complejo. Para agentes autónomos que corren tareas de horas, el enfoque auto-gestionado de Letta evita la fragilidad de reglas hardcodeadas.
-
Siempre implementá el olvido. Un sistema de memoria sin decay se convierte en un pasivo. Los hechos desactualizados causan más daño que los hechos faltantes.
El tooling maduró enormemente en el último año. Lo que antes requería infraestructura custom ahora es un pip install o una llamada API. Lo difícil ya no es la tecnología — es diseñar la arquitectura de memoria correcta para tu caso específico.
Tus usuarios no van a agradecerte por un sistema de memoria perfecto. Pero definitivamente se van a dar cuenta cuando tu agente se olvide.
Explora herramientas relacionadas
Prueba estas herramientas gratuitas de Pockit