Cómo Construir un Agente de IA con Memoria Persistente usando Supabase + OpenAI
Precios y datos verificados al 10 de junio de 2026. La información de tarifas de OpenAI y Supabase puede cambiar; se recomienda contrastar con la documentación oficial antes de producción.
Si alguna vez has construido un chatbot o agente con la API de OpenAI, habrás topado con la misma pared tarde o temprano: el modelo no recuerda nada. Cada llamada a la API es una pizarra en blanco. El usuario repite el contexto de conversación anterior, el agente vuelve a pedir datos que ya dio, y la experiencia se degrada a algo que parece un bot de 2019 disfrazado de IA moderna. Construir un agente de IA con memoria persistente no es un lujo, es el requisito mínimo para que un agente sea verdaderamente útil en producción.
El problema va más allá de la experiencia de usuario. Cuando compensas la falta de memoria metiendo todo el historial en el contexto, los costos se disparan. Con GPT-4.1 a $5.00/M tokens de entrada (la tarifa actual para nuevas integraciones), un hilo de conversación de 50 turnos con contexto completo puede costar 10–20 veces más que una consulta puntual. A escala, eso es la diferencia entre un negocio rentable y uno que sangra dinero en llamadas a API. La solución es recuperar solo el contexto relevante cuando se necesita, no embutir toda la historia en cada request.
En este artículo construimos desde cero un agente conversacional con memoria semántica persistente usando Supabase como backend vectorial y OpenAI como motor de embeddings y razonamiento. No hay abstracciones mágicas ni frameworks que oculten la fontanería. Verás exactamente qué SQL se ejecuta, qué llamadas a API se hacen, y cómo todo encaja en un sistema listo para producción.
Por qué la memoria es el talón de Aquiles de los agentes de IA
El costo oculto de los agentes sin memoria
Los LLMs son inherentemente stateless. Cada petición a la API parte de cero: el modelo procesa el array messages[] que le envías, genera una respuesta, y olvida todo. La forma más naïve de darle "memoria" a un agente es concatenar todo el historial de conversación en ese array. Funciona para tres mensajes. Para cincuenta, ya es un problema económico y técnico.
Considera este escenario concreto: un agente de soporte técnico que maneja 1.000 conversaciones activas con un promedio de 30 turnos cada una. Si cada turno envía el historial completo, el input acumulado crece cuadráticamente. Con GPT-4.1 a $5.00/M tokens de entrada, una sesión de 30 turnos con historial completo (~45.000 tokens promedio por request final) cuesta cerca de $0.22 solo en esa última petición. Con recuperación selectiva por similitud semántica, ese mismo agente puede operar con contexto reducido a ~3.000 tokens relevantes, recortando el costo por petición en más del 90%. Datos de producción reales muestran reducciones del 62% en el costo mensual de API al implementar memoria externa frente a context-stuffing.
Memoria en contexto vs. memoria externa
Existen dos estrategias fundamentales:
Memoria en contexto (in-context memory): todo el historial viaja en el array messages[]. Simple de implementar, sin infraestructura adicional. Explota cuando las conversaciones son largas, hay múltiples sesiones, o cuando el agente debe recordar información de semanas atrás. GPT-4.1 soporta hasta 1 millón de tokens de contexto, pero pagas cada token en cada petición.
Memoria externa (out-of-context memory): las conversaciones y hechos relevantes se almacenan en una base de datos. Antes de cada petición, se recuperan los fragmentos más relevantes mediante búsqueda semántica y se inyectan al contexto. El modelo solo ve lo que necesita. Esta es la arquitectura que construiremos, y es la que escala en producción real.
La arquitectura: Supabase + pgvector + OpenAI Embeddings
Por qué Supabase sobre Pinecone, Weaviate o Qdrant
La pregunta inevitable: ¿por qué no usar una base de datos vectorial dedicada? La respuesta es simple — economía y complejidad operativa.
Pinecone cobra $70/mes para 1 millón de vectores en su plan Starter. Supabase Pro cuesta $25/mes e incluye pgvector de forma gratuita en todas las instancias, junto con auth, storage, APIs REST y Realtime en el mismo servicio. Para la mayoría de equipos que ya tienen una base de datos relacional, Supabase permite colocar los embeddings junto a los datos relacionales sin añadir otro vendor a la factura ni otra conexión que mantener.
El rendimiento real de pgvector con índices HNSW (Hierarchical Navigable Small World) es production-grade para la mayoría de workloads: latencia por debajo de 10ms en el percentil 99 para datasets de hasta ~5 millones de vectores en una instancia Pro estándar. Si tienes decenas de millones de vectores o necesitas sub-5ms en p999, entonces sí vale la pena evaluar Pinecone. Para el 95% de los proyectos, no es el caso.
La arquitectura del sistema tiene cuatro capas:
- Capa de embeddings:
text-embedding-3-smallde OpenAI convierte cada mensaje o fragmento de memoria en un vector de 1.536 dimensiones. Costo: $0.02/M tokens — esencialmente gratuito a escala humana. - Capa de almacenamiento: tabla
agent_memoriesen Supabase (Postgres + pgvector) con índice HNSW sobre la columnaembedding. - Capa de recuperación: búsqueda por similitud coseno para extraer los N fragmentos más relevantes al contexto actual.
- Capa de razonamiento: GPT-4.1 Mini ($0.40/M input, $1.60/M output) recibe el contexto recuperado + la consulta actual y genera la respuesta.
El modelo de datos en Postgres
La tabla de memorias es minimalista pero completa:
sql-- Habilitar la extensión pgvector (solo una vez por proyecto) CREATE EXTENSION IF NOT EXISTS vector; -- Tabla principal de memorias del agente CREATE TABLE agent_memories ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id TEXT NOT NULL, -- identifica al usuario dueño de la memoria session_id TEXT, -- sesión de conversación (opcional) content TEXT NOT NULL, -- el texto original del fragmento summary TEXT, -- resumen comprimido (para memorias antiguas) importance INTEGER DEFAULT 5, -- puntuación 1-10 para priorizar en retrieval embedding VECTOR(1536) NOT NULL, -- vector generado con text-embedding-3-small created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Índice HNSW para búsqueda aproximada rápida (recomendado en 2026) CREATE INDEX ON agent_memories USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -- Índice por usuario para filtrar eficientemente antes de buscar CREATE INDEX ON agent_memories (user_id);
Construcción del agente: código completo en Python
Configurar el entorno
bash# Instalar dependencias pip install openai supabase python-dotenv # .env requerido # OPENAI_API_KEY=sk-... # SUPABASE_URL=https://xxxx.supabase.co # SUPABASE_KEY=eyJ...
El agente con memoria persistente
El siguiente código implementa el ciclo completo: guardar memoria → recuperar contexto relevante → generar respuesta.
pythonimport os import json from openai import OpenAI from supabase import create_client, Client from dotenv import load_dotenv load_dotenv() # ─── Inicialización de clientes ─────────────────────────────────────────────── openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) supabase: Client = create_client( os.getenv("SUPABASE_URL"), os.getenv("SUPABASE_KEY") ) # ─── Constantes de configuración ───────────────────────────────────────────── EMBEDDING_MODEL = "text-embedding-3-small" # $0.02/M tokens — más eficiente para RAG CHAT_MODEL = "gpt-4.1-mini" # $0.40/$1.60 por M tokens input/output TOP_K_MEMORIES = 5 # cuántos fragmentos recuperar por consulta IMPORTANCE_MIN = 4 # umbral mínimo para guardar una memoria MAX_TOKENS_RESP = 800 # límite de output por respuesta # ─── Función 1: Generar embedding de un texto ───────────────────────────────── def get_embedding(text: str) -> list[float]: """ Convierte texto en un vector de 1536 dimensiones usando text-embedding-3-small. El resultado se usa tanto para guardar memorias como para buscarlas. """ response = openai_client.embeddings.create( model=EMBEDDING_MODEL, input=text.strip() # eliminamos espacios para no contaminar el vector ) return response.data[0].embedding # lista de 1536 floats # ─── Función 2: Guardar una memoria en Supabase ─────────────────────────────── def save_memory(user_id: str, content: str, session_id: str = None, importance: int = 5): """ Persiste un fragmento de memoria con su embedding en Postgres. 'importance' permite priorizar memorias críticas en el retrieval. """ embedding = get_embedding(content) # generamos el vector del contenido supabase.table("agent_memories").insert({ "user_id": user_id, "session_id": session_id, "content": content, "importance": importance, "embedding": embedding # pgvector acepta listas Python directamente }).execute() # ─── Función 3: Recuperar memorias relevantes por similitud coseno ──────────── def retrieve_memories(user_id: str, query: str, top_k: int = TOP_K_MEMORIES) -> list[dict]: """ Busca los fragmentos de memoria más similares semánticamente a la consulta actual. Usa la función RPC de Supabase que encapsula la búsqueda vectorial con pgvector. """ query_embedding = get_embedding(query) # vectorizamos la consulta del usuario # Llamamos a una función SQL almacenada en Supabase (ver abajo) result = supabase.rpc( "match_memories", # nombre de la función en Postgres { "query_embedding": query_embedding, "match_user_id": user_id, "match_count": top_k, "importance_min": IMPORTANCE_MIN } ).execute() return result.data # lista de dicts con content, importance y similarity # ─── Función 4: Construir el prompt con contexto recuperado ─────────────────── def build_system_prompt(memories: list[dict]) -> str: """ Inyecta las memorias recuperadas como contexto en el system prompt. Solo cargamos lo relevante, no todo el historial — aquí está el ahorro de tokens. """ if not memories: return "Eres un asistente útil y conciso." # Formateamos cada memoria con su puntuación de relevancia memory_block = "\n".join([ f"- [{m['similarity']:.2f}] {m['content']}" for m in memories ]) return f"""Eres un asistente con memoria persistente. Tienes acceso a los siguientes recuerdos relevantes del usuario (ordenados por relevancia): {memory_block} Usa este contexto cuando sea pertinente. No lo menciones explícitamente a menos que el usuario lo haga.""" # ─── Función 5: El ciclo principal del agente ───────────────────────────────── def agent_respond(user_id: str, user_message: str, session_id: str = None) -> str: """ Flujo completo: recuperar contexto → generar respuesta → guardar en memoria. Este es el loop que se ejecuta en cada turno de conversación. """ # PASO 1: Recuperamos las memorias más relevantes para esta consulta relevant_memories = retrieve_memories(user_id, user_message) # PASO 2: Construimos el system prompt con el contexto recuperado system_prompt = build_system_prompt(relevant_memories) # PASO 3: Llamamos a GPT-4.1 Mini con el contexto inyectado response = openai_client.chat.completions.create( model=CHAT_MODEL, max_tokens=MAX_TOKENS_RESP, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message} ] ) agent_reply = response.choices[0].message.content # PASO 4: Guardamos tanto la consulta del usuario como la respuesta del agente # Solo guardamos si parecen tener información útil (evitamos memoria spam) if len(user_message.split()) > 5: # filtramos mensajes muy cortos save_memory(user_id, f"Usuario: {user_message}", session_id, importance=6) if len(agent_reply.split()) > 10: # filtramos respuestas triviales save_memory(user_id, f"Agente: {agent_reply}", session_id, importance=5) return agent_reply # ─── Uso del agente ─────────────────────────────────────────────────────────── if __name__ == "__main__": USER = "usuario_123" # Primera interacción: el agente no sabe nada del usuario r1 = agent_respond(USER, "Estoy construyendo una API REST con FastAPI y PostgreSQL") print("Agente:", r1) # Segunda interacción (puede ser días después): el agente recuerda el contexto r2 = agent_respond(USER, "¿Cómo agrego autenticación JWT a mi proyecto?") print("Agente:", r2) # → El agente sabrá que el proyecto usa FastAPI y PostgreSQL # sin que el usuario lo repita. Eso es memoria persistente real.
La función SQL de búsqueda vectorial
Este bloque SQL debe crearse en el SQL Editor de Supabase. Encapsula la búsqueda con pgvector y aplica filtros de importancia:
sql-- Función RPC para búsqueda semántica con filtros CREATE OR REPLACE FUNCTION match_memories( query_embedding VECTOR(1536), match_user_id TEXT, match_count INT DEFAULT 5, importance_min INT DEFAULT 3 ) RETURNS TABLE ( id UUID, content TEXT, importance INT, similarity FLOAT ) LANGUAGE SQL STABLE AS $$ SELECT id, content, importance, -- Similitud coseno: 1.0 = idéntico, 0.0 = sin relación 1 - (embedding <=> query_embedding) AS similarity FROM agent_memories WHERE user_id = match_user_id AND importance >= importance_min ORDER BY embedding <=> query_embedding -- operador de distancia coseno en pgvector LIMIT match_count; $$;
Optimización de costos y preparación para producción
Reducción de tokens con retrieval selectivo
La diferencia de costo entre un agente naïve y uno con memoria externa bien diseñado es significativa. Con GPT-4.1 Mini como modelo de chat y text-embedding-3-small para embeddings, el costo por conversación de 30 turnos se distribuye así:
| Estrategia | Tokens promedio por request | Costo por 10.000 conversaciones/mes |
|---|---|---|
| Context-stuffing (historial completo) | ~40.000 tokens | ~$640 |
| Memoria externa (top-5 relevantes) | ~3.000 tokens | ~$48 |
| Ahorro | 93% menos tokens | $592/mes |
Estimación con GPT-4.1 Mini a $0.40/M input. Los embeddings cuestan ~$0.01 adicional por 1.000 conversaciones.
Tres tácticas específicas para mantener los costos bajos en producción:
1. Memory summarization progresiva: cada 20 turnos, consolida las memorias más antiguas en un único resumen comprimido y elimina los originales. Reduce el volumen vectorial sin perder información clave. Implementa un job nocturno con un llamado a GPT-4.1 Nano ($0.10/M) para generar estos resúmenes.
2. Filtrado por importancia: no todas las frases merecen ser guardadas. El campo importance (1-10) permite priorizar. Usa un modelo ligero para puntuar automáticamente la relevancia de cada fragmento antes de guardarlo. Los saludos, confirmaciones triviales ("ok, entendido") y frases de relleno no deben contaminar la memoria.
3. Batch API para embeddings en diferido: cuando proceses grandes volúmenes de conversaciones históricas o hagas re-indexación, usa la Batch API de OpenAI para obtener el 50% de descuento en embeddings ($0.01/M tokens en lugar de $0.02/M).
Seguridad y aislamiento de datos con Row Level Security
En Supabase, habilita RLS (Row Level Security) sobre la tabla agent_memories para garantizar que cada usuario solo acceda a sus propias memorias:
sql-- Habilitar RLS en la tabla ALTER TABLE agent_memories ENABLE ROW LEVEL SECURITY; -- Política: cada usuario solo ve sus propias memorias CREATE POLICY "usuarios_ven_sus_memorias" ON agent_memories FOR ALL USING (user_id = auth.uid()::text);
Si consumes la tabla desde el backend con el service_role key (como en el código Python anterior), el RLS no aplica. Si expones la tabla desde el cliente (frontend o móvil), RLS es obligatorio.
Observabilidad en producción
Para monitorear la salud del sistema de memoria en producción, añade logging de métricas clave en cada ciclo del agente: latencia del retrieval, número de memorias recuperadas, tokens enviados vs. tokens máximos permitidos, y puntuación de similaridad promedio. Una similaridad promedio por debajo de 0.65 suele indicar que las memorias guardadas no están siendo lo suficientemente específicas, señal de que deberías revisar la estrategia de chunking o los umbrales de importancia.
Conclusión: de agente stateless a sistema que aprende
Lo que hemos construido es más que un chatbot con historial. Es la base de un agente de IA que aprende del contexto de cada usuario de forma acumulativa, sin reentrenamiento, sin costos prohibitivos y sin depender de un vendor de base de datos vectorial de $70/mes.
El stack Supabase + pgvector + OpenAI Embeddings es, a junio de 2026, la combinación más equilibrada entre costo, simplicidad operativa y rendimiento para la mayoría de equipos de producto. pgvector incluido gratis en todos los planes de Supabase, text-embedding-3-small a $0.02/M tokens, y GPT-4.1 Mini a $0.40/M de entrada forman un trío que permite operar un agente con memoria real por menos de $50/mes a escala de decenas de miles de conversaciones mensuales.
Los siguientes pasos naturales desde aquí son: añadir memoria episódica (hechos concretos sobre el usuario, separados del historial conversacional), implementar grafo de relaciones entre memorias usando las capacidades relacionales de Postgres, y explorar hybrid retrieval combinando similitud semántica (pgvector) con búsqueda full-text (GIN index en Postgres) para capturar tanto contexto semántico como términos exactos.
Si este artículo te fue útil, compártelo con tu equipo y deja un comentario con el caso de uso que estás construyendo. Si quieres profundizar en la capa de evaluación del retrieval o en la arquitectura multi-agente con memoria compartida, escríbeme — son los próximos temas en la agenda.
Preguntas frecuentes
Fredo
Ingeniero de Sistemas · Especialista en costos de IA
"Ingeniero de sistemas especializado en arquitectura de costos para APIs de IA. Analiza y compara modelos de lenguaje en producción para ayudar a equipos de desarrollo latinoamericanos a optimizar su infraestructura de IA sin destruir sus márgenes."