Sistemas de memoria
Cerrando la brecha de estado. Memoria de trabajo, vector stores de largo plazo, logs episódicos, memoria semántica. Estrategias de gestión (resumen, decaimiento, priorización). Caso de estudio: Generative Agents (Park et al., 2023).
01Objetivos de aprendizaje
Al finalizar esta clase, los estudiantes serán capaces de:
- Explicar por qué la memoria es esencial para construir agentes de IA capaces e identificar las limitaciones de las interacciones sin estado con LLMs.
- Distinguir entre memoria a corto plazo, memoria de trabajo, memoria a largo plazo, memoria episódica y memoria semántica en el contexto de agentes de IA.
- Comparar enfoques de implementación para la memoria de agentes, incluyendo memoria en contexto, bases de datos vectoriales, almacenes clave-valor y bases de datos de grafos.
- Implementar un sistema de memoria básico basado en embeddings en Python que permita almacenar y recuperar experiencias pasadas.
- Analizar estrategias de gestión de memoria como resumen, olvido y priorización, y explicar cuándo es apropiada cada una.
- Evaluar críticamente la arquitectura de memoria del sistema "Generative Agents" (Park et al., 2023).
021. Por qué importa la memoria: estado en un mundo sin estado
El problema fundamental
Los modelos de lenguaje de gran tamaño (LLMs) son, en esencia, funciones sin estado. Dada una secuencia de tokens de entrada, producen una secuencia de salida. No poseen ningún mecanismo inherente para recordar interacciones previas, acumular conocimiento con el tiempo o aprender de errores pasados. Cada llamada a la API comienza desde cero.
Pensemos en lo que esto significa en la práctica. Imaginemos que contratamos a un consultor brillante, pero que cada mañana se despierta con amnesia total. Conserva todas sus habilidades y conocimientos generales, pero no recuerda nada sobre nuestra empresa, nuestras reuniones anteriores ni el proyecto en el que hemos estado trabajando juntos. Cada día tendríamos que volver a explicar todo desde el principio. Esa es la situación a la que nos enfrentamos con los LLMs sin estado.
Esta es una limitación grave para la construcción de agentes. Consideremos un escenario sencillo: pedimos a un asistente de IA que nos ayude a planificar unas vacaciones. En el primer mensaje, mencionamos que somos alérgicos al marisco. Diez mensajes después, el asistente sugiere un restaurante de pescado y marisco. Sin memoria, el agente no tiene forma de retener y aplicar información de momentos anteriores en la conversación, y mucho menos de conversaciones anteriores.
Idea clave: La brecha de estado es una de las diferencias más importantes entre la cognición humana y los sistemas basados en LLMs. La cognición humana es profundamente estatal: arrastramos contexto de cada interacción, construyendo modelos mentales de las personas con las que hablamos, las tareas en las que trabajamos y el mundo que nos rodea. Cerrar esta brecha es uno de los desafíos centrales en la construcción de agentes de IA eficaces.
Una perspectiva histórica
La importancia de la memoria en la IA no es una idea nueva. Los primeros sistemas de IA de los años 50 y 60 usaban tablas de símbolos y bases de datos explícitas para mantener el estado. Los sistemas expertos de los años 80 tenían la "memoria de trabajo" como componente central de su arquitectura (el sistema de producción OPS5, por ejemplo, tenía una memoria de trabajo explícita que almacenaba hechos sobre el problema actual). Lo que cambió con los LLMs es que el propio motor de razonamiento carece de memoria: esta debe añadirse externamente.
El desafío es análogo a la transición de protocolos sin estado (como el HTTP original) a aplicaciones web con estado. HTTP es un protocolo sin estado: cada petición es independiente. Los desarrolladores web resolvieron esto con cookies, sesiones y bases de datos. De forma similar, necesitamos construir infraestructura de memoria alrededor de los LLMs sin estado para crear agentes con estado.
Por qué los agentes necesitan más memoria que los chatbots
Un chatbot sencillo puede funcionar a menudo sólo con el historial de conversación en su ventana de contexto. Un agente, sin embargo, necesita memoria por varias razones adicionales:
- Ejecución de tareas de múltiples pasos: Los agentes trabajan en tareas que abarcan muchos pasos. Necesitan recordar qué han hecho, qué planeaban hacer y qué resultados intermedios obtuvieron. Un agente de programación que olvida que ya instaló una dependencia perderá tiempo (y tokens) reinstalándola.
- Seguimiento de interacciones con herramientas: Cuando un agente llama a APIs, consulta bases de datos o ejecuta código, necesita recordar los resultados y utilizarlos en el razonamiento posterior. Si un agente busca en la web y encuentra información relevante, necesita llevar esa información consigo.
- Aprendizaje por experiencia: Un agente verdaderamente capaz debería mejorar con el tiempo, recordando qué enfoques funcionaron y cuáles fallaron en tareas similares. La primera vez que un agente se encuentra con un error específico de una API, puede pasar muchos pasos depurando. La segunda vez, debería recordar la solución de inmediato.
- Personalización: Los agentes que trabajan con usuarios de forma recurrente necesitan recordar preferencias, solicitudes pasadas y contexto establecido. Un asistente personal que no puede recordar nuestras restricciones dietéticas, preferencias de reuniones o estilo de comunicación es significativamente menos útil.
- Continuidad entre sesiones: A diferencia de una única conversación, las tareas de los agentes pueden extenderse a lo largo de múltiples sesiones. El agente necesita retomar donde lo dejó. Consideremos un agente de investigación que tarda días en completar una revisión bibliográfica: no puede empezar de nuevo en cada sesión.
La restricción de la ventana de contexto
Los LLMs modernos tienen ventanas de contexto que van desde 4K hasta más de 1M de tokens. Podría pensarse que una ventana de contexto suficientemente grande elimina la necesidad de sistemas de memoria explícitos. Este es un error común, y es incorrecto por varias razones:
- El coste escala linealmente (o peor) con la longitud del contexto. Meter todo en la ventana de contexto es caro. Si estamos pagando 10 por interacción sólo en contexto.
- La precisión de recuperación se degrada con la longitud del contexto. El fenómeno "perdido en el medio" (Liu et al., 2024) muestra que los LLMs tienen dificultades para usar información colocada en la mitad de contextos largos. La información al principio y al final se usa eficazmente, pero la del medio a menudo se ignora.
- Las ventanas de contexto siguen siendo finitas. Incluso una ventana de 1M de tokens acabará agotándose con un agente de larga duración. Un agente de ingeniería de software que trabaja en una base de código grande puede exceder fácilmente cualquier ventana de contexto.
- No toda la información es igualmente relevante. Un sistema de memoria bien diseñado recupera sólo la información más pertinente para el paso actual, mejorando tanto la eficiencia como la precisión. Volcarlo todo en el contexto es como entregar a alguien una biblioteca entera cuando necesita un dato específico.
Idea clave: Una ventana de contexto grande es un búfer, no un sistema de memoria. La memoria requiere almacenamiento selectivo, recuperación eficiente y gestión inteligente, capacidades que una ventana de contexto brutá no proporciona.
Inténtalo tú mismo: experimentando el problema de la memoria
Antes de adentrarnos en las soluciones, probemos este experimento mental. Imaginemos que somos un agente sin más memoria que nuestra ventana de contexto actual. Un usuario nos da la siguiente secuencia de instrucciones en conversaciones separadas:
- Sesión 1: "Estoy trabajando en un proyecto llamado Phoenix. Usamos Python 3.11 y PostgreSQL."
- Sesión 2: "Continúa trabajando en el proyecto Phoenix."
En la Sesión 2, no tenemos ni idea de a qué se refiere "Phoenix", qué stack tecnológico utiliza ni qué progreso se hizo. Este es el problema qué estamos resolviendo.
032. Memoria a corto plazo: contexto de conversación y cuadernos de notas
El contexto de conversación como memoria
La forma más sencilla de memoria de un agente es el propio historial de conversación. Cada mensaje intercambiado entre el usuario, el agente y sus herramientas forma un registro cronológico que el LLM puede consultar.
User: Find me papers about media bias detection published after 2022.
Agent: [calls search tool] I found 15 papers. Here are the top 5...
User: Which of those use transformer-based models?
Agent: [references previous results] Of the 15 papers I found, 8 use transformer-based models...En este intercambio, la capacidad del agente para responder a la pregunta de seguimiento depende enteramente de que los mensajes anteriores estén en su ventana de contexto. El pronombre "those" (esos) en el segundo mensaje no tiene significado sin el contexto del primer intercambio.
Este es el mismo principio que explica cómo funciona la memoria a corto plazo humana en la conversación. Cuando alguien nos dice su nombre en una fiesta, lo retenemos en la mente durante los siguientes minutos. Si la conversación cambia de tema durante diez minutos y luego nos preguntan "¿Recuerdas mi nombre?", puede que lo hayamos perdido. El contexto de conversación en los LLMs funciona de forma similar: la información reciente es accesible, pero se desvanece a medida que el contexto se llena de información más nueva.
Limitaciones del contexto de conversación bruto:
- Crece linealmente con la longitud de la interacción, excediendo eventualmente la ventana de contexto.
- Contiene mucho ruido (intentos fallidos, salidas verbosas de herramientas, cortesías, mensajes del sistema).
- Sin priorización: la información reciente no tiene inherentemente más peso que la anterior (aunque existen efectos posicionales en cómo los LLMs atienden al contexto).
- Sin estructura: todo es una secuencia plana de mensajes, lo que dificulta encontrar información específica.
Cuadernos de notas (Scratchpads)
Un scratchpad es un espacio de trabajo explícito donde el agente puede anotar resultados intermedios, planes y notas para sí mismo. A diferencia del historial de conversación, que es un registro pasivo de todo lo ocurrido, un scratchpad es gestionado activamente por el agente: este elige qué escribir y puede actualizar o eliminar entradas.
Pensemos en un scratchpad como el bloc de notas que un detective usa durante una investigación. No anota cada palabra de cada conversación. Escribe los hechos clave, las pistas a seguir y las hipótesis actuales. El bloc está curado y organizado, a diferencia de una transcripción bruta.
class Scratchpad:
"""A simple scratchpad for agent intermediate reasoning.
The scratchpad provides a structured way for agents to maintain
key information across reasoning steps. Unlike raw conversation
history, the scratchpad is actively curated -- the agent decides
what to record and can update entries as understanding evolves.
"""
def __init__(self):
self.entries = []
def write(self, key: str, value: str) -> None:
"""Write or update a scratchpad entry.
If an entry with the given key already exists, it is updated
in place. This allows the agent to refine its notes as it
learns more -- for example, updating a 'current_hypothesis'
entry as new evidence comes in.
"""
# Update existing entry if key exists
for entry in self.entries:
if entry["key"] == key:
entry["value"] = value
return
self.entries.append({"key": key, "value": value})
def read(self, key: str) -> str | None:
"""Read a scratchpad entry by key."""
for entry in self.entries:
if entry["key"] == key:
return entry["value"]
return None
def delete(self, key: str) -> bool:
"""Remove a scratchpad entry when it is no longer needed."""
for i, entry in enumerate(self.entries):
if entry["key"] == key:
self.entries.pop(i)
return True
return False
def to_prompt(self) -> str:
"""Format scratchpad contents for inclusion in the LLM prompt.
This method is called before each LLM invocation to inject
the scratchpad contents into the prompt, giving the agent
access to its curated notes.
"""
if not self.entries:
return "Scratchpad: (empty)"
lines = ["Scratchpad:"]
for entry in self.entries:
lines.append(f" - {entry['key']}: {entry['value']}")
return "\n".join(lines)Veamos cómo un agente podría usar este scratchpad durante una tarea de investigación:
- El usuario pide: "Encuentra el artículo más citado sobre detección de sesgo mediático de 2023."
- El agente busca, encuentra 10 artículos. Escribe en el scratchpad:
search_results: "Found 10 papers. Top candidates: Smith2023 (45 citations), Jones2023 (38 citations), Lee2023 (52 citations)" - El agente necesita verificar los recuentos de citas. Escribe:
current_task: "Verify citation count for Lee2023 on Google Scholar" - Tras la verificación, actualiza:
answer: "Lee2023 with 52 citations is the most cited" - El scratchpad ahora contiene un resumen limpio de los hallazgos del agente, no todo el historial de búsqueda.
El patrón del scratchpad se utiliza en sistemas como chain-of-thought prompting (Wei et al., 2022) y en frameworks de agentes donde el agente mantiene explícitamente un "bloc de notas" con hechos clave y resultados intermedios.
Gestión de la ventana de contexto
Cuando el historial de conversación crece demasiado, los agentes necesitan estrategias para gestionarlo. Este no es un problema teórico: ocurre en la práctica en los primeros minutos de una tarea compleja de un agente.
-
Ventana deslizante: Conservar sólo los N mensajes más recientes. Es el enfoque más sencillo. Es como una cámara de seguridad que sólo guarda las últimas 24 horas de grabación: fácil de implementar pero se pierde contexto temprano potencialmente importante. Si el usuario indicó una preferencia importante en el mensaje n.º 3 y estamos en el mensaje n.º 50, esa preferencia se ha perdido.
-
Resumen: Resumir periódicamente los mensajes más antiguos en una representación compacta. Esto preserva la información clave pero puede perder detalles. Es como leer el resumen de un libro en lugar del libro completo: captamos los puntos principales pero perdemos los matices.
-
Retención selectiva: Conservar los mensajes qué contienen decisiones clave, preferencias del usuario o resultados importantes, descartando los intercambios rutinarios. Es más sofisticado pero requiere la capacidad de juzgar qué mensajes son importantes.
def summarize_context(messages: list[dict], model: str = "gpt-4") -> str:
"""Summarize older messages to compress the conversation context.
This function takes a list of messages and produces a compact
summary that preserves the essential information. In practice,
this is called when the conversation history approaches the
context window limit.
The key challenge is deciding what to preserve. The prompt
instructs the LLM to focus on facts, decisions, and preferences
rather than conversational pleasantries or failed attempts.
"""
summary_prompt = (
"Summarize the following conversation, preserving all key facts, "
"decisions, user preferences, and task progress. Omit small talk, "
"failed attempts that were later corrected, and verbose tool outputs. "
"Be concise but complete:\n\n"
)
for msg in messages:
summary_prompt += f"{msg['role']}: {msg['content']}\n"
# Call the LLM to generate a summary
response = llm_call(model=model, prompt=summary_prompt)
return responseError común: "El resumen no tiene pérdidas." No es así. Cada paso de resumen pierde algo de información. La pregunta es si la información pérdida importa. En la práctica, una buena estrategia de resumen pierde sobre todo ruido (salidas verbosas de herramientas, intentos fallidos) y retiene sobre todo señal (hechos clave, decisiones, preferencias). Pero pueden perderse detalles importantes, por lo que la información crítica también debe almacenarse en la memoria a largo plazo.
043. Memoria de trabajo: mantenimiento del estado de la tarea entre pasos
¿Qué es la memoria de trabajo?
En psicología cognitiva, la memoria de trabajo se refiere al sistema que retiene y manipula información necesaria para las tareas cognitivas en curso. No es simplemente un búfer pasivo: es un espacio de trabajo activo donde la información se combina, transforma y usa para el razonamiento. Cuando hacemos aritmética mental (¿cuánto es 37 por 12?), estamos usando la memoria de trabajo para retener resultados intermedios (37 por 10 es 370, 37 por 2 es 74, 370 más 74 es 444).
Para los agentes de IA, la memoria de trabajo cumple una función análoga: mantiene el estado actual de la tarea, incluyendo el plan, el progreso y cualquier dato intermedio relevante. Es la diferencia entre un agente que avanza a trompicones paso a paso por una tarea (sin recordar el panorama general) y uno que mantiene una visión clara de dónde está, dónde ha estado y hacia dónde va.
La memoria de trabajo se distingue de la memoria a corto plazo en que es estructurada y orientada a la tarea, en lugar de ser simplemente un búfer de entradas recientes. El historial de conversación es un registro cronológico; la memoria de trabajo es una representación organizada del estado actual de la tarea.
Una analogía: el modelo mental del cirujano
Consideremos un cirujano realizando una operación compleja. No se limita a reaccionar a lo que ve en cada momento. Mantiene un modelo mental de:
- Qué intenta conseguir (el objetivo)
- Qué ha hecho ya (los pasos completados)
- Qué necesita hacer a continuación (el plan)
- Qué ha salido mal y cómo se adaptó (gestión de errores)
- El estado actual del paciente (resultados intermedios)
Esta es la memoria de trabajo en acción. Un agente de IA necesita el mismo tipo de seguimiento estructurado del estado.
Representación del estado de la tarea
Una memoria de trabajo bien diseñada para un agente podría incluir:
from dataclasses import dataclass, field
from enum import Enum
class TaskStatus(Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
FAILED = "failed"
BLOCKED = "blocked"
@dataclass
class TaskState:
"""Represents the current state of an agent's task execution.
This dataclass captures everything the agent needs to know about
its current task: the goal, the plan, where it is in execution,
what results it has obtained, and any errors encountered.
By serializing this state into the prompt (via to_prompt()), the
agent gets a clear picture of its progress at each step, rather
than having to piece it together from conversation history.
"""
goal: str # What are we trying to achieve?
plan: list[str] = field(default_factory=list) # The step-by-step plan
current_step: int = 0 # Which step are we on?
intermediate_results: dict[str, str] = field(default_factory=dict) # Results so far
status: TaskStatus = TaskStatus.PENDING # Overall status
errors: list[str] = field(default_factory=list) # Errors encountered
context_variables: dict[str, str] = field(default_factory=dict) # Key facts
def advance(self, result: str) -> None:
"""Record the result of the current step and advance to the next.
This method is called after each successful step execution.
It stores the result keyed by the step name and moves the
current_step pointer forward. When all steps are complete,
it automatically marks the task as COMPLETED.
"""
step_name = self.plan[self.current_step] if self.current_step < len(self.plan) else "unknown"
self.intermediate_results[step_name] = result
self.current_step += 1
if self.current_step >= len(self.plan):
self.status = TaskStatus.COMPLETED
def record_error(self, error: str) -> None:
"""Record an error encountered during execution.
Errors are accumulated rather than overwritten, creating a
log that helps the agent (and developers) understand what
went wrong during execution.
"""
self.errors.append(error)
def to_prompt(self) -> str:
"""Serialize task state for inclusion in the agent prompt.
This is the critical method: it converts the structured task
state into a text format that the LLM can understand. The
format clearly shows which steps are done, which is next,
and what results have been obtained.
"""
lines = [
f"Goal: {self.goal}",
f"Status: {self.status.value}",
f"Plan ({self.current_step}/{len(self.plan)} steps completed):",
]
for i, step in enumerate(self.plan):
marker = "[done]" if i < self.current_step else "[next]" if i == self.current_step else "[ ]"
result = self.intermediate_results.get(step, "")
result_str = f" -> {result}" if result else ""
lines.append(f" {marker} {step}{result_str}")
if self.errors:
lines.append("Errors encountered:")
for err in self.errors:
lines.append(f" - {err}")
return "\n".join(lines)Sigamos un ejemplo. Supongamos que un agente tiene la tarea de "Analizar el sentimiento de las reseñas de clientes de ProductoX". Tras planificar y ejecutar dos de cuatro pasos, la salida de to_prompt() podría ser:
Goal: Analyze the sentiment of customer reviews for ProductX
Status: in_progress
Plan (2/4 steps completed):
[done] Fetch customer reviews from the database -> Retrieved 1,247 reviews
[done] Clean and preprocess the review text -> Cleaned 1,247 reviews, removed 23 duplicates
[next] Run sentiment analysis on preprocessed reviews
[ ] Generate summary report with visualizationsEsto le da al LLM una visión clara y estructurada del estado de la tarea, mucho mejor que recorrer docenas de mensajes de conversación.
Memoria de trabajo en la práctica
El framework ReAct (Yao et al., 2023) utiliza implícitamente memoria de trabajo a través de sus trazas intercaladas de razonamiento y acción. En cada paso, el agente tiene acceso a todos los pensamientos, acciones y observaciones previos, que en conjunto forman su memoria de trabajo. El paso de "pensamiento" es particularménte importante: es donde el agente razona sobre lo que sabe, lo que necesita hacer a continuación y qué obstáculos enfrenta.
Sistemas más sofisticados como Voyager (Wang et al., 2023) mantienen una memoria de trabajo explícita que incluye:
- El objetivo actual
- Una biblioteca de habilidades de comportamientos aprendidos previamente
- El estado del entorno (p. ej., inventario en Minecraft)
- Retroalimentación de intentos recientes
Inténtalo tú mismo: Diseña una estructura de memoria de trabajo para un agente de atención al cliente. ¿Qué campos incluirías? Piensa en: el problema del cliente, su estado emocional, las acciones ya realizadas, las políticas de empresa aplicables y el estado de escalado.
054. Memoria a largo plazo: almacenes de conocimiento persistentes
La necesidad de persistencia
La memoria a corto plazo y la memoria de trabajo son efímeras: existen sólo durante la duración de una tarea o sesión. Cuando el agente se apaga (o la llamada a la API termina), desaparecen. La memoria a largo plazo persiste entre sesiones, permitiendo al agente acumular conocimiento, aprender de la experiencia y mantener continuidad.
La analogía es directa: la memoria a corto plazo es como la RAM, y la memoria a largo plazo es como un disco duro. La RAM es rápida pero volátil (se pierde al apagar). Un disco duro es más lento pero persistente. Así como los ordenadores necesitan ambas, los agentes necesitan ambos tipos de memoria.
La memoria a largo plazo responde a preguntas como:
- "¿Qué me preguntó este usuario la semana pasada?"
- "¿Qué enfoque funcionó cuando intenté este tipo de tarea antes?"
- "¿Cuáles son los hechos clave que he aprendido sobre este dominio?"
- "¿Cuáles son las preferencias y restricciones de este usuario?"
Enfoques de implementación
Hay varias formas de implementar la memoria a largo plazo, cada una con distintas fortalezas. La elección depende del tipo de información almacenada y de cómo debe recuperarse.
1. Almacenamiento basado en archivos
El enfoque más sencillo: escribir memorias en archivos y leerlas de vuelta. Este es el enfoque utilizado por herramientas como los archivos CLAUDE.md de Claude Code, donde el contexto del proyecto se almacena en archivos markdown que se cargan al inicio de cada sesión.
import json
from pathlib import Path
from datetime import datetime
class FileMemory:
"""Simple file-based long-term memory.
This approach stores memories as JSON Lines (one JSON object per
line), which makes it easy to append new memories without
rewriting the entire file. It is suitable for small-scale
applications and prototyping.
Think of this as a simple diary: entries are written chronologically
and can be searched by keyword. It does not scale well (searching
requires reading the entire file) but it is easy to understand,
debug, and implement.
"""
def __init__(self, memory_dir: str):
self.memory_dir = Path(memory_dir)
self.memory_dir.mkdir(parents=True, exist_ok=True)
self.memory_file = self.memory_dir / "memories.jsonl"
def store(self, content: str, metadata: dict | None = None) -> None:
"""Store a memory entry.
Each entry is a single line of JSON, containing the content,
a timestamp, and optional metadata. The JSONL format allows
efficient appending without loading the entire file.
"""
entry = {
"content": content,
"timestamp": datetime.now().isoformat(),
"metadata": metadata or {},
}
with open(self.memory_file, "a") as f:
f.write(json.dumps(entry) + "\n")
def retrieve_all(self) -> list[dict]:
"""Retrieve all stored memories."""
if not self.memory_file.exists():
return []
memories = []
with open(self.memory_file) as f:
for line in f:
memories.append(json.loads(line.strip()))
return memories
def search(self, query: str) -> list[dict]:
"""Simple keyword-based search over memories.
This is a brute-force approach: it loads all memories and
checks each one for keyword matches. For small collections
(hundreds of memories), this is fine. For larger collections,
you would need indexing (see vector databases below).
"""
query_lower = query.lower()
results = []
for memory in self.retrieve_all():
if query_lower in memory["content"].lower():
results.append(memory)
return resultsEste enfoque es fácil de implementar pero no escala y carece de capacidades de búsqueda semántica. Si la preferencia de un usuario se almacenó como "I prefer vegetarian food" y buscamos "dietary restrictions", la búsqueda por palabras clave no la encontrará. Aquí es donde las bases de datos vectoriales se vuelven esenciales.
2. Almacenamiento en base de datos vectorial
Las bases de datos vectoriales almacenan memorias como embeddings y soportan búsqueda por similitud, permitiendo recuperación semántica. Este es el enfoque más común en los sistemas de agentes modernos. La idea central: convertir texto en un vector numérico (embedding) que captura su significado, y luego encontrar vectores similares usando distancia matemática.
La analogía es una biblioteca. En una biblioteca tradicional, encontramos libros por encabezados temáticos y palabras clave (como la búsqueda por palabras clave). Una base de datos vectorial es más como un bibliotecario que entiende lo que queremos decir: si pedimos "libros sobre alimentación saludable", también sugeriría libros catalogados bajo "nutrición", "dieta" y "bienestar", conceptos semánticamente relacionados aunque las palabras sean diferentes.
Bases de datos vectoriales populares incluyen:
- Chroma: Ligera, embebida, buena para prototipado. Se ejecuta en el mismo proceso que la aplicación.
- Pinecone: Servicio gestionado en la nube, escala bien. Buena para producción pero requiere una cuenta.
- Weaviate: Código abierto, soporta búsqueda híbrida (combinando búsqueda vectorial y por palabras clave).
- Qdrant: Código abierto, alto rendimiento, escrita en Rust.
- pgvector: Extensión de PostgreSQL, buena para equipos que ya usan Postgres. Mantiene todo en una sola base de datos.
3. Almacenes clave-valor
Para memorias estructuradas (preferencias de usuario, configuración, hechos), un almacén clave-valor puede ser más apropiado que una base de datos vectorial. Cuando sabemos exactamente lo que buscamos (p. ej., "¿cuál es el lenguaje de programación preferido de este usuario?"), una consulta clave-valor es más rápida y fiable que la búsqueda semántica.
import sqlite3
from datetime import datetime
class KeyValueMemory:
"""Key-value store for structured agent memories.
This is ideal for storing discrete facts that have a clear key:
- User preferences: ("preferred_language", "Python")
- Configuration: ("max_retries", "3")
- Named facts: ("user_timezone", "Europe/Madrid")
We use SQLite for persistence, which gives us ACID transactions
and SQL querying without requiring a separate database server.
The 'category' field allows grouping related memories.
"""
def __init__(self, db_path: str):
self.conn = sqlite3.connect(db_path)
self.conn.execute(
"""CREATE TABLE IF NOT EXISTS memories (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
category TEXT,
updated_at TEXT
)"""
)
self.conn.commit()
def set(self, key: str, value: str, category: str = "general") -> None:
"""Store or update a key-value pair.
INSERT OR REPLACE ensures that if the key already exists,
its value is updated rather than creating a duplicate.
This is important because facts change: a user might update
their preferred programming language from Python to Rust.
"""
self.conn.execute(
"INSERT OR REPLACE INTO memories (key, value, category, updated_at) VALUES (?, ?, ?, ?)",
(key, value, category, datetime.now().isoformat()),
)
self.conn.commit()
def get(self, key: str) -> str | None:
"""Retrieve a value by its exact key."""
row = self.conn.execute("SELECT value FROM memories WHERE key = ?", (key,)).fetchone()
return row[0] if row else None
def get_by_category(self, category: str) -> dict[str, str]:
"""Retrieve all key-value pairs in a category.
This is useful for loading all user preferences at once,
or all facts about a specific project.
"""
rows = self.conn.execute(
"SELECT key, value FROM memories WHERE category = ?", (category,)
).fetchall()
return {key: value for key, value in rows}4. Bases de datos de grafos
Para conocimiento relacional (entidades, relaciones, ontologías), las bases de datos de grafos como Neo4j proporcionan potentes capacidades de consulta. La ventaja clave de las bases de datos de grafos es que representan de forma natural las relaciones entre entidades:
(User:Person {name: "Alice"}) -[:PREFERS]-> (Diet:Preference {type: "vegetarian"})
(User:Person {name: "Alice"}) -[:WORKS_AT]-> (Company:Organization {name: "Acme Corp"})
(Task:Task {id: "t1"}) -[:DEPENDS_ON]-> (Task:Task {id: "t2"})Las bases de datos de grafos destacan cuando la memoria involucra relaciones complejas entre entidades. Por ejemplo, si un agente gestiona un proyecto con múltiples personas, tareas y dependencias, una base de datos de grafos puede responder preguntas como "¿qué tareas asignadas a Alice dependen de tareas asignadas a Bob?" con una sola consulta. Intentar responder esto con un almacén clave-valor plano o una base de datos vectorial sería mucho más difícil.
Sin embargo, las bases de datos de grafos añaden una complejidad arquitectónica significativa. Hay que diseñar un esquema, aprender un lenguaje de consulta (como Cypher para Neo4j) y gestionar la base de datos. Para la mayoría de aplicaciones de agentes, los enfoques más sencillos (clave-valor + vectorial) son suficientes.
Idea clave: En la práctica, la mayoría de los sistemas de agentes en producción usan un enfoque híbrido: un almacén clave-valor para hechos estructurados (preferencias de usuario, configuración), una base de datos vectorial para conocimiento no estructurado (conversaciones pasadas, documentos) y el contexto de conversación para el estado inmediato. Cada tipo de información va al sistema de almacenamiento mejor adaptado a ella.
065. Memoria episódica: recordar experiencias pasadas
¿Qué es la memoria episódica?
Inspirada en el concepto de las ciencias cognitivas, la memoria episódica para agentes de IA se refiere al almacenamiento y recuperación de experiencias o episodios pasados específicos. Cada episodio captura qué ocurrió, cuándo ocurrió y el contexto en qué ocurrió.
En la cognición humana, la memoria episódica es lo que nos permite recordar eventos concretos: "el martes pasado probé el nuevo restaurante italiano de la calle Mayor y la pasta estaba excelente". Es diferente de la memoria semántica (saber que "la pasta es un plato italiano") porque está vinculada a un momento, lugar y contexto específicos.
Para los agentes de IA, la memoria episódica responde a preguntas como:
- "La última vez que intenté analizar un PDF, ¿qué enfoque usé?"
- "¿Qué pasó cuando llamé a esta API con esos parámetros?"
- "¿Cómo reaccionó el usuario cuando sugerí este enfoque antes?"
- "La última vez que una migración de base de datos falló, ¿cuál fue la causa raíz?"
El poder de la memoria episódica es qué permite aprender de la experiencia. En lugar de abordar cada tarea desde cero usando solo conocimiento general, el agente puede recordar qué funcionó (y qué no) en situaciones similares.
Estructura de un episodio
¿Qué información debería capturar un episodio? Pensemos en lo que un médico escribe en el historial de un paciente tras una visita: cuál era el problema (contexto), qué hizo (acción), qué pasó (resultado) y si fue efectivo (éxito). La memoria episódica de un agente debería capturar información similar:
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class Episode:
"""A single episodic memory entry.
Each episode is a self-contained record of something that happened.
It captures not just the action taken, but the full context: what
was the situation, what was tried, what resulted, and whether it
was successful. This rich structure enables the agent to learn
nuanced lessons from experience.
"""
description: str # What happened (natural language summary)
timestamp: datetime # When it happened
context: dict[str, str] # Surrounding context (task, project, user, etc.)
action_taken: str # What the agent did
outcome: str # What resulted from the action
success: bool # Whether the outcome was positive
tags: list[str] = field(default_factory=list) # Categorization tags
importance: float = 0.5 # Importance score (0.0 to 1.0)
def to_text(self) -> str:
"""Convert episode to natural language for LLM consumption.
This method creates a readable narrative from the structured
fields. The LLM can understand and reason about this text
representation when deciding how to handle similar situations.
"""
success_str = "successfully" if self.success else "unsuccessfully"
return (
f"On {self.timestamp.strftime('%Y-%m-%d')}, while working on "
f"{self.context.get('task', 'unknown task')}, I {success_str} "
f"{self.action_taken}. The outcome was: {self.outcome}"
)Memoria episódica en Generative Agents
El artículo pionero "Generative Agents: Interactive Simulacra of Human Behavior" (Park et al., 2023) introdujo una arquitectura convincente para la memoria episódica en agentes de IA. Este artículo merece un estudio detallado porque demuestra cómo la memoria puede dar lugar a comportamientos notablemente humanos.
En el sistema Generative Agents, 25 agentes de IA viven en un pueblo simulado (inspirado en Los Sims). Cada agente tiene un nombre, ocupación, relaciones y rutinas diarias. Lo notable del sistema no es el razonamiento de los agentes individuales, sino los comportamientos sociales emergentes que surgen de sus sistemas de memoria. Los agentes formaron amistades, organizarón fiestas, difundieron rumores e incluso se presentaron como candidatos a alcalde, todo emergiendo de la interacción entre memoria, recuperación y planificación.
El sistema de memoria funciona así:
-
Las observaciones se registran como entradas en un flujo de memoria, cada una con una marca temporal, descripción y punteros a memorias relacionadas. Todo lo que el agente percibe queda registrado: "Isabella Rodríguez está preparando la cafetería", "María López dijo que está interesada en presentarse a alcaldesa".
-
La recuperación usa una función de puntuación que combina tres factores:
- Recencia: Las memorias más recientes puntúan más alto (decaimiento exponencial). Una memoria de hace una hora es más relevante que una de la semana pasada, en igualdad de condiciones.
- Importancia: Las memorias clasificadas como más importantes puntúan más alto. "El padre de Isabella falleció" es más importante que "Isabella desayunó".
- Relevancia: Las memorias semánticamente similares a la consulta actual puntúan más alto. Al decidir qué hacer en una fiesta, las memorias sobre eventos sociales previos son más relevantes que las memorias sobre ir al supermercado.
-
La reflexión sintetiza periódicamente memorias episódicas en ideas de nivel superior, que a su vez se almacenan como memorias. Tras acumular muchas observaciones sobre el comportamiento de un vecino, el agente podría generar la reflexión: "María López parece estar muy interesada en el liderazgo comunitario". Estas reflexiones se convierten en memorias y pueden recuperarse en consultas futuras.
La función de puntuación de recuperación del artículo:
score(memory) = alpha * recency(memory) + beta * importance(memory) + gamma * relevance(memory, query)Dónde alpha, beta y gamma son parámetros de ponderación que equilibran los tres factores.
Idea clave: El sistema Generative Agents muestra que comportamientos sofisticados pueden emerger de mecanismos de memoria relativamente simples. Los agentes no tienen razonamiento social explícito: su comportamiento social emerge de recordar interacciones, reflexionar sobre patrones y usar esas reflexiones para guiar el comportamiento futuro. La memoria es la base de la inteligencia emergente.
Implementación de la recuperación episódica
Implementemos las tres funciones de puntuación del artículo de Generative Agents:
import math
from datetime import datetime
def compute_recency_score(
memory_timestamp: datetime, current_time: datetime, decay_factor: float = 0.995
) -> float:
"""Compute recency score with exponential decay.
More recent memories get higher scores. The decay_factor controls
how quickly older memories lose relevance.
With the default decay_factor of 0.995:
- 1 hour old: score = 0.995^1 = 0.995 (very recent, high score)
- 24 hours old: score = 0.995^24 = 0.887 (still quite relevant)
- 1 week old: score = 0.995^168 = 0.429 (moderately relevant)
- 1 month old: score = 0.995^720 = 0.027 (mostly forgotten)
This mirrors how human memory works: recent events are vivid,
while older events fade unless they are particularly important
or frequently recalled.
"""
hours_elapsed = (current_time - memory_timestamp).total_seconds() / 3600.0
return math.pow(decay_factor, hours_elapsed)
def compute_relevance_score(
memory_embedding: list[float], query_embedding: list[float]
) -> float:
"""Compute cosine similarity between memory and query embeddings.
Cosine similarity measures how similar two vectors are in terms
of their direction (ignoring magnitude). It ranges from -1
(opposite directions) to 1 (same direction), with 0 meaning
no relationship.
For text embeddings, cosine similarity captures semantic
relatedness: "dog" and "puppy" will have high similarity,
while "dog" and "algebra" will have low similarity.
"""
dot_product = sum(a * b for a, b in zip(memory_embedding, query_embedding))
norm_a = math.sqrt(sum(a * a for a in memory_embedding))
norm_b = math.sqrt(sum(b * b for b in query_embedding))
if norm_a == 0 or norm_b == 0:
return 0.0
return dot_product / (norm_a * norm_b)
def retrieve_episodes(
episodes: list[dict],
query_embedding: list[float],
current_time: datetime,
alpha: float = 1.0, # Weight for recency
beta: float = 1.0, # Weight for importance
gamma: float = 1.0, # Weight for relevance
top_k: int = 5,
) -> list[dict]:
"""Retrieve the most relevant episodes using the Generative Agents scoring.
This function implements the core retrieval algorithm from the
Generative Agents paper. Each memory is scored on three dimensions
(recency, importance, relevance), and the top-k highest-scoring
memories are returned.
The alpha, beta, and gamma weights allow you to tune the balance:
- High alpha: Prefer recent memories (good for fast-changing contexts)
- High beta: Prefer important memories (good for critical decisions)
- High gamma: Prefer relevant memories (good for specific queries)
"""
scored = []
for episode in episodes:
recency = compute_recency_score(episode["timestamp"], current_time)
importance = episode["importance"]
relevance = compute_relevance_score(episode["embedding"], query_embedding)
score = alpha * recency + beta * importance + gamma * relevance
scored.append((score, episode))
scored.sort(key=lambda x: x[0], reverse=True)
return [episode for _, episode in scored[:top_k]]Inténtalo tú mismo: Consideremos un agente con 100 memorias episódicas. Llega una consulta moderadamente relevante para una memoria muy antigua pero muy importante, y altamente relevante para una memoria reciente pero poco importante. ¿Qué memoria debería recuperarse? Experimenta con diferentes valores de alpha/beta/gamma para ver cómo cambia la puntuación. No hay una única respuesta correcta: depende del caso de uso.
076. Memoria semántica: hechos y recuperación de conocimiento
¿Qué es la memoria semántica?
Mientras que la memoria episódica almacena experiencias específicas ("el martes pasado probé el nuevo restaurante"), la memoria semántica almacena conocimiento general y hechos ("París es la capital de Francia"). En las ciencias cognitivas, la memoria semántica es nuestro conocimiento del mundo independiente de cuándo o dónde lo aprendimos. Sabemos que el agua hierve a 100 grados Celsius, pero probablemente no recordamos la lección específica dónde lo aprendimos.
Para los agentes de IA, la memoria semántica almacena:
- Conocimiento del dominio relevante para las tareas del agente
- Preferencias y perfiles de usuarios
- Reglas y heurísticas aprendidas
- Hechos extraídos de documentos o interacciones
Memoria semántica frente a conocimiento paramétrico
Los LLMs ya contienen un vasto conocimiento semántico en sus parámetros (conocimiento paramétrico). GPT-4 "sabe" que París es la capital de Francia porque este hecho apareció en sus datos de entrenamiento. Entonces, ¿por qué necesitamos un sistema de memoria semántica explícito?
El propósito de un sistema de memoria semántica explícito es:
- Complementar el conocimiento paramétrico con información específica del dominio o actualizada. La documentación interna de la API de nuestra empresa no está en los datos de entrenamiento de GPT-4.
- Corregir el conocimiento paramétrico cuando los datos de entrenamiento del LLM están desactualizados o son incorrectos. Si una empresa cambia de CEO, el conocimiento paramétrico del LLM queda obsoleto hasta que se reentrene.
- Personalizar el conocimiento para el usuario o contexto de despliegue específico. El LLM conoce las mejores prácticas generales para Python, pero nuestro equipo tiene convenciones de código específicas.
- Auditar y explicar de dónde proviene un conocimiento concreto. Cuando un agente hace una afirmación, podemos rastrearla hasta un documento o interacción específica en lugar de un parámetro opaco del modelo.
Error común: "Los LLMs ya lo saben todo, así que no necesitamos memoria semántica." Los LLMs saben mucho, pero su conocimiento está congelado en el momento del entrenamiento, puede contener errores y carece de información sobre nuestro contexto específico. La memoria semántica es el puente entre el conocimiento general y el conocimiento específico, actualizado y personalizado.
Implementación de la memoria semántica
Un sistema práctico de memoria semántica almacena hechos como entradas estructuradas con atribución de fuente. Usamos tripletas sujeto-predicado-objeto, la misma estructura utilizada en grafos de conocimiento y la web semántica:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class SemanticFact:
"""A single fact in semantic memory.
Facts are stored as subject-predicate-object triples, a format
borrowed from knowledge representation theory. This structure
makes it easy to query ("what do we know about X?") and update
("the CEO of Acme is now Jane, not John").
The confidence and source fields support reasoning about fact
reliability: a fact from an official document has higher
confidence than a fact inferred from casual conversation.
"""
subject: str # What the fact is about (e.g., "Acme Corp")
predicate: str # The relationship or property (e.g., "CEO is")
object: str # The value or target (e.g., "Jane Smith")
confidence: float # How confident we are (0.0 to 1.0)
source: str # Where this fact came from
last_verified: datetime # When this was last confirmed
def to_triple(self) -> str:
return f"({self.subject}, {self.predicate}, {self.object})"
def to_natural_language(self) -> str:
return f"{self.subject} {self.predicate} {self.object} (confidence: {self.confidence:.0%})"
class SemanticMemory:
"""A simple semantic memory store using subject-predicate-object triples.
This implementation stores facts in memory (for simplicity) and
supports querying by subject and/or predicate. In production,
you would back this with a database (SQL for structured queries,
or a graph database for relationship traversal).
A key design decision: when a new fact is added with the same
subject and predicate as an existing fact, the old fact is
replaced. This ensures the agent always uses the most current
information. For example, if the user's preferred language
changes from Python to Rust, the old preference is overwritten.
"""
def __init__(self):
self.facts: list[SemanticFact] = []
def add_fact(self, fact: SemanticFact) -> None:
"""Add a fact, replacing any existing fact with same subject and predicate."""
self.facts = [
f for f in self.facts
if not (f.subject == fact.subject and f.predicate == fact.predicate)
]
self.facts.append(fact)
def query(self, subject: str | None = None, predicate: str | None = None) -> list[SemanticFact]:
"""Query facts by subject and/or predicate.
Examples:
- query(subject="Alice") returns all facts about Alice
- query(predicate="works at") returns all employment facts
- query(subject="Alice", predicate="prefers") returns Alice's preferences
"""
results = self.facts
if subject:
results = [f for f in results if f.subject.lower() == subject.lower()]
if predicate:
results = [f for f in results if f.predicate.lower() == predicate.lower()]
return sorted(results, key=lambda f: f.confidence, reverse=True)
def to_prompt(self, subject: str | None = None) -> str:
"""Format relevant facts for inclusion in the LLM prompt."""
facts = self.query(subject=subject) if subject else self.facts
if not facts:
return "No relevant facts in memory."
lines = ["Known facts:"]
for fact in facts:
lines.append(f" - {fact.to_natural_language()}")
return "\n".join(lines)Extracción de conocimiento
Una capacidad importante es la extracción automática de hechos a partir de conversaciones y documentos. En lugar de requerir que un humano añada hechos manualmente a la memoria semántica, el agente puede extraerlos:
EXTRACTION_PROMPT = """Extract factual information from the following text as
subject-predicate-object triples. Only extract facts that are explicitly stated,
not inferences or opinions.
Text: {text}
Format each fact as:
- Subject: ...
- Predicate: ...
- Object: ...
- Confidence: high/medium/low
"""Por ejemplo, dado el texto "Alice works at Acme Corp as a senior engineer. She prefers Python for backend development", el LLM extraería:
- (Alice, works at, Acme Corp) [confidence: high]
- (Alice, role is, senior engineer) [confidence: high]
- (Alice, prefers for backend, Python) [confidence: high]
087. Enfoques de implementación: un análisis comparativo
Memoria en contexto
Cómo funciona: Toda la memoria se coloca directamente en la ventana de contexto del LLM como parte del prompt.
Ventajas:
- La más sencilla de implementar: sin infraestructura, sin bases de datos, sin modelos de embedding.
- Sin dependencias externas: funciona con cualquier LLM.
- El LLM puede razonar sobre toda la memoria disponible simultáneamente, ya que lo ve todo a la vez.
Desventajas:
- Limitada por el tamaño de la ventana de contexto: a medida que la memoria crece, se alcanza el límite.
- El coste aumenta linealmente con el tamaño de la memoria: se paga por cada token en el contexto.
- La precisión de recuperación se degrada con la longitud (Liu et al., 2024): el problema de "perdido en el medio".
Ideal para: Tareas cortas, volúmenes de memoria pequeños, prototipado, chatbots sencillos.
Bases de datos vectoriales
Cómo funciona: Las memorias se codifican en representaciones vectoriales y se almacenan en una base de datos especializada. La recuperación se realiza mediante búsqueda por similitud.
# Example using chromadb
import chromadb
# Create a client and collection
client = chromadb.Client()
collection = client.create_collection(name="agent_memory")
# Store a memory -- ChromaDB automatically generates embeddings
collection.add(
documents=["The user prefers Python over JavaScript for data analysis tasks."],
metadatas=[{"type": "preference", "timestamp": "2024-01-15"}],
ids=["mem_001"],
)
# Retrieve relevant memories -- note that the query is semantically
# matched, not just keyword matched. "programming language" would
# also retrieve this memory even though those exact words are not in it.
results = collection.query(
query_texts=["What programming language should I use?"],
n_results=3,
)Ventajas:
- Búsqueda semántica: encuentra memorias conceptualmente relacionadas, no solo coincidencias de palabras clave.
- Escala a millones de memorias eficientemente.
- Recuperación en tiempo constante (vecinos más próximos aproximádos).
Desventajas:
- Requiere un modelo de embedding (añade complejidad y latencia).
- La búsqueda aproximada puede no encontrar resultados relevantes (típicamente 95-99% de recall).
- Sin consultas estructuradas: no se puede hacer "dame todas las preferencias añadidas después de enero de 2024".
Ideal para: Grandes almacenes de memoria, recuperación semántica, sistemas basados en RAG, memoria episódica.
Almacenes clave-valor
Cómo funciona: Las memorias se almacenan como pares clave-valor, a menudo con metadatos adicionales para filtrado.
Ventajas:
- Búsquedas exactas rápidas: O(1) para recuperar una memoria específica.
- Estructurado y predecible: siempre se obtiene exactamente lo que se pidió.
- Fácil de actualizar y eliminar: cambiar una preferencia es una simple actualización.
Desventajas:
- Sin búsqueda semántica: hay que conocer la clave exacta.
- No apto para consultas abiertas como "¿qué sé sobre este tema?"
- Estructura plana: sin soporte para relaciones complejas.
Ideal para: Preferencias de usuario, configuración, hechos estructurados, estado de sesión.
Bases de datos de grafos
Cómo funciona: Las memorias se almacenan cómo nodos y aristas en un grafo, capturando entidades y sus relaciones.
Ventajas:
- Consultas relacionales ricas: "encontrar todas las tareas asignadas a personas del equipo de ingeniería".
- Representación natural del conocimiento: entidades y relaciones se mapean directamente al grafo.
- Soporta razonamiento multi-salto: "¿quién gestiona a la persona que escribió este código?"
Desventajas:
- Complejo de configurar y mantener: requiere diseño de esquema y una base de datos dedicada.
- Curva de aprendizaje del lenguaje de consulta: Cypher (Neo4j) o SPARQL no son triviales.
- Excesivo para necesidades de memoria simples: la mayoría de agentes no necesitan razonamiento multi-salto.
Ideal para: Dominios complejos con muchas relaciones entre entidades, grafos de conocimiento, datos organizacionales.
Tabla comparativa
| Característica | En contexto | BD vectorial | Clave-valor | BD de grafos |
|---|---|---|---|---|
| Búsqueda semántica | Vía LLM | Nativa | No | Limitada |
| Escalabilidad | Baja | Alta | Alta | Media |
| Complejidad de configuración | Ninguna | Baja | Baja | Alta |
| Consultas estructuradas | No | Limitadas | Sí | Sí |
| Modelado de relaciones | Vía LLM | No | No | Nativo |
| Costé a escala | Alto | Bajo | Bajo | Medio |
| Ideal para | Prototipado | Recuperación no estructurada | Hechos estructurados | Conocimiento relacional |
098. Gestión de memoria: resumen, olvido y priorización
El problema de la gestión de memoria
A medida que un agente acumula memorias, se enfrenta a los mismos desafíos que los humanos: demasiada información, capacidad de procesamiento limitada y la necesidad de distinguir memorias importantes de triviales. Se estima que el cerebro humano se encuentra con unos 34 GB de información al día, pero solo retiene una fracción mínima. Los agentes se enfrentan a un problema de selección similar.
Sin gestión de memoria, el almacén de memoria crece sin límite. La recuperación se vuelve más lenta, más cara y menos precisa (porque las memorias irrelevantes diluyen las relevantes). Las estrategias de gestión de memoria abordan esto decidiendo qué conservar, qué comprimir y qué descartar.
Resumen
El resumen comprime múltiples memorias en una representación más compacta. Se utiliza a dos niveles:
Resumen de conversación: Comprimir las partes más antiguas de una conversación para que quepan en la ventana de contexto. Este es el caso de uso más común: cuando una conversación excede el límite de contexto, los mensajes más antiguos se resumen.
def progressive_summarization(
messages: list[dict], max_context_tokens: int = 4000
) -> list[dict]:
"""Progressively summarize older messages to fit context budget.
Keeps recent messages verbatim and summarizes older ones. This
approach preserves detail where it matters most (recent context)
while retaining the gist of older interactions.
The recursive implementation handles cases where even after
summarization, the result is still too long -- it summarizes
again until the budget is met.
"""
# Estimate tokens (rough: 1 token per 4 characters)
def estimate_tokens(msgs):
return sum(len(m["content"]) // 4 for m in msgs)
if estimate_tokens(messages) <= max_context_tokens:
return messages # Everything fits, no summarization needed
# Split: older half gets summarized, recent half stays verbatim
midpoint = len(messages) // 2
older = messages[:midpoint]
recent = messages[midpoint:]
summary = summarize_context(older)
summarized_msg = {"role": "system", "content": f"Summary of earlier conversation: {summary}"}
result = [summarized_msg] + recent
# Recursively summarize if still too long
if estimate_tokens(result) > max_context_tokens:
return progressive_summarization(result, max_context_tokens)
return resultResumen basado en reflexión: Como se vio en Generative Agents (Park et al., 2023), sintetizando periódicamente memorias en observaciones de nivel superior. Esto no es sólo compresión, es abstracción. El agente examina muchas memorias específicas y genera ideas generales.
REFLECTION_PROMPT = """Given the following recent observations, generate 3-5
higher-level insights or patterns. These should be generalizations that could
be useful in future situations. Focus on patterns, preferences, and lessons learned.
Recent observations:
{observations}
Higher-level insights:"""Por ejemplo, dadas observaciones como "API call to ServiceX failed with timeout", "API call to ServiceX failed with 503" y "API call to ServiceX succeeded after retry", la reflexión podría generar: "ServiceX is unreliable and often requires retries. Always implement retry logic when calling ServiceX."
Olvido
No todas las memorias deben conservarse indefinidamente. El olvido es una estrategia importante de gestión de memoria, no un defecto. En la cognición humana, el olvido cumple una función vital: previene la sobrecarga de información y asegura que la información más relevante sea fácilmente accesible. Un agente que lo recuerda todo por igual tiene dificultades para encontrar la señal en el ruido.
Estrategias de olvido:
- Decaimiento temporal: Las memorias que no se han accedido durante mucho tiempo se eliminan gradualmente. Es el enfoque más sencillo y refleja el decaimiento exponencial de la función de recuperación de Generative Agents.
- Poda basada en relevancia: Las memorias que nunca se recuperan (baja relevancia para las consultas) son candidatas a eliminación. Si una memoria nunca ha sido útil, probablemente nunca lo será.
- Eliminación de redundancia: Cuando una nueva memoria reemplaza a una antigua, la antigua puede eliminarse. Si aprendemos que "el CEO es Jane" y ya teníamos "el CEO es John", la memoria antigua es obsoleta.
- Expulsión basada en capacidad: Cuando la memoria alcanza un límite de tamaño, se eliminan las memorias menos importantes. Es la estrategia LRU (Least Recently Used, menos recientemente usado) de los sistemas operativos, aplicada a las memorias del agente.
from datetime import datetime, timedelta
def forget_old_memories(
memories: list[dict], max_age_days: int = 90, min_importance: float = 0.3
) -> list[dict]:
"""Remove memories that are old and unimportant.
High-importance memories are kept regardless of age -- these are
the "core memories" that define the agent's knowledge (like a
user's dietary restrictions or critical project constraints).
Low-importance memories are forgotten after max_age_days, unless
they have been recently accessed (which would update their
access timestamp and make them "recent" again).
"""
cutoff = datetime.now() - timedelta(days=max_age_days)
retained = []
forgotten_count = 0
for memory in memories:
is_recent = memory["timestamp"] > cutoff
is_important = memory.get("importance", 0.5) >= min_importance
if is_recent or is_important:
retained.append(memory)
else:
forgotten_count += 1
if forgotten_count > 0:
print(f"Forgot {forgotten_count} old, low-importance memories.")
return retainedPriorización
Al recuperar memorias, el agente debe decidir cuáles son las más relevantes. El enfoque de Generative Agents (combinando recencia, importancia y relevancia) es una estrategia. Otras incluyen:
- Basada en frecuencia: Las memorias que se acceden con frecuencia probablemente son más importantes. Si el agente sigue recuperando un hecho particular, probablemente es central para el trabajo actual.
- Saliencia emocional: En agentes orientados a usuarios, las memorias asociadas con emociones fuertes del usuario (frustración, satisfacción) pueden priorizarse. Si un usuario estaba muy frustrado por una experiencia previa, el agente debería recordarlo.
- Relevancia para la tarea: Las memorias etiquetadas con el tipo de tarea actual reciben un impulso. Al trabajar en un proyecto Python, las memorias relacionadas con Python son más relevantes que las relacionadas con JavaScript.
- Detección de contradicciones: Las memorias que se contradicen entre sí señalan una necesidad de actualización. Si una memoria dice "el endpoint de la API es /v1/users" y otra dice "el endpoint de la API es /v2/users", el agente debería investigar.
109. Ejemplo práctico: construcción de un sistema de memoria con recuperación basada en embeddings
Construyamos un sistema de memoria completo y funcional que un agente pueda usar para almacenar y recuperar memorias mediante embeddings. Recorreremos cada componente y explicaremos las decisiones de diseño.
Implementación completa
"""
A complete embedding-based memory system for AI agents.
This implementation uses sentence-transformers for embeddings and
numpy for similarity computation. In production, you would use a
vector database (Chroma, Qdrant, pgvector) for scalability, but
this implementation makes every step transparent for learning.
Requirements:
pip install sentence-transformers numpy
"""
import json
import math
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
import numpy as np
from sentence_transformers import SentenceTransformer
@dataclass
class Memory:
"""A single memory entry.
This dataclass represents everything we store about a memory:
- id: A unique identifier for retrieval and deletion.
- content: The text of the memory itself.
- timestamp: When the memory was created (for recency scoring).
- memory_type: A category label ("episodic", "semantic", "preference").
- importance: How important this memory is (0.0 to 1.0).
- metadata: Arbitrary additional data (project name, task type, etc.).
- access_count: How often this memory has been retrieved.
- last_accessed: When this memory was last retrieved.
- embedding: The vector representation of the content.
"""
id: str
content: str
timestamp: str
memory_type: str # "episodic", "semantic", "preference"
importance: float # 0.0 to 1.0
metadata: dict = field(default_factory=dict)
access_count: int = 0
last_accessed: str = ""
embedding: list[float] = field(default_factory=list)
class AgentMemorySystem:
"""A complete memory system with embedding-based retrieval.
This system supports:
- Storing memories with automatic embedding generation
- Semantic retrieval using cosine similarity
- Importance-based scoring (Generative Agents style)
- Recency-based scoring (exponential decay)
- Persistence to disk (JSON format)
- Access tracking (for frequency-based prioritization)
Architecture overview:
1. When a memory is stored, we generate an embedding vector using
a sentence-transformer model. This vector captures the semantic
meaning of the text.
2. When a query comes in, we generate an embedding for the query
and compare it to all stored memory embeddings using cosine
similarity.
3. The final score combines recency, importance, and semantic
relevance (following Park et al., 2023).
4. The top-k scoring memories are returned.
"""
def __init__(
self,
persist_path: str | None = None,
embedding_model: str = "all-MiniLM-L6-v2",
decay_rate: float = 0.995,
):
self.persist_path = Path(persist_path) if persist_path else None
self.decay_rate = decay_rate
self.memories: list[Memory] = []
# Load the embedding model.
# all-MiniLM-L6-v2 produces 384-dimensional embeddings and is
# fast enough for real-time use. For higher quality, consider
# BGE-large-en-v1.5 (1024 dimensions) or a similar model.
print(f"Loading embedding model: {embedding_model}")
self.encoder = SentenceTransformer(embedding_model)
# Load persisted memories if available
if self.persist_path and self.persist_path.exists():
self._load()
def store(
self,
content: str,
memory_type: str = "episodic",
importance: float = 0.5,
metadata: dict | None = None,
) -> str:
"""Store a new memory with its embedding.
This is the write path of the memory system. When a new memory
is stored:
1. A unique ID is generated.
2. The content is embedded using the sentence-transformer model.
3. The memory is appended to the in-memory list.
4. If persistence is configured, the entire list is saved to disk.
Args:
content: The text content of the memory.
memory_type: Category of memory (episodic, semantic, preference).
importance: Importance score from 0.0 to 1.0.
metadata: Additional metadata to store with the memory.
Returns:
The ID of the stored memory.
"""
memory_id = str(uuid.uuid4())[:8]
now = datetime.now().isoformat()
# Generate embedding -- this is the key step that enables
# semantic retrieval later. The embedding captures the
# meaning of the content as a vector of numbers.
embedding = self.encoder.encode(content).tolist()
memory = Memory(
id=memory_id,
content=content,
timestamp=now,
memory_type=memory_type,
importance=importance,
metadata=metadata or {},
access_count=0,
last_accessed=now,
embedding=embedding,
)
self.memories.append(memory)
# Persist if configured
if self.persist_path:
self._save()
return memory_id
def retrieve(
self,
query: str,
top_k: int = 5,
memory_type: str | None = None,
alpha: float = 1.0, # Weight for recency
beta: float = 1.0, # Weight for importance
gamma: float = 1.0, # Weight for relevance
) -> list[dict]:
"""Retrieve the most relevant memories for a query.
Uses a weighted combination of recency, importance, and
semantic relevance (following Park et al., 2023).
The scoring formula is:
score = alpha * recency + beta * importance + gamma * relevance
where:
- recency is exponential decay based on hours since creation
- importance is the stored importance value (0-1)
- relevance is cosine similarity between query and memory embeddings
Args:
query: The query text to search for.
top_k: Number of memories to return.
memory_type: Optional filter by memory type.
alpha: Weight for recency in the scoring function.
beta: Weight for importance in the scoring function.
gamma: Weight for semantic relevance in the scoring function.
Returns:
List of memory dicts sorted by relevance score.
"""
if not self.memories:
return []
# Filter by type if specified
candidates = self.memories
if memory_type:
candidates = [m for m in candidates if m.memory_type == memory_type]
if not candidates:
return []
# Compute query embedding -- same model used for storage
query_embedding = self.encoder.encode(query)
now = datetime.now()
scored_memories = []
for memory in candidates:
# Recency score: exponential decay based on hours elapsed
mem_time = datetime.fromisoformat(memory.timestamp)
hours_elapsed = (now - mem_time).total_seconds() / 3600.0
recency = math.pow(self.decay_rate, hours_elapsed)
# Importance score: directly from the memory
importance = memory.importance
# Relevance score: cosine similarity between embeddings
mem_embedding = np.array(memory.embedding)
q_embedding = np.array(query_embedding)
cosine_sim = np.dot(mem_embedding, q_embedding) / (
np.linalg.norm(mem_embedding) * np.linalg.norm(q_embedding) + 1e-8
)
relevance = float(cosine_sim)
# Combined score using the Generative Agents formula
score = alpha * recency + beta * importance + gamma * relevance
scored_memories.append({
"id": memory.id,
"content": memory.content,
"memory_type": memory.memory_type,
"importance": memory.importance,
"timestamp": memory.timestamp,
"metadata": memory.metadata,
"scores": {
"recency": round(recency, 4),
"importance": round(importance, 4),
"relevance": round(relevance, 4),
"total": round(score, 4),
},
})
# Sort by total score descending
scored_memories.sort(key=lambda x: x["scores"]["total"], reverse=True)
# Update access counts for retrieved memories -- this enables
# frequency-based analysis later
top_ids = {m["id"] for m in scored_memories[:top_k]}
for memory in self.memories:
if memory.id in top_ids:
memory.access_count += 1
memory.last_accessed = now.isoformat()
return scored_memories[:top_k]
def format_for_prompt(self, memories: list[dict]) -> str:
"""Format retrieved memories for inclusion in an LLM prompt.
This method transforms the structured memory data into a
text format that can be injected into a prompt. It includes
the memory type and relevance score to help the LLM
understand the source and reliability of each memory.
"""
if not memories:
return "No relevant memories found."
lines = ["Relevant memories:"]
for i, mem in enumerate(memories, 1):
lines.append(
f" [{i}] ({mem['memory_type']}, relevance: {mem['scores']['relevance']:.2f}) "
f"{mem['content']}"
)
return "\n".join(lines)
def get_stats(self) -> dict:
"""Return statistics about the memory system."""
type_counts = {}
for memory in self.memories:
type_counts[memory.memory_type] = type_counts.get(memory.memory_type, 0) + 1
return {
"total_memories": len(self.memories),
"by_type": type_counts,
"avg_importance": (
sum(m.importance for m in self.memories) / len(self.memories)
if self.memories else 0
),
}
def _save(self) -> None:
"""Persist memories to disk as JSON."""
data = [asdict(m) for m in self.memories]
with open(self.persist_path, "w") as f:
json.dump(data, f, indent=2)
def _load(self) -> None:
"""Load memories from disk."""
with open(self.persist_path) as f:
data = json.load(f)
self.memories = [Memory(**entry) for entry in data]
# -- Usage Example ---------------------------------------------------
def main():
"""Demonstrate the memory system."""
# Initialize memory system
memory = AgentMemorySystem(persist_path="agent_memories.json")
# Store various types of memories
memory.store(
"The user prefers concise explanations with code examples.",
memory_type="preference",
importance=0.8,
)
memory.store(
"Successfully fixed a bug in the data pipeline by adding null checks.",
memory_type="episodic",
importance=0.6,
metadata={"task": "bug_fix", "project": "data_pipeline"},
)
memory.store(
"Python's asyncio library is used for concurrent I/O operations.",
memory_type="semantic",
importance=0.4,
)
memory.store(
"The user's project uses PostgreSQL with pgvector for embeddings.",
memory_type="semantic",
importance=0.7,
metadata={"project": "main_app"},
)
memory.store(
"Previous attempt to use Redis for caching failed due to memory limits.",
memory_type="episodic",
importance=0.5,
metadata={"task": "caching", "outcome": "failed"},
)
# Retrieve memories for a query
print("=" * 60)
print("Query: 'How should I store embeddings in the database?'")
print("=" * 60)
results = memory.retrieve("How should I store embeddings in the database?", top_k=3)
print(memory.format_for_prompt(results))
print()
print("=" * 60)
print("Query: 'What went wrong in past tasks?'")
print("=" * 60)
results = memory.retrieve("What went wrong in past tasks?", top_k=3, memory_type="episodic")
print(memory.format_for_prompt(results))
print()
print("Memory stats:", memory.get_stats())
if __name__ == "__main__":
main()Salída esperada
============================================================
Query: 'How should I store embeddings in the database?'
============================================================
Relevant memories:
[1] (semantic, relevance: 0.72) The user's project uses PostgreSQL with pgvector for embeddings.
[2] (semantic, relevance: 0.34) Python's asyncio library is used for concurrent I/O operations.
[3] (episodic, relevance: 0.28) Previous attempt to use Redis for caching failed due to memory limits.
============================================================
Query: 'What went wrong in past tasks?'
============================================================
Relevant memories:
[1] (episodic, relevance: 0.51) Previous attempt to use Redis for caching failed due to memory limits.
[2] (episodic, relevance: 0.29) Successfully fixed a bug in the data pipeline by adding null checks.
Memory stats: {'total_memories': 5, 'by_type': {'preference': 1, 'episodic': 2, 'semantic': 2}, 'avg_importance': 0.6}Nótese cómo la primera consulta recupera la memoria de pgvector (semánticamente relevante para "embeddings in database") aunque la consulta no usa las palabras exactas "pgvector" o "PostgreSQL". Este es el poder de la búsqueda semántica. La segunda consulta, filtrada sólo a memorias episódicas, recupera primero la experiencia de fallo, exactamente lo que querríamos al preguntar sobre problemas pasados.
Inténtalo tú mismo: Extiende esta implementación con un método
forget()que elimine memorias de más de 90 días con importancia inferior a 0.3. Luego añáde un métodoreflect()que use un LLM para generar ideas de nivel superior a partir de las memorias episódicas almacenadas.
1110. Temas avanzados
Arquitecturas de memoria en sistemas de vanguardia
Varios sistemas recientes muestran arquitecturas de memoria sofisticadas:
MemGPT (Packer et al., 2023): Trata la gestión del contexto del LLM como un problema de sistema operativo, con una jerarquía de memoria virtual que página información dentro y fuera de la ventana de contexto. Igual que un sistema operativo gestiona la RAM física paginando datos hacia y desde el disco, MemGPT gestiona la ventana de contexto del LLM paginando información hacia y desde almacenamiento externo. El sistema usa llamadas a funciones para gestionar su propia memoria: cuando el contexto se llena, escribe información menos importante al "disco" (una base de datos externa) y lee información más relevante. Esto permite conversaciones que se extienden mucho más allá de la ventana de contexto, con el agente manteniendo un contexto coherente a largo plazo.
Voyager (Wang et al., 2023): Un agente potenciado por LLM para Minecraft que mantiene una biblioteca de habilidades, una forma de memoria procedimental donde las secuencias de acciones exitosas se almacenan como funciones JavaScript reutilizables. Cuando el agente descubre como construir un pico de madera, guarda esa habilidad como una función. Más tarde, cuando necesita un pico, puede recuperar y ejecutar la habilidad guardada en lugar de descubrirla de nuevo. Esto es análogo a como los humanos desarrollan memoria motora: la primera vez que nos atamos los zapatos, requiere esfuerzo consciente. Tras la práctica, se vuelve automático, almacenado como memoria procedimental en lugar de conocimiento declarativo.
Cognitive Architectures for Language Agents (CoALA) (Sumers et al., 2024): Un framework que organiza la memoria del agente en tres componentes que reflejan la cognición humana: memoria de trabajo (el contexto actual del agente), memoria episódica (experiencias pasadas) y memoria semántica (conocimiento general). Este framework proporciona una forma principiada de diseñar sistemas de memoria para agentes lingüísticos, basándose en décadas de investigación en ciencias cognitivas. La contribución clave es una taxonomía de diseños de memoria y un conjunto de principios de diseño para elegir entre ellos.
Memoria y privacidad
Una consideración importante en los sistemas de memoria de agentes es la privacidad. Cuanto más recuerda un agente, más datos sensibles almacena potencialmente.
- ¿Qué debería recordarse? Los agentes deberían tener políticas claras sobre qué información almacenan, especialmente datos personales. Un asistente médico debería recordar que un paciente es alérgico a la penicilina, pero ¿debería recordar cada queja mencionada de pasada?
- Derecho al olvido: Los usuarios deberían poder solicitar la eliminación de sus memorias almacenadas. Esto no es solo una buena práctica: regulaciones como el RGPD lo exigen explícitamente.
- Control de acceso: En sistemas multiusuario, las memorias deben estar correctamente delimitadas para prevenir fugas de información. Las memorias del Agente A sobre el Usuario A no deben ser accesibles al servir al Usuario B.
- Cifrado: Las memorias sensibles deberían cifrarse en repóso. Si la base de datos de memoria se ve comprometida, los datos deberían ser ilegibles sin la clave de cifrado.
Consistencia de la memoria
Cuando las memorias entran en conflicto, el agente necesita una estrategia de resolución:
- Sesgo de recencia: Preferir la memoria más reciente (asume que la información más reciente es más precisa). Generalmente es un valor predeterminado razonable.
- Credibilidad de la fuente: Preferir memorias de fuentes más fiables. Un hecho de un documento oficial pesa más que uno de una conversación informal.
- Ponderación por confianza: Preferir memorias con puntuaciones de confianza más altas.
- Detección explícita de contradicciones: Marcar memorias conflictivas para revisión humana en lugar de elegir silenciosamente una.
Idea clave: La consistencia de la memoria es un problema no resuelto en los sistemas de agentes. En la práctica, la mayoría de los sistemas usan la recencia como criterio de desempate, pero esto puede llevar a errores cuando información desactualizada se reintroduce por error. La detección y resolución robustas de contradicciones siguen siendo áreas de investigación activa.
12Preguntas para debate
-
Memoria e identidad: Si las memorias de un agente se borran, ¿sigue siendo el "mismo" agente? ¿Cómo contribuye la memoria a la noción de identidad o continuidad del agente? Reflexionemos: si perdiéramos todos nuestros recuerdos, ¿seguiríamos siendo "nosotros"? ¿Qué nos dice esto sobre la relación entre memoria e identidad en los sistemas de IA?
-
El derecho al olvido: ¿Deberían los agentes de IA tener un mecanismo de "derecho al olvido" donde los usuarios puedan solicitar la eliminación de memorias específicas? ¿Cuáles son las implicaciones para la capacidad del agente frente a la privacidad del usuario? Consideremos la tensión: un agente que lo recuerda todo es más capaz, pero un usuario que no puede controlar lo que se recuerda puede sentirse vigilado.
-
Manipulación de memoria: Si las memorias moldean el comportamiento de un agente, ¿cuáles son los riesgos de la inyección adversarial de memorias (alimentar deliberadamente al agente con memorias falsas para manipular su comportamiento futuro)? ¿Cómo podríamos diseñar defensas contra esto? Pista: considerar la atribución de fuente, la puntuación de confianza y la verificación de consistencia.
-
Fidelidad cognitiva: ¿Hasta qué punto deberían los sistemas de memoria de agentes de IA reflejar la memoria humana? ¿Hay casos donde arquitecturas de memoria no humanas (p. ej., recuerdo perfecto sin olvido) serían más útiles? Pista: considerar que el olvido humano a veces causa errores pero también cumple funciones útiles como la generalización y el filtrado de ruido.
-
La memoria como ventaja competitiva: En los sistemas de IA comerciales, las memorias acumuladas de interacciones con usuarios podrían ser una ventaja competitiva significativa. ¿Cuáles son las implicaciones éticas de esto? ¿Deberían los usuarios ser propietarios de las memorias de su agente y poder portarlas a un sistema diferente?
-
Comportamiento emergente a partir de la memoria: El artículo de Generative Agents (Park et al., 2023) observó comportamientos sociales emergentes surgidos de la memoria y la reflexión. ¿Qué otros comportamientos emergentes podrían surgir de sistemas de memoria sofisticados? ¿Podrían emerger también comportamientos negativos (rencores, sesgos, manipulación)?
13Resumen y conclusiones clave
-
La memoria transforma LLMs sin estado en agentes con estado. Sin memoria, los agentes no pueden mantener contexto, aprender de la experiencia ni personalizar su comportamiento. La memoria es posiblemente el componente de infraestructura más importante para construir agentes capaces.
-
Múltiples tipos de memoria sirven para diferentes propósitos:
- Memoria a corto plazo (contexto de conversación) para la interacción inmediata.
- Memoria de trabajo (estado de la tarea) para la ejecución de múltiples pasos.
- Memoria episódica para aprender de experiencias pasadas.
- Memoria semántica para acumular conocimiento.
-
Ninguna implementación única cubre todas las necesidades. La memoria en contexto es sencilla pero limitada; las bases de datos vectoriales permiten búsqueda semántica; los almacenes clave-valor se adaptan a datos estructurados; las bases de datos de grafos capturan relaciones. La mayoría de los sistemas en producción usan un enfoque híbrido.
-
La función de puntuación de Generative Agents (recencia + importancia + relevancia) proporciona un marco principiado para la recuperación de memorias que equilibra múltiples factores. Es un buen punto de partida para cualquier sistema de memoria.
-
La gestión de memoria es esencial. El resumen, el olvido y la priorización evitan que los sistemas de memoria se vuelvan inmanejábles y aseguran que la información más relevante salga a la superficie. Recordar todo no es el objetivo: recordar las cosas correctas sí lo es.
-
La memoria plantea cuestiones éticas sobre privacidad, propiedad de datos y potencial de manipulación. No son preocupaciones teóricas: deben abordarse en cualquier despliegue en producción.
14Referencias
-
Park, J. S., O'Brien, J. C., Caí, C. J., Morris, M. R., Liang, P., & Bernstein, M. S. (2023). Generative Agents: Interactive Simulácra of Human Behavior. Proceedings of the 36th Annual ACM Symposium on User Interface Software and Technology (UIST).
-
Wei, J., Wang, X., Schuurmans, D., Bosma, M., Ichter, B., Xia, F., Chi, E., Le, Q., & Zhou, D. (2022). Chain-of-thought prompting elicits reasoning in large language models. Advances in Neural Information Processing Systems (NeurIPS), 35.
-
Yao, S., Zhao, J., Yu, D., Du, N., Shafran, I., Narasimhan, K., & Cao, Y. (2023). ReAct: Synergizing Reasoning and Acting in Language Models. International Conference on Learning Representations (ICLR).
-
Wang, G., Xie, Y., Jiang, Y., Mandlekar, A., Xiao, C., Zhu, Y., Fan, L., & Anandkumar, A. (2023). Voyager: An Open-Ended Embodied Agent with Large Language Models. arXiv preprint arXiv:2305.16291.
-
Packer, C., Wooders, S., Lin, K., Fang, V., Patil, S. G., Stoica, I., & González, J. E. (2023). MemGPT: Towards LLMs as Operating Systems. arXiv preprint arXiv:2310.08560.
-
Liu, N. F., Lin, K., Hewitt, J., Paranjape, A., Bevilacqua, M., Petroni, F., & Liang, P. (2024). Lost in the Middle: How Language Models Use Long Contexts. Transactions of the Association for Computational Linguistics (TACL), 12.
-
Sumers, T. R., Yao, S., Narasimhan, K., & Griffiths, T. L. (2024). Cognitive Architectures for Language Agents. Transactions on Machine Learning Research (TMLR).
-
Beurer-Kellner, L., Fischer, M., & Vechev, M. (2023). Prompting Is Programming: A Query Language for Large Language Models. Proceedings of the ACM on Programming Languages (PLDI), 7.
Parte de "Agentic AI: Foundations, Architectures, and Applications" (CC BY-SA 4.0).