RAG autoalojado para preguntas sobre documentos: LangChain, pgvector y LLM intercambiables
Cómo una app RAG open source convierte archivos PDF y DOCX en respuestas: troceado, pgvector, cambio entre OpenAI y Claude con un solo ajuste y lecciones de desarrollar con poco presupuesto.
Los documentos rara vez responden solos a las preguntas sobre ellos. Normativas, informes y contratos se acumulan en forma de PDF, archivos de Word y escaneos, y casi nadie tiene tiempo de leerlos de principio a fin. Crear un sistema de generación aumentada por recuperación (RAG) capaz de responder preguntas sobre los archivos subidos es una forma práctica de solucionarlo, y el proyecto open source AI-DocumentIntelligence es un buen caso de estudio sobre cómo mantener un sistema así simple y fiable.
El hueco que cubre
La misma situación aparece en cualquier organización con mucho papeleo. Una trabajadora social necesita una línea de una normativa de 40 páginas. Un analista necesita una sola cifra de la página 22 de un informe. El archivo está disponible en teoría, pero encontrar algo en él lleva una eternidad.
Los equipos suelen acabar con dos opciones. Una es un producto cerrado de IA para documentos, con precios poco claros y reglas difusas sobre qué pasa con los datos. La otra es seguir buscando a mano.
Una alternativa abierta y autoalojada ofrece un tercer camino. El objetivo no es otro envoltorio fino sobre la API de alguien. Es algo lo bastante sencillo como para que un equipo que maneja documentos sensibles pueda inspeccionarlo, cambiar piezas y confiar en ello.
Cómo funciona: subir, trocear, vectorizar, preguntar
La aplicación hace cuatro cosas:
- Acepta archivos PDF, DOCX o TXT.
- Divide cada archivo en fragmentos pequeños y con sentido.
- Convierte los fragmentos en embeddings y los guarda en PostgreSQL mediante la extensión
pgvector. - Responde preguntas en lenguaje natural sobre el documento usando OpenAI o Anthropic Claude, y un solo ajuste cambia entre los dos.
Ese último punto importa más de lo que parece. Muchos proyectos RAG de tutorial te atan a un único proveedor de modelos. Los proyectos reales a menudo necesitan cambiar de proveedor por coste, cumplimiento normativo o disponibilidad, y nadie quiere reescribir la app cuando eso ocurre. Aquí LLM_PROVIDER es un simple valor de configuración y no algo enterrado en lo más profundo del código.
El stack
La lista de tecnologías es corta a propósito:
- Frontend: React 18 con TypeScript
- Backend: Node.js y Express en TypeScript
- Orquestación: LangChain
- Almacén vectorial: PostgreSQL con pgvector
- Modelos: OpenAI o Anthropic Claude, a elegir
- Herramientas: Docker, ts-node y dotenv
Nada de esto obliga a un equipo a aprender una nueva forma de publicar software. Cualquiera que ya sepa ejecutar un servicio Node.js junto a una base de datos PostgreSQL puede ejecutar esto.
Arquitectura
El esquema general es una interfaz en React que habla con una API en Express, que a su vez pasa las subidas a un procesador de documentos:
┌────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ React UI │ ───► │ Express API │ ───► │ Document Processor │
│ (upload+chat)│ │ (TypeScript) │ │ (chunking / split) │
└────────────┘ └──────────────────┘ └────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌────────────────────┐
│ LangChain Layer │ ◄──► │ PostgreSQL + pgvector│
│ (OpenAI / Claude) │ │ (embeddings store) │
└──────────────────┘ └────────────────────┘
El troceado y la vectorización ocurren una sola vez, al subir el archivo. A partir de ahí, cada pregunta sigue la misma ruta corta. La pregunta se convierte en un embedding, pgvector devuelve los fragmentos más cercanos, esos fragmentos viajan al modelo elegido junto con la pregunta y la respuesta vuelve acompañada del historial de chat de ese documento.
No hay nada exótico en este diseño, y de eso se trata. El valor está en hacer bien las partes sencillas: elegir el proveedor de forma limpia, dividir el texto con criterio y fallar con elegancia cuando falta una clave o un ajuste.
El código que importa
Bastan unos pocos fragmentos para mostrar las ideas principales.
Cambiar de proveedor se reduce a leer una variable de entorno:
// backend/src/llm/provider.ts (simplified)
const provider = process.env.LLM_PROVIDER; // "openai" | "anthropic"
Después, una pequeña función factoría devuelve el modelo de chat adecuado según ese valor:
export function getChatModel() {
if (provider === "anthropic") {
return new ChatAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
model: process.env.ANTHROPIC_MODEL,
});
}
return new ChatOpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}
El troceado se apoya en el splitter recursivo de LangChain, configurado más pensando en preguntas y respuestas que en el simple almacenamiento:
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 150,
});
const chunks = await splitter.splitDocuments(rawDocs);
El solapamiento merece más atención de la que suele recibir. Si es demasiado pequeño, una respuesta puede quedar partida entre dos fragmentos. Si es demasiado grande, pagas por almacenar y buscar la misma frase tres veces. Un solapamiento de 150 caracteres en fragmentos de 1000 dio los mejores resultados con una mezcla de archivos de prueba PDF y DOCX.
La recuperación y la generación también se mantienen separadas. Cada etapa se puede probar por separado, así que puedes confirmar que la recuperación encuentra los fragmentos correctos sin gastar créditos de API en la generación. Con una cuenta gratuita de OpenAI, ese ahorro es considerable.
Qué funcionó y qué costó
Decisiones que compensaron
- Un proveedor intercambiable desde el primer día. La mayor parte de la lógica de recuperación se construyó y probó con embeddings locales gratuitos antes de tocar ninguna API de pago, así que los costes se mantuvieron cerca de cero durante el desarrollo.
- Mantener el troceado y la vectorización separados de la respuesta a preguntas. Cuando una respuesta parece incorrecta, puedes saber rápido si la culpa es de la recuperación o de la generación.
- Docker Compose para todo el stack (backend, frontend y Postgres). Cualquiera puede clonar el repo y tenerlo funcionando en minutos en lugar de pelearse con dependencias toda una tarde.
Lo que resultó más difícil de lo esperado
- Trabajar sin créditos de API de pago. No tener acceso a una cuenta de pago de OpenAI o Claude es una limitación real. La solución fue validar primero la recuperación con modelos de embeddings locales gratuitos y añadir después el paso de generación de pago. Es un buen consejo para cualquier proyecto RAG con poco presupuesto.
- Elegir el tamaño de fragmento. Es poco vistoso y es crucial. Ningún ajuste de prompts salva un mal troceado, y corregirlo produjo la mayor mejora individual del proyecto.
- Hacer que funcione en la máquina de otra persona. Un portátil donde todo funciona y un
docker compose uplimpio en el ordenador de un desconocido son cosas distintas. Un.env.exampleclaro, ajustes explícitos del nombre de la base de datos y una solución documentada para los conflictos de puertos llevaron más tiempo que la propia lógica RAG, pero deciden si alguien más puede usar el proyecto.
Cómo ejecutarlo tú mismo
El repo es open source y arranca en local con unos pocos comandos:
git clone https://github.com/Srameshgitnow/AI-DocumentIntelligence.git
cd AI-DocumentIntelligence
docker compose up --build
Las variables de entorno, los pasos completos de instalación y las notas para resolver problemas están en el README del proyecto. Los informes de errores, las ideas sobre el enfoque para cambiar de proveedor y los pull requests tienen cabida en el gestor de incidencias.
La idea de fondo de proyectos como este es aplicar la orquestación de LLM con LangChain o LangGraph a problemas reales de flujo de trabajo, como preguntas sobre normativas en servicios públicos, agentes de revisión de código o convertir notas de voz en historias de usuario ágiles, y saltarse la demo pulida que nunca se enfrenta a documentos reales. Lo simple, intercambiable y fácil de inspeccionar suele ganar a lo ingenioso.