Cómo hacer un RAG real con Next.js, PostgreSQL, pgvector y OpenRouter

Si has escuchado mucho la palabra RAG (Retrieval-Augmented Generation) y crees que es algo complicado, la buena noticia es que puedes construir uno con tecnologías que probablemente ya conoces: Next.js, PostgreSQL con la extensión pgvector y una API de IA a través de OpenRouter.

En esta guía vamos a crear Extracto, una aplicación que:

  1. Permite subir PDFs o imágenes (facturas, recibos, contratos) y ver una vista previa.
  2. Extrae los datos con un modelo de visión y los muestra en un formulario editable.
  3. Al confirmar, guarda los datos estructurados en tablas y genera embeddings en pgvector.
  4. Ofrece un chat para hacer preguntas sobre los documentos, citando de qué archivo sale cada respuesta.
  5. Queda desplegada en producción con un dominio público.

Todo el desarrollo se hace pidiéndole las cosas a un agente de código (en el video uso Claude Code, pero los pasos son los mismos con Codex, Cursor u OpenCode). El objetivo no es escribir cada línea a mano, sino entender qué hace cada pieza para poder pedirla bien y luego extenderla.

📺 Este artículo acompaña al video "Cómo hacer un RAG real con Next.js, PG Vector y Open Router" del canal Fazt.

Tabla de contenidos

¿Qué es un RAG y qué vamos a construir?

Un RAG (Retrieval-Augmented Generation, o generación aumentada por recuperación) es un patrón en el que, antes de que el modelo de IA responda, buscamos información relevante en nuestros propios datos y se la pasamos como contexto. Así el modelo responde sobre tus documentos y no solo con lo que aprendió en su entrenamiento.

En nuestro caso, el flujo final se ve así:

  • Subes una factura en PDF → la IA la lee y rellena un formulario.
  • Revisas y corriges los campos → confirmas y se guarda.
  • Preguntas "¿cuánto suman todas las facturas?" → la app responde con el total y te dice de qué documentos obtuvo los datos.

Arquitectura del proyecto

Pieza Tecnología usada ¿Se puede cambiar?
Frontend Next.js Sí: HTML simple, Astro, Vite, etc.
Backend API Routes de Next.js Sí: Python, Go, cualquier lenguaje
Base de datos PostgreSQL + pgvector Sí: otras bases con soporte de vectores
Modelo de visión (extracción) DeepSeek vía OpenRouter Sí: cualquier modelo multimodal
Modelo de embeddings Vía OpenRouter
Despliegue Plataforma con servidor MCP (Seenode en el video)

Next.js hace de frontend y backend en el mismo proyecto, lo que simplifica mucho el ejemplo. Pero lo importante del día de hoy no es la interfaz, sino lo que pasa en el backend y en la base de datos.

¿Qué es pgvector y por qué lo necesitamos?

PostgreSQL ya guarda texto, números, fechas e incluso archivos. pgvector es una extensión de código abierto que añade un nuevo tipo de dato: vector, junto con búsqueda por similitud (distancia L2, producto interno y distancia coseno, entre otras).

¿Por qué hace falta? Porque para que un modelo de IA "entienda" el significado de textos como "PC" o "mouse", primero los convertimos en embeddings: listas de números que representan su significado. Textos con significados parecidos quedan "cerca" entre sí. Esa conversión la hace otro modelo de IA (un modelo de embeddings), no una librería tradicional.

Un ejemplo mínimo de cómo se ve esto en SQL:

-- Habilitar la extensión
CREATE EXTENSION IF NOT EXISTS vector;

-- Tabla de fragmentos de documentos con su embedding
CREATE TABLE document_chunks (
  id          BIGSERIAL PRIMARY KEY,
  document_id BIGINT NOT NULL,
  content     TEXT NOT NULL,
  embedding   VECTOR(1536) -- la dimensión depende del modelo de embeddings
);

-- Buscar los 5 fragmentos más parecidos a la pregunta (distancia coseno)
SELECT id, document_id, content
FROM document_chunks
ORDER BY embedding <=> $1
LIMIT 5;

💡 PostgreSQL no es la única opción. MySQL, SQL Server, Oracle y hasta SQLite (con extensiones como sqlite-vec) ya ofrecen formas de guardar y buscar vectores. Revisa la documentación de tu base de datos, porque el soporte varía según la versión.

Paso 1: Crear el proyecto con Next.js, PostgreSQL y pgvector

Creamos una carpeta vacía llamada extracto, entramos desde la terminal y lanzamos el agente.

En el video uso Claude Code con el flag --dangerously-skip-permissions para no aprobar cada comando manualmente. Úsalo con cuidado: le das al agente permiso para ejecutar cualquier cosa, incluso instalar programas. Si prefieres algo más seguro, usa los modos de permisos más restrictivos que ofrece la herramienta o trabaja dentro de un contenedor.

La clave es ir por fases y no pedir toda la aplicación en un solo prompt. El primer prompt fue algo así:

Crea una app llamada "extracto" con Next.js y PostgreSQL como base de datos,
con la extensión pgvector (usando Docker para crear el contenedor).
Crea una sola página con un área para subir imágenes o PDF
y mostrar una vista previa de los archivos.

Si no usas Docker, simplemente quita esa parte del prompt. El resultado es una página con drag & drop y previsualización del archivo. Todavía no extrae nada.

Paso 2: Extraer datos con un modelo de visión (OCR con IA)

El siguiente prompt:

Envía el archivo subido a un LLM con visión. Que detecte si es una factura,
un recibo o un contrato, y que extraiga los datos en formato JSON.
Muestra ese JSON en pantalla.

A este proceso se le suele llamar OCR (Optical Character Recognition), aunque técnicamente aquí usamos un modelo multimodal: no solo reconoce caracteres, también entiende la estructura del documento y la reorganiza en un JSON (un formato de datos estructurado que podemos guardar en una base de datos o enviar a otro sistema).

¿Modelo multimodal o OCR especializado?

Existen servicios especializados como Azure AI Document Intelligence, Google Document AI o Amazon Textract. Suelen ser más fiables con campos numéricos (importes, totales), por eso en producción es común combinarlos: el OCR especializado extrae los números y el LLM reorganiza, resume o interpreta el contenido.

Para este ejemplo usamos un modelo multimodal por simplicidad.

Tip: preguntas paralelas con /btw en Claude Code

Mientras el agente trabaja, puedes escribir /btw (by the way) para hacer una pregunta rápida sin interrumpir la tarea principal, por ejemplo: "¿qué modelos con visión puedo usar aparte de los de Anthropic?". La respuesta aparece en una ventana aparte y no se añade al historial de la conversación.

¿Qué modelo elegir?

Esto cambia constantemente, así que investiga antes de elegir. Una buena referencia es Artificial Analysis, donde puedes comparar modelos por inteligencia, capacidades de visión y precio.

En el video elijo DeepSeek por ser de los más baratos. Un detalle importante: el modelo base DeepSeek V4 Flash solo acepta texto; para leer imágenes necesitas una variante con visión, como DeepSeek V4 Flash Vision Exp o el más reciente DeepSeek V4.1 Flash, que acepta texto e imágenes. Revisa en la ficha del modelo en OpenRouter que tenga imagen como entrada antes de usarlo.

Paso 3: Conectar OpenRouter

OpenRouter es un gateway de modelos de IA: con una sola API key accedes a modelos de OpenAI, Google, Anthropic, DeepSeek, Mistral y muchos más, con una API compatible con la de OpenAI.

Sobre los costos, conviene ser precisos: según su FAQ, OpenRouter no añade margen sobre el precio de inferencia de los proveedores, pero sí cobra una comisión al comprar créditos. Es decir, la ventaja no es ahorrar dinero, sino la comodidad: una sola clave, una sola factura y la posibilidad de combinar distintos modelos (uno para extraer, otro para embeddings, otro para generar texto o imágenes).

Para crear tu clave:

  1. Crea una cuenta en openrouter.ai.
  2. Recarga créditos (vas a pagar por uso de la API).
  3. Ve a Keys y crea una nueva clave. Puedes ponerle fecha de expiración y un límite de crédito (por ejemplo, gastar solo $5 de los $10 que recargaste).
  4. Pásale la clave al agente para que la configure en tu archivo .env.

⚠️ Seguridad: nunca subas tus claves a GitHub. Asegúrate de que .env esté en tu .gitignore y, si alguna vez pegas una clave en una sesión o se filtra, rótala.

Un punto que mucha gente confunde: pagar Claude Code o Codex cubre el desarrollo de la app. Pero cuando tu aplicación está en producción, cada petición a la IA se paga por consumo. Por eso importa tanto elegir el modelo adecuado.

En el video, el agente incluso hizo pruebas con un PNG, un JPG y un PDF y estimó el tiempo y el costo por documento. La extracción de una factura tardó unos 10 segundos y devolvió un JSON con el tipo de documento, un nivel de confianza, el idioma, las líneas, subtotales y totales.

Paso 4: Validar los datos y crear tests

Hacer vibe coding está bien para prototipar, pero si vas a llevar esto a producción necesitas validar. El prompt:

Define esquemas en Zod para cada tipo de documento. Valida las fechas,
que subtotal + impuestos = total, y que los campos obligatorios
no estén vacíos.

Zod es una librería de validación para TypeScript que además te genera los tipos automáticamente.

Y luego:

Crea tests comprobando cada caso. Ejemplo: se sube un PDF exitoso,
se sube un PDF incompleto.

El agente generó tests para casos como:

  • Imagen válida.
  • Tipo de archivo no permitido.
  • Archivo de más de 20 MB.
  • PDF incompleto.
  • Respuesta del modelo que no es un JSON válido.
  • Error 401 de OpenRouter (clave inválida).

Esta sección es clave: hace que la aplicación ya haya pasado por los casos problemáticos antes de desplegarla.

Paso 5: Guardar en PostgreSQL y generar embeddings

Ahora sí, la integración con la base de datos:

Al confirmar, guarda los datos estructurados en tablas de PostgreSQL.
Los textos del documento (títulos, nombres, direcciones, etc.)
conviértelos en embeddings y guárdalos en pgvector.
Usa OpenRouter también para el modelo de embeddings.

El resultado:

  • Tablas tradicionales: documentos, líneas (concepto, cantidad, importe), contratos… Diseño de base de datos de toda la vida.
  • Tabla de chunks (document_chunks): el texto del documento se divide en fragmentos de ~800 caracteres (cortando por saltos de línea), cada fragmento se convierte en un embedding y se guarda en una columna vector.

Un detalle importante: solo se generan embeddings de los documentos confirmados por el usuario. Si embebes todo lo que el modelo extrae, sin revisión, "contaminas" la base de conocimiento con posibles errores.

Paso 6: El RAG — búsqueda semántica con citas

Agrega un chat que responda preguntas usando búsqueda semántica
sobre los embeddings, y que cite el documento de origen de cada respuesta.

El flujo del RAG es:

  1. La pregunta del usuario se convierte en un embedding.
  2. Se buscan en pgvector los fragmentos más parecidos.
  3. Esos fragmentos se envían al modelo como contexto.
  4. El modelo responde solo con esa información y cita el documento de origen.

La búsqueda semántica compara significados, no palabras exactas. Por ejemplo, al preguntar "¿qué dice el contrato sobre la fianza?", el sistema encuentra el fragmento relevante aunque el texto no use exactamente esas palabras.

Evitar que el RAG invente respuestas

La prueba más importante de un RAG es preguntarle algo que no está en los documentos (en el video: "¿cuál es la penalización por retraso?"). La respuesta correcta es "no encuentro esa información", no un dato inventado.

Una aclaración técnica: es habitual usar una temperatura baja (cercana a 0) en un RAG, pero la temperatura no elimina las alucinaciones. La temperatura controla qué tan aleatoria o determinista es la salida; que el modelo responda "no lo sé" depende sobre todo de:

  • Un buen prompt de sistema ("responde solo con el contexto proporcionado; si no está, dilo").
  • Una recuperación que traiga los fragmentos correctos.
  • Citas obligatorias para poder verificar cada respuesta.
  • Tests con preguntas cuya respuesta no existe en los datos.

Paso 7: Cuándo usar SQL en lugar de embeddings

Este es, quizá, el aprendizaje más valioso del video. Cuando pregunté "¿cuál es la suma total de todos los documentos?", el modelo intentó calcularlo a partir de los fragmentos de texto. Eso tiene dos problemas:

  • Te cobran tokens por hacer una suma.
  • El resultado puede ser inexacto, porque trabaja sobre texto extraído por OCR que pudo variar o haber sido editado.

La solución:

Si la pregunta es de cálculos u operaciones numéricas, usa una consulta SQL
sobre las tablas en lugar de los embeddings.

La regla general:

Tipo de pregunta Qué usar
"¿Cuánto suman las facturas?", "¿cuántos documentos hay?", "precio unitario de X" SQL sobre las tablas estructuradas
Buscar un texto exacto SQL (LIKE, búsqueda de texto completo)
"¿Qué dice el contrato sobre…?", "¿cuánto costó el diseño de la página?" Búsqueda semántica (embeddings + LLM)

Un RAG bien hecho no le delega todo a la IA: usa el modelo para entender textos y la base de datos para lo que hace mejor, calcular.

Paso 8: Desplegar a producción con un MCP

Para producción usé Seenode, una plataforma que permite tener el servicio web y la base de datos PostgreSQL en el mismo proyecto. Lo interesante es que ofrece un servidor MCP (Model Context Protocol), lo que permite que tu agente de código (Claude Code, Codex, Cursor, VS Code…) cree y configure el despliegue por ti.

El proceso fue:

  1. Crear una cuenta (recomiendo registrarse con GitHub para conectar los repositorios directamente).
  2. Copiar el comando de conexión MCP para tu agente desde el panel.
  3. Reiniciar el agente (en Claude Code, claude -c o claude --continue para retomar la sesión anterior).
  4. Autenticar el MCP desde /mcp y autorizar tu espacio de trabajo.
  5. Pedir: "Despliega todo el proyecto en Seenode".

El agente creó el servicio de Next.js y la base de datos, configuró las variables de entorno y los conectó. El primer despliegue tardó unos 20 minutos (incluyendo algunas correcciones que hizo el propio agente); las actualizaciones posteriores tardaron alrededor de 3 minutos. La base de datos ya incluía pgvector, así que no hubo que configurar nada extra.

Los precios que se muestran en el video pueden cambiar; revisa la página de precios oficial de la plataforma antes de desplegar.

Además, el proyecto quedó en GitHub, así que puedes seguir trabajando y desplegar con cada push.

Paso 9: Mejorar el diseño con Impeccable

Para pulir la interfaz usé Impeccable, un skill de diseño para agentes de código (Claude Code, Codex, Cursor, Gemini CLI y otros) que parte del skill frontend-design de Anthropic y añade guías y comandos para evitar el típico diseño genérico generado por IA.

Instalación desde la raíz del proyecto:

npx impeccable install

Otra opción es buscar "impeccable" en el directorio de Skills, copiar su comando de instalación y seleccionar tu agente (en mi caso, Claude) en el instalador.

Luego recarga los skills en tu agente y pídele algo como: "rediseña completamente la interfaz". Impeccable genera varias propuestas de diseño en un servidor local para que elijas una (en mi caso, un estilo SaaS clásico, inspirado en Stripe).

⚠️ Este skill tarda y consume bastantes tokens, tenlo en cuenta.

Después, commit, push a GitHub y "sube los cambios a Seenode usando el MCP". En unos minutos el nuevo diseño está en producción.

Siguientes pasos

La aplicación es intencionalmente básica. Algunas ideas para extenderla:

  • Autenticación y registro de usuarios (y que cada usuario vea solo sus documentos).
  • Procesar más tipos de documentos y ajustar los esquemas de extracción.
  • Exportar reportes en PDF o generar gráficas de gastos.
  • Combinar un OCR especializado con el LLM para campos numéricos críticos.
  • Añadir índices vectoriales (HNSW o IVFFlat en pgvector) cuando crezca el volumen de datos.

📦 Código fuente: repositorio en GitHub

Si tienes dudas o quieres ver otro tipo de proyecto, déjalo en los comentarios del video. Y si necesitas ayuda con tu propio proyecto, en fazt.dev puedes reservar asesorías personalizadas.

Preguntas frecuentes

¿Qué es un RAG en inteligencia artificial?

Es un patrón en el que se recupera información relevante de tus propios datos (documentos, base de datos) y se le pasa al modelo como contexto antes de generar la respuesta, para que responda sobre tu información y no solo con su conocimiento general.

¿Qué es pgvector?

Es una extensión de código abierto para PostgreSQL que añade el tipo de dato vector y permite hacer búsquedas por similitud, ideal para guardar embeddings junto al resto de tus datos.

¿OpenRouter es más barato que usar las APIs directamente?

No necesariamente. OpenRouter no añade margen al precio de inferencia, pero cobra una comisión al comprar créditos. Su ventaja es usar muchos modelos con una sola clave y una sola facturación.

¿Necesito un modelo especial para extraer datos de PDFs e imágenes?

Necesitas un modelo que acepte imágenes como entrada (multimodal). Verifica en la ficha del modelo que soporte visión; algunas versiones de un mismo modelo son solo de texto.

¿Poner la temperatura en 0 evita que el RAG invente respuestas?

No por sí sola. La temperatura controla la aleatoriedad. Para reducir invenciones necesitas buen prompt, buena recuperación, citas y tests con preguntas sin respuesta en los datos.

¿Cuándo debo usar SQL en lugar de embeddings?

Para cálculos (sumas, conteos, promedios) y búsquedas exactas. Los embeddings son para preguntas de significado, cuando no sabes exactamente cómo está escrito lo que buscas.