IAPythonSupabaseOpenAIAgentes

Cómo Construir un Agente de IA con Memoria Persistente usando Supabase + OpenAI

Fredo
Publicado10 de junio de 2026
·13 min de lectura
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:

  1. Capa de embeddings: text-embedding-3-small de 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.
  2. Capa de almacenamiento: tabla agent_memories en Supabase (Postgres + pgvector) con índice HNSW sobre la columna embedding.
  3. Capa de recuperación: búsqueda por similitud coseno para extraer los N fragmentos más relevantes al contexto actual.
  4. 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.

python
import 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í:

EstrategiaTokens promedio por requestCosto por 10.000 conversaciones/mes
Context-stuffing (historial completo)~40.000 tokens~$640
Memoria externa (top-5 relevantes)~3.000 tokens~$48
Ahorro93% 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

La memoria persistente en agentes de IA es la capacidad de recordar información entre sesiones distintas, almacenando conversaciones y contexto en una base de datos externa. A diferencia del contexto en ventana, que se borra con cada sesión, la memoria externa persiste indefinidamente y se recupera selectivamente según relevancia.
Supabase incluye pgvector de forma gratuita en todos sus planes. El plan Free cubre 500MB de almacenamiento y es suficiente para prototipos. El plan Pro cuesta $25/mes e incluye capacidad para millones de vectores sin cargo adicional, frente a los $70/mes de Pinecone para un volumen similar.
Usa pgvector en Supabase cuando ya tienes datos relacionales en Postgres, tu dataset es menor a 5 millones de vectores, o quieres minimizar la cantidad de servicios en tu stack. Elige Pinecone o Weaviate si necesitas latencia sub-5ms en el percentil 99.9 o si manejas decenas de millones de vectores con consultas de muy alta concurrencia.
F

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."