"Agregar memoria" son en realidad tres problemas distintos bajo un mismo nombre. Esta guía separa la memoria de trabajo, la episódica y la semántica, guarda cada una en el store más barato que le sirva, y te da un schema concreto en Supabase más una política de lectura y escritura para que recuperar salga barato, exacto y relevante, para que tu agente nunca le mande un paquete a la dirección del año pasado.

En resumen
- La memoria no es una sola función. La de trabajo, la episódica y la semántica son tres stores con retención distinta, retrieval distinto y maneras distintas de fallar, así que diséñalas por separado.
- La búsqueda vectorial te devuelve lo parecido, no lo verdadero. Los datos que tienen que estar exactos (una dirección, un plan, un opt-out) van en almacenamiento clave-valor exacto, nunca en un match por similitud.
- Lo difícil no es guardar los recuerdos; es decidir qué escribir, qué recuperar y qué dejar que se vaya apagando. Una política de escritura y un presupuesto de retrieval pesan más que la base de datos.
- En un solo proyecto de Supabase te corren las tres capas: filas de Postgres para lo episódico, pgvector para el recall semántico y una tabla sencilla para los datos canónicos.
- El retrieval es un presupuesto, no un vaciadero. Fija un techo de tokens, llénalo por prioridad (primero los datos, luego los episodios relevantes) y déjale espacio al modelo para pensar.
Pídele a tres ingenieros que le "agreguen memoria" a un agente y vas a terminar con tres sistemas distintos, porque "memoria" esconde tres problemas separados: mantener coherente la conversación actual, recordar qué pasó en sesiones anteriores y conocer datos duraderos sobre el usuario y el dominio. Si los juntas, construyes el anti-patrón clásico, todo metido en un solo vector store, que sale lento, caro y, sin que te des cuenta, equivocado justo en los datos que más importan. Esta guía separa las tres capas, guarda cada una en el store más barato que le sirva, y te da un schema concreto en Supabase más la política de lectura y escritura que de verdad decide si tu agente se siente como que recuerda o como que está adivinando.
El encuadre que vuelve obvio todo lo demás: la memoria no es una base de datos que le atornillas al agente. Es un presupuesto que gastas. Cada token de contexto recuperado le quita espacio al modelo para razonar, así que la ingeniería de verdad está en decidir qué vale la pena llevar al siguiente turno y qué soltar.
01 · Requisitos y el modelo de tres capas
Antes de tocar SQL, deja claro el modelo, porque condiciona cada decisión de almacenamiento y retrieval que viene después. Hay tres tipos de memoria, y son problemas genuinamente distintos:
- Memoria de trabajo: la conversación actual. Vive en la ventana de contexto y desaparece cuando termina la sesión. Su única forma de fallar es desbordarse.
- Memoria episódica: qué pasó en sesiones anteriores. "El martes pasado el usuario preguntó por reembolsos y lo resolvimos." Con marca de tiempo, narrativa, recuperada por recencia y relevancia.
- Memoria semántica: datos duraderos sobre el usuario o el dominio. "Esta cuenta está en el plan Pro." "El usuario prefiere unidades métricas." Atemporales hasta que cambian, y tienen que estar exactos.
Necesitas tener un par de cosas listas antes de escribir código:
- Un proyecto de Supabase (o cualquier Postgres 14+ con la extensión vector). El tier gratis sobra para armar esto.
- Un modelo de embeddings y su dimensión de salida exacta, por ejemplo 1536. Ese número queda fijo en el tipo de una columna y no lo puedes cambiar después sin volver a generar todos los embeddings.
- Las API keys del modelo de embeddings y de Claude, en variables de entorno, jamás en la base de datos ni en el bundle del cliente.
- Una respuesta clara a una pregunta por cada dato que vayas a guardar: ¿esto tiene que estar exacto, o solo ser relevante? Esa sola pregunta decide a dónde va cada escritura.
Importante
Por cada pieza de información, decide si tiene que ser exacta o nada más relevante. Una dirección de envío, un plan de suscripción, un flag de consentimiento: esos tienen que estar exactos, así que van en almacenamiento exacto. El resumen de una conversación pasada puede ser difuso, así que puede vivir en vectores. Si aciertas a dónde mandar cada cosa, la mayoría de los bugs de "el agente se olvidó / el agente metió la pata" ni siquiera llegan a pasar.
02 · Memoria de trabajo: resume, no recortes
La memoria de trabajo es la conversación que el modelo tiene enfrente. El arreglo ingenuo cuando se desborda es descartar los turnos más viejos, y así es justo como un agente se olvida del límite que le diste hace cinco mensajes. El arreglo más barato y mejor es resumir los turnos viejos en un brief continuo y mantener ese brief en contexto en vez del transcript crudo.
El patrón es una compactación progresiva: deja los últimos turnos tal cual, y una vez que el transcript pasa cierto umbral de tokens, le pides al modelo que pliegue todo lo más viejo en un brief compacto que conserve decisiones, límites y preguntas abiertas, y luego descartas los turnos crudos que ese brief reemplazó.
Lo que el brief tiene que conservar, en orden de prioridad:
- Decisiones y compromisos, como "acordamos usar Postgres", "la fecha límite es el viernes".
- Límites duros, como "tiene que quedar por debajo de 200ms", "nunca le escribas un correo al cliente".
- Hilos abiertos: qué sigue sin resolverse y esperando.
Todo lo demás (cortesías, contexto repetido, callejones sin salida) se puede perder sin problema. El arte está en el prompt que escribe el brief, no en el almacenamiento; aquí no hay nada que persistir más allá de la sesión.
Consejo
Deja siempre los dos o tres turnos más recientes en crudo. Los resúmenes pierden la forma exacta en que el usuario acaba de decir algo, y el turno inmediato es justo donde más importa esa forma. Compacta lo viejo, conserva lo nuevo.
03 · Memoria episódica: filas con tiempo y un resumen
La memoria episódica es el registro de sesiones pasadas: qué se habló, qué se decidió, cómo terminó. Guárdala como filas de Postgres a secas, una fila por sesión o por evento que valga la pena, con una marca de tiempo y un resumen corto. Aquí no entran los vectores primero; este es un lugar para recencia y filtrado estructurado, con recall semántico montado encima solo cuando la recencia por sí sola no alcanza.
Una fila por episodio, escrita al cerrar la sesión por el mismo resumidor que corrió la compactación de tu memoria de trabajo:
create table episodic_memory (
id bigint generated always as identity primary key,
user_id uuid not null,
summary text not null, -- "Resolvió duda de reembolso; prometió seguimiento el viernes"
outcome text, -- "resolved" | "escalated" | "open"
embedding vector(1536), -- opcional, para recall semántico
created_at timestamptz not null default now()
);
create index on episodic_memory (user_id, created_at desc);
El retrieval va por recencia primero: "las últimas cinco sesiones con este usuario" es un solo query indexado y resuelve la mayoría de los casos. Agrega la columna de embedding para también poder pedir "sesiones pasadas parecidas en sentido a lo que está preguntando ahora" cuando la conversación hace referencia a algo viejo. El índice sobre (user_id, created_at desc) es lo que mantiene rápido el caso común; el vector es el camino de excepción, no el default.
La disciplina que vuelve útil la memoria episódica es escribir buenos resúmenes. Una fila que dice "el usuario hizo unas preguntas" es ruido. Una fila que dice "usuario en plan Pro pidió bajar de plan; le expliqué el prorrateo; decidió quedarse" es un recuerdo que de verdad vale la pena tener.
04 · Memoria semántica: vectores para recall, almacenamiento exacto para la verdad
Esta es la capa que todos agarran de primero y en la que más se equivocan. La memoria semántica guarda datos duraderos, y se parte limpiamente en dos stores según la pregunta de la sección 01: ¿exacto, o nada más relevante?
Datos que tienen que estar exactos, usa clave-valor exacto
La dirección actual de un usuario, su plan, su preferencia de idioma, si se dio de baja del correo: esos nunca se deben aproximar. Guárdalos en una tabla sencilla con clave por usuario y nombre del dato, y léelos con un lookup exacto:
create table user_facts (
user_id uuid not null,
key text not null, -- "shipping_address" | "plan" | "email_optout"
value text not null,
updated_at timestamptz not null default now(),
primary key (user_id, key)
);
Leer un dato es un lookup por primary key que devuelve el único valor verdadero. Actualizarlo sobrescribe la fila en su lugar, así que el agente siempre ve el estado actual, no la conjetura histórica más cercana.
Atención
Nunca resuelvas por similitud vectorial un dato que tiene que estar exacto. Si buscas la dirección de envío actual de un usuario por "el texto guardado más parecido", te va a devolver el apartamento del año pasado sin avisar, y el paquete se va para allá. La búsqueda vectorial devuelve lo parecido, no lo verdadero. Reserva los vectores para "encontrar contexto relacionado"; usa almacenamiento exacto para todo lo que tenga que estar bien.
Conocimiento que solo necesita ser relevante, usa vectores
Preferencias dichas al pasar, notas del dominio, "cosas que al usuario le suelen importar", conocimiento difuso que se va acumulando, es donde pgvector se gana el sueldo. Genera el embedding de cada nota y recupéralas por similitud con el turno actual. Es la misma maquinaria de vecino más cercano que usa RAG, pero apuntada a datos sobre el usuario en vez de a un corpus de documentos.
La división es justo el punto: el almacenamiento exacto responde "qué es verdad" y los vectores responden "qué podría ser relevante". Si los mezclas, te queda un agente que afirma con toda seguridad cosas equivocadas que tranquilamente debería haber consultado.
05 · Ejemplo práctico: armar el contexto dentro de un presupuesto
Ahora conecta las capas en un solo paso de retrieval. El error aquí es volcar al prompt todo lo que encuentres. El retrieval es un presupuesto: fija un techo de tokens para la memoria, llénalo por prioridad y para. Los datos exactos son baratos y sostienen todo lo demás, así que van primero; los episodios relevantes y las notas semánticas llenan lo que quede.
async function buildContext(userId: string, userTurn: string, tokenBudget = 1500) {
// 1. Primero los hechos exactos — pequeños, obligatorios, tienen que estar correctos.
const facts = await getUserFacts(userId) // reads por primary key
// 2. Episodios recientes — recencia primero, baratos y casi siempre suficientes.
const recent = await getRecentEpisodes(userId, 5)
// 3. Recall semántico — solo episodios/notas relevantes para este turno.
const related = await searchSemantic(userId, userTurn, 4)
// Llena el presupuesto por prioridad; nunca lo excedas.
return packByPriority(
[factsBlock(facts), episodesBlock(recent), notesBlock(related)],
tokenBudget,
)
}
El orden es la política. Los datos entran enteros porque son pequeños y la corrección depende de ellos. Los episodios vienen después porque la recencia resuelve la mayoría de las preguntas. Las notas semánticas llenan el presupuesto que sobre. Cuando el presupuesto está apretado, el primer bloque que se recorta es el de menor prioridad, y el modelo igual se queda con sus datos y su historial reciente.
Errores comunes
- Todo en un solo vector store. El anti-patrón estrella. Los datos que tienen que estar exactos terminan emparejados de forma difusa, y el agente manda el paquete a la dirección equivocada. Mejor enruta por exacto vs. relevante.
- Recortar la memoria de trabajo. Descartar los turnos viejos elimina sin avisar los límites que diste. Resume en un brief continuo; deja los últimos turnos en crudo.
- Resúmenes episódicos vagos. "El usuario hizo preguntas" es ruido que ensucia el recall. Escribe resúmenes que una sesión futura de verdad querría tener.
- Retrieval sin tope. Echarle veinte matches al prompt le quita espacio al razonamiento e infla el costo. Fija un presupuesto y llénalo por prioridad.
- Nunca dejar que la memoria decaiga. Los episodios viejos se acumulan y diluyen la relevancia. Vence o bájale el peso a las filas viejas que nunca se recuperan.
06 · Decaimiento, corrección y saber cuándo esto ya alcanza
La memoria que solo crece a la larga te juega en contra: mientras más filas viejas cargues, más ruidoso se vuelve cada retrieval. Mete el decaimiento desde el arranque. Bájale el peso a los episodios que nunca se recuperan, vence las filas de poco valor después de cierta ventana y, esto es clave, haz que corregir salga barato. Cuando un dato cambia, sobrescribe la fila exacta; cuando un episodio queda superado, márcalo en vez de dejar que afloren las dos versiones.
Sé honesto también sobre el techo. Este diseño de tres stores en un solo proyecto de Supabase le sirve de sobra a productos reales: Postgres para lo episódico y los datos exactos, pgvector para el recall semántico. Solo le ves los límites cuando el índice semántico ya no cabe en memoria, o cuando necesitas búsqueda híbrida pesada con reranking sobre millones de notas, y aun así la migración es mecánica, porque cada capa ya está separada y cada fila ya carga sus claves. Arranca aquí; la mayoría de los agentes nunca lo superan.
La memoria no es una función que instalas; es un conjunto de decisiones sobre qué guardar, qué consultar y qué olvidar. Separa las tres capas, enruta cada dato según si tiene que estar exacto o nada más ser relevante, gasta el retrieval como un presupuesto, y tu agente deja de adivinar y empieza a recordar de verdad, sobre infraestructura que ya tienes corriendo.
Puntos clave
- "Agregar memoria" son tres problemas: de trabajo (la conversación), episódica (sesiones pasadas) y semántica (datos duraderos). Diseña cada una por separado.
- Enruta cada dato con una sola pregunta: ¿tiene que estar exacto o nada más ser relevante? Lo exacto va en almacenamiento clave-valor exacto; lo relevante va en vectores.
- Compacta la memoria de trabajo en un brief continuo en vez de recortar; escribe la episódica como filas por recencia con buenos resúmenes.
- El retrieval es un presupuesto de tokens que llenas por prioridad (primero los datos, luego los episodios recientes, al final las notas semánticas), no un volcado de todos los matches.
- Mete decaimiento y corrección barata desde el día uno; las tres capas caben en un solo Postgres de Supabase, y el diseño migra de forma mecánica si algún día se te queda corto.
Preguntas frecuentes
¿De verdad necesito tres stores? ¿No puede una sola base de datos vectorial encargarse de todo?
Sí puedes meter todo en un solo vector store, y es justamente el error más común. La búsqueda vectorial devuelve lo parecido, no lo verdadero, así que un dato que tiene que ser exacto (una dirección, un plan) termina emparejado de forma difusa y el agente usa el valor equivocado con toda seguridad. Los tres stores no son tres bases de datos que tengas que correr; en Supabase son tres tablas en un mismo Postgres. La división es por corrección, no por infraestructura.
¿Cuándo escribo la memoria episódica, en cada turno o al final de la sesión?
Al final de la sesión, o en un corte que tenga sentido (un ticket resuelto, una tarea completada). Escribir una fila por turno te inunda la tabla de ruido de poco valor que después le ensucia el recall. Deja que el mismo resumidor que compacta la memoria de trabajo produzca un buen resumen del episodio cuando la sesión cierre.
¿Cómo evito que el retrieval de memoria se coma toda mi ventana de contexto?
Trata el retrieval como un presupuesto fijo de tokens y llénalo por prioridad. Los datos exactos entran primero porque son pequeños y sostienen todo lo demás, luego los episodios recientes, y al final las notas semánticas. Cuando se acaba el presupuesto, se recorta el bloque de menor prioridad, nunca los datos. Un presupuesto inicial típico es de mil a dos mil tokens para la memoria, y dejas el resto para que el modelo razone.
¿Qué pasa cuando un dato guardado cambia, por ejemplo cuando el usuario se muda?
Como los datos exactos viven en una tabla clave-valor con clave por usuario y nombre del dato, sobrescribes la fila en su lugar y el agente ve el valor nuevo de inmediato. Esa es justo la razón por la que esos datos no van en vectores: en un vector store la dirección vieja se queda dando vueltas como un match parecido. Con almacenamiento exacto hay un solo valor actual, y actualizarlo es una sola escritura.
¿Necesito pgvector de entrada, o puedo empezar sin él?
Puedes lanzar un agente útil con solo compactación de memoria de trabajo, filas episódicas por recencia y una tabla de datos exactos, sin vectores. Agrega pgvector cuando la recencia deje de alcanzar y el agente necesite recordar algo semánticamente relacionado de hace rato. Los vectores son el camino de recall para el conocimiento difuso, no un requisito para tener memoria.
¿Cómo evito que los recuerdos viejos degraden el retrieval con el tiempo?
Mete el decaimiento desde el arranque. Bájale el peso a los episodios que nunca se recuperan, vence las filas de poco valor después de cierta ventana y marca los episodios superados en vez de dejar dos versiones aflorando. La memoria que solo crece se vuelve más ruidosa con cada fila, así que olvidar a propósito es parte del diseño, no algo que dejas para después.
¿Prefieres que lo hagamos por ti?
Esto mismo lo construimos para negocios como el tuyo. La primera conversación es gratis y sin compromiso.
Escríbenos por WhatsAppPrimera conversación gratis. Te responde el fundador.
Recursos relacionados

Arma un índice RAG en Supabase con pgvector
RAG es búsqueda por vecino más cercano sobre texto que convertiste en embeddings, más un modelo que lee los resultados. Esta guía monta todo sobre Postgres pelado (schema, chunking, un índice HNSW, una función de búsqueda detrás de RLS y la llamada de retrieval a Claude) y te dice sin rodeos dónde pgvector se queda corto.

Memoria y RAG para agentes que no olvidan: capas, recuperación y pruebas
Casi toda la memoria de agentes es una base de datos vectorial pegada con cinta adhesiva a un bucle de chat. Esta es una nota de campo sobre tratar la memoria como un sistema por capas, recuperar solo lo que cambia la respuesta, compactar sin amnesia y demostrar que algo de eso de verdad sirvió.

Agrega caché de prompts a una llamada de la API de Claude
Marca un prefijo estable como cacheable y bajas latencia y costo en prompts grandes que se repiten. El detalle está en el modelo de match por prefijo, que decide si consigues un hit o terminas pagando precio completo sin enterarte.