Sistemas de Memoria para Agentes: Contexto Persistente para tu IA
Cada agente de IA tiene un problema de memoria. Cada llamada al modelo es sin estado. El modelo no recuerda la solicitud anterior. No recuerda el nombre del usuario, sus preferencias ni sus decisiones previas. Sin memoria externa, tu agente comienza desde cero en cada turno.
Esta guía cubre cuatro estrategias de memoria — de la más simple a la más poderosa — y cuándo usar cada una. Todos los ejemplos usan TypeScript y el SDK de Anthropic.
Por qué los Agentes Necesitan Memoria
Considera un agente de revisión de código. En la primera ejecución, el usuario explica las convenciones de su equipo: sin tipos any, siempre manejar errores explícitamente, preferir el estilo funcional. El agente produce una buena revisión.
En la segunda ejecución, el usuario envía otro archivo. El agente ha olvidado todo. Omite las mismas violaciones de convención que señaló el día anterior.
Este es el problema central: los agentes no tienen estado, pero los agentes útiles necesitan continuidad.
La memoria da a los agentes tres capacidades:
- Recuperación — obtener hechos de más adelante en una conversación o de una sesión anterior
- Personalización — adaptarse a las preferencias del usuario con el tiempo
- Coordinación — en sistemas multi-agente, compartir estado entre agentes
Los Cuatro Tipos de Memoria
Antes de escribir código, es útil nombrar las estrategias con claridad. La memoria de los agentes cae en cuatro patrones:
| Tipo | Dónde se almacena | Alcance | Mejor para |
|---|---|---|---|
| Buffer | En contexto (arreglo de mensajes) | Sesión actual | Conversaciones cortas |
| Resumen | En contexto (comprimido) | Sesión actual | Conversaciones largas |
| Semántica | Externa (base de datos vectorial) | Entre sesiones | Recuperación de conocimiento |
| Episódica | Externa (almacén clave-valor) | Entre sesiones | Hechos de usuario, preferencias |
Cada estrategia tiene una compensación diferente entre simplicidad, costo y capacidad. Elige el mínimo que resuelva tu problema.
Estrategia 1: Buffer de Conversación
La estrategia más simple es pasar el historial completo de la conversación a cada llamada al modelo. El modelo ve cada mensaje anterior y responde con contexto completo.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Message { role: "user" | "assistant"; content: string;}
// El buffer contiene el historial completo de la conversaciónconst buffer: Message[] = [];
async function chat(userMessage: string): Promise<string> { // Agregar el nuevo mensaje del usuario al buffer buffer.push({ role: "user", content: userMessage });
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, // Pasar el buffer completo en cada llamada messages: buffer, });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
// Agregar la respuesta del asistente al buffer buffer.push({ role: "assistant", content: assistantMessage });
return assistantMessage;}
// Ejemplo de usoawait chat("Me llamo Alex y prefiero Python sobre TypeScript.");await chat("¿Qué lenguaje debería usar para mi próximo proyecto?");// El modelo recuerda la preferencia establecida en el primer mensajeCuándo usarlo: Conversaciones de menos de 20 turnos. Chatbots simples. Prototipos.
El límite: Las ventanas de contexto son finitas. Una sesión larga eventualmente supera el límite de tokens, y los mensajes más antiguos deben descartarse. El modelo pierde el contexto temprano de forma silenciosa, lo que causa un comportamiento inconsistente.
Estrategia 2: Resumen Progresivo
Cuando las conversaciones se prolongan, resume los turnos anteriores en lugar de descartarlos. Mantén un resumen comprimido de lo que sucedió antes de una ventana deslizante de mensajes recientes.
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface ConversationState { summary: string; // Historial comprimido de turnos anteriores recentMessages: Array<{ role: "user" | "assistant"; content: string }>; maxRecentTurns: number; // Mantener los últimos N turnos de forma literal}
const state: ConversationState = { summary: "", recentMessages: [], maxRecentTurns: 6, // 3 turnos de usuario + 3 de asistente};
// Comprimir los turnos anteriores en el resumen progresivoasync function compressSummary( currentSummary: string, messagesToCompress: Array<{ role: string; content: string }>): Promise<string> { const formatted = messagesToCompress .map((m) => `${m.role.toUpperCase()}: ${m.content}`) .join("\n");
const response = await client.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 512, messages: [ { role: "user", content: `Actualiza el resumen de la conversación agregando los nuevos intercambios a continuación.Devuelve solo el resumen actualizado. Mantenlo en menos de 200 palabras.
Resumen actual:${currentSummary || "(ninguno)"}
Nuevos intercambios:${formatted}`, }, ], });
return response.content[0].type === "text" ? response.content[0].text : currentSummary;}
async function chat(userMessage: string): Promise<string> { // Cuando el buffer supera el límite, comprimir la mitad más antigua if (state.recentMessages.length >= state.maxRecentTurns * 2) { const toCompress = state.recentMessages.splice(0, state.maxRecentTurns); state.summary = await compressSummary(state.summary, toCompress); }
state.recentMessages.push({ role: "user", content: userMessage });
// Construir el arreglo de mensajes: resumen del sistema + turnos recientes literales const messages = [ ...(state.summary ? [ { role: "user" as const, content: `Contexto de antes en esta conversación:\n${state.summary}`, }, { role: "assistant" as const, content: "Entendido. Usaré ese contexto.", }, ] : []), ...state.recentMessages, ];
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, messages, });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
state.recentMessages.push({ role: "assistant", content: assistantMessage }); return assistantMessage;}La llamada de resumen usa un modelo rápido y económico (Haiku). La conversación principal usa el modelo más capaz (Sonnet). Esta división mantiene los costos bajos mientras preserva la calidad.
Cuándo usarlo: Sesiones de más de 20 turnos. Agentes de soporte. Agentes de tareas de larga duración.
El límite: Los resúmenes pierden detalle. Los hechos específicos establecidos al principio de una conversación pueden comprimirse en declaraciones vagas. Para la recuperación precisa de hechos específicos, usa la memoria episódica.
Estrategia 3: Memoria Episódica
La memoria episódica almacena hechos específicos sobre un usuario o sesión en un almacén clave-valor. El agente extrae hechos durante una conversación y los recupera en sesiones futuras.
import Anthropic from "@anthropic-ai/sdk";import fs from "fs/promises";
const client = new Anthropic();
// En producción, reemplaza este almacén de archivos con Redis o una base de datosconst MEMORY_PATH = "./memory.json";
interface EpisodicStore { [userId: string]: Record<string, string>;}
async function loadMemory(): Promise<EpisodicStore> { try { const data = await fs.readFile(MEMORY_PATH, "utf-8"); return JSON.parse(data); } catch { return {}; }}
async function saveMemory(store: EpisodicStore): Promise<void> { await fs.writeFile(MEMORY_PATH, JSON.stringify(store, null, 2));}
// Extraer hechos estructurados del intercambio más recienteasync function extractFacts( userMessage: string, assistantReply: string): Promise<Record<string, string>> { const response = await client.messages.create({ model: "claude-haiku-4-5-20251001", max_tokens: 256, messages: [ { role: "user", content: `Extrae cualquier hecho personal, preferencia o decisión importante de este intercambio.Devuelve un objeto JSON de pares clave-valor. Devuelve {} si no hay nada que valga la pena recordar.
Usuario: ${userMessage}Asistente: ${assistantReply}`, }, ], });
try { const text = response.content[0].type === "text" ? response.content[0].text : "{}"; const match = text.match(/\{[\s\S]*\}/); return match ? JSON.parse(match[0]) : {}; } catch { return {}; }}
// Formatear los hechos almacenados como una inyección en el prompt del sistemafunction formatMemory(facts: Record<string, string>): string { const entries = Object.entries(facts); if (entries.length === 0) return ""; return ( "Lo que sabes sobre este usuario:\n" + entries.map(([k, v]) => `- ${k}: ${v}`).join("\n") );}
async function chat(userId: string, userMessage: string): Promise<string> { const store = await loadMemory(); const userFacts = store[userId] ?? {};
const systemPrompt = formatMemory(userFacts);
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: systemPrompt || undefined, messages: [{ role: "user", content: userMessage }], });
const assistantMessage = response.content[0].type === "text" ? response.content[0].text : "";
// Extraer hechos y persistirlos para sesiones futuras const newFacts = await extractFacts(userMessage, assistantMessage); if (Object.keys(newFacts).length > 0) { store[userId] = { ...userFacts, ...newFacts }; await saveMemory(store); }
return assistantMessage;}
// Sesión 1await chat("usuario-123", "Prefiero respuestas concisas. Sin viñetas a menos que sea necesario.");// Sesión 2 (un proceso diferente, días después)await chat("usuario-123", "Explica cómo funcionan los apretones de mano TCP.");// El agente recuerda la preferencia de formato de la sesión 1Cuándo usarlo: Agentes orientados al usuario que se ejecutan en varias sesiones. Personalización. Seguimiento de preferencias.
El límite: Los hechos se acumulan con el tiempo. Los hechos obsoletos pueden producir un comportamiento incorrecto (un usuario cambia su lenguaje preferido; la preferencia anterior sigue en el almacén). Agrega un mecanismo para actualizar o expirar hechos.
Estrategia 4: Memoria Semántica (Búsqueda Vectorial)
La memoria episódica almacena hechos discretos. La memoria semántica almacena documentos, código o fragmentos de conversación indexados por significado. Cuando el agente necesita información, busca en el índice usando una consulta.
Esta es la base de la Generación con Recuperación Aumentada (RAG).
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
interface Document { id: string; content: string; embedding?: number[];}
// Calcular la similitud coseno entre dos vectoresfunction cosineSimilarity(a: number[], b: number[]): number { const dot = a.reduce((sum, val, i) => sum + val * b[i], 0); const magA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); const magB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); return dot / (magA * magB);}
class SemanticMemory { private documents: Document[] = [];
// Indexar un documento para recuperarlo más tarde async add(id: string, content: string): Promise<void> { const embedding = await embed(content); // Usa un modelo de embeddings dedicado this.documents.push({ id, content, embedding }); }
// Recuperar los k documentos más relevantes para una consulta async search(query: string, topK = 3): Promise<Document[]> { const queryEmbedding = await embed(query); return this.documents .filter((doc) => doc.embedding) .map((doc) => ({ doc, score: cosineSimilarity(queryEmbedding, doc.embedding!), })) .sort((a, b) => b.score - a.score) .slice(0, topK) .map(({ doc }) => doc); }}
const memory = new SemanticMemory();
async function answerWithContext(question: string): Promise<string> { // Recuperar documentos relevantes const relevant = await memory.search(question); const context = relevant.map((d) => d.content).join("\n\n---\n\n");
const response = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 1024, system: context ? `Responde la pregunta usando el siguiente contexto:\n\n${context}` : undefined, messages: [{ role: "user", content: question }], });
return response.content[0].type === "text" ? response.content[0].text : "";}
// Ejemplo: indexar una base de conocimiento y consultarlaawait memory.add("doc-1", "El equipo usa React 18 con el patrón de app router.");await memory.add("doc-2", "Todas las rutas de API deben validar la entrada con esquemas Zod.");await memory.add("doc-3", "Las consultas de base de datos usan Drizzle ORM. Evitar SQL sin procesar.");
const answer = await answerWithContext("¿Cómo debería escribir un nuevo endpoint de API?");// El agente recupera doc-2 y doc-3 y genera una respuesta fundamentadaPara uso en producción, reemplaza el almacén en memoria con una base de datos vectorial dedicada. Pgvector es la opción más simple si ya usas PostgreSQL. Chroma y Qdrant son buenas opciones independientes.
Cuándo usarlo: Asistentes de documentación. Agentes de búsqueda de código. Preguntas y respuestas sobre bases de conocimiento.
Conectar la Memoria a MCP
Si seguiste la guía del servidor MCP, puedes exponer la memoria como un recurso MCP. Esto hace que tu capa de memoria sea accesible para cualquier cliente compatible con MCP.
import { Server } from "@modelcontextprotocol/sdk/server/index.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server( { name: "memory-server", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } });
// Herramienta: almacenar un hechoserver.setRequestHandler("tools/call", async (request) => { if (request.params.name === "remember") { const { key, value, userId } = request.params.arguments as { key: string; value: string; userId: string; }; await storeEpisodicFact(userId, key, value); return { content: [{ type: "text", text: `Almacenado: ${key} = ${value}` }] }; }
if (request.params.name === "recall") { const { userId } = request.params.arguments as { userId: string }; const facts = await loadEpisodicFacts(userId); return { content: [{ type: "text", text: JSON.stringify(facts, null, 2) }], }; }});
const transport = new StdioServerTransport();await server.connect(transport);Cualquier agente que hable MCP puede ahora llamar a remember y recall como herramientas estándar. La memoria se convierte en un servicio compartido en lugar de una implementación por agente.
Elegir la Estrategia Correcta
Usa esta tabla de decisión como punto de partida:
| Situación | Estrategia |
|---|---|
| Sesión única, menos de 20 turnos | Buffer |
| Sesión única, de larga duración | Resumen |
| Multi-sesión, preferencias de usuario | Episódica |
| Base de conocimiento grande para consultar | Semántica |
| Todo lo anterior | Combinar: resumen para la sesión + episódica/semántica entre sesiones |
Comienza con el buffer. Agrega una capa de resumen cuando las conversaciones se prolonguen. Agrega memoria episódica cuando los usuarios regresen en varias sesiones. Agrega memoria semántica cuando tengas un corpus de documentos para recuperar.
No agregues complejidad antes de necesitarla. Un buffer de conversación es la respuesta correcta para la mayoría de los prototipos.
Conclusión
La memoria no es una sola cosa — es un conjunto de estrategias que abordan diferentes problemas en diferentes alcances. El buffer maneja la sesión actual. El resumen extiende esa sesión. La memoria episódica une las sesiones. La memoria semántica fundamenta las respuestas en una base de conocimiento.
Elige la capa que resuelva el problema que tienes hoy. Envuélvela detrás de una interfaz limpia para poder intercambiar implementaciones más tarde. Y si tus agentes se comunican a través de MCP, expón la memoria como una herramienta compartida — mantiene cada agente simple mientras le da al sistema en su conjunto un cerebro persistente.
Artículos Relacionados
- Construyendo Tu Primer Servidor MCP
- Patrones Multi-Agente: Orquestadores, Workers y Pipelines
- Patrones de Uso de Herramientas: Interfaces Agente-Herramienta Confiables
- Introducción al Desarrollo Agéntico