De cero a un sistema RAG: acirtos y errores

Hace unos meses me encargaron crear una herramienta interna para los ingenieros de la empresa: un Chat que usara un LLM local. Hasta aquí nada extraordinario de contar. Entonces entraron los requisitos: que tuviera una respuesta ágil, insisto... ¡ágil!, y... que además pudiera dar contestaciones sobre todos los proyectos que se han hecho en la empresa a lo largo de toda su historia (casi una década). No se quería un buscador tradicional, sino una herramienta con la que poder hacer preguntas en lenguaje natural y obtener respuestas con referencias a los documentos originales. Haciendo hincapié en que debía proporcionar información a partir de los archivos OrcaFlex (un software de simulación de dinámica de cuerpos flotantes, cables, etc. muy usado en la industria offshore). Ya parecía complejo, pero se me confirmó cuando me dieron acceso a 1 TB de proyectos, mezclado con documentación técnica, informes, análisis, normativas, CSVs, etc. La montaña rusa de emociones había comenzado.

Te adelanto que no fue un proceso ni rápido ni fácil, y por ello me gustaría compartirlo. Desde los primeros intentos, equivocaciones, hasta la arquitectura final que terminó en producción. También quiero resaltar que no había hecho algo similar con anterioridad y tampoco conocía cómo trabajaba un RAG.

Iremos problema en problema, y la solución que puse a cada uno de ellos.

Problema 1: seleccionar la tecnología adecuada

El primer paso fue definir el stack.

Necesitaba un modelo de lenguaje local, sin depender de APIs externas, por temas de confidencialidad. Ollama se presentó como la opción más madura y fácil de usar para ejecutar modelos LLaMA en local. Probé varios embeddings, y nomic-embed-text ofrecía un buen rendimiento y calidad para documentos técnicos.

Lo siguiente era un motor RAG que orquestara el proceso de indexación de documentos, generación de embeddings, almacenamiento en la base vectorial y consultas. Sin él, por muy rápido que sea el modelo de lenguaje, no podríamos recuperar información relevante de los documentos. Puedes verlo como un índice de un libro: sin él, tendrías que leer todo el libro para encontrar la información que buscas. Y con un buen índice, puedes ir directo a la página correcta. A este proceso lo llamaré indexación para que nos entendamos, aunque realmente es un proceso de vectorización e indexación.

Después de una búsqueda, me encontré con un framework maduro opensource llamado LlamaIndex.

El lenguaje que usaría sería Python, puedo poner una larga lista de razones, pero la más importante es que me siento cómodo y productivo. Además, tanto Ollama como LlamaIndex tienen excelentes SDKs en Python.

Ya estaba listo para empezar a construir el software. Programé mis primeros scripts para hacer pruebas vectores del sistema RAG y hacer algunos experimentos de consulta. Funcionaba realmente bien con muy poco código. Pensé que sería un proyecto de unas semanas. No podía estar más equivocado.

El siguiente paso era trabajar con los documentos reales. ¡Agárrate que vienen curvas!

Problema 2: el caos documental

Mi fuente de archivos fue una carpeta en Azure con una cantidad ingente de documentos técnicos: cientos de gigabytes, miles de archivos, formatos variados, sin ningún tipo de organización ni estructura más allá de la jerarquía de carpetas. El sueño de cualquier ingeniero de datos (nótese la ironía).

Crují mis dedos, indiqué que el resultado del RAG lo guardara en disco y lancé mi primer script. LlamaIndex acabó desbordando la memoria RAM de mi portátil en algunos minutos, ahogando mi SO hasta congelar todo. Probé muchas configuraciones, sistemas caché y demás estrategias, pero en algún momento mi equipo siempre moría.

Después de hacer debugging, descubrí que procesaba archivos enormes que no aportaban nada: vídeos, simulaciones, archivos de backup... Documentos que no aportaban nada a un sistema RAG, pero que LlamaIndex intentaba procesar como si fueran texto. Si un archivo pesaba varios gigabytes, el sistema intentaba cargarlo entero en memoria para procesarlo, lo que era un suicidio.

Incluí en el pipeline un sistema de filtrado que excluía archivos por extensión y por patrones en el nombre (archivos de simulación, resultados numéricos, etc.).

Categoría Extensiones excluidas
Vídeo mp4, avi, mov, mkv, wmv, flv, webm, m4v, mpg, mpeg, 3gp, mts...
Imágenes jpg, jpeg, png, gif, bmp, tiff, svg, ico, webp, heic, psd...
Ejecutables exe, dll, msi, bat, sh, app, dmg, so, jar...
Comprimidos zip, rar, 7z, tar, gz, bz2, xz
Simulación sim, dat
Temporales tmp, temp, cache, log, swp, pyc, crdownload, partial...
Backups bak, 3dmbak, dwgbak, dxfbak, pdfbak, stlbak, old, bkp, original...
Correo msg, pst, eml, oft

También quité archivos que costaban mucho de procesar y tampoco aportaban valor, como CSVs, JSONs, entre otros. Por otro lado, convertí a texto plano los archivos PDF, DOCX, XLSX, PPTX, etc. para que LlamaIndex pudiera procesarlos sin problemas.

El resultado fue una reducción de un 54% en el número de archivos a indexar. Y por supuesto, mi memoria RAM dejó de explotar.

Ya podía empezar a indexar sin miedo.

Problema 3: indexar 451GB de documentos sin morir en el intento

Un RAG implica crear un archivo de índice vectorial que contenga los embeddings de los documentos. Los vectores son representaciones numéricas de los documentos que permiten medir su similitud. LlamaIndex posee un sencillo sistema que puedes configurar con un par de líneas. Tan solo le indicas el directorio y él se encarga de guardar toda la información ahí dentro en forma JSON. Es realmente cómodo, funciona bien, salvo que trabajes con cientos de gigabytes de documentos. El sistema se volvió inmanejable: cada vez que el servicio se reiniciaba, tenía que volver a procesar todos los documentos desde cero, lo que podía llevar días. Además, el formato que usa por defecto no es óptimo para las búsquedas grandes (JSON).

Incluí un sistema de checkpoints para guardar el progreso de la indexación. Cada vez que encontrara un problema, no perdería todo el progreso, sino que podría reanudar desde el último archivo procesado. Sin embargo, los datos se corrompían, era propenso a errores y muy lento. Estaba ante un cuello de botella que no podía superar.

Después de muchas pruebas y errores, y leer más al respecto, decidí dar el salto a una base de datos vectorial dedicada: ChromaDB. La base de datos de Google para almacenar y consultar vectores. No confundir con el navegador Chrome/Chromium. ChromaDB es una capa de abstracción que almacena por encima de una base de datos tradicional, configuré SQLite, y ofrece funcionalidades específicas, como búsquedas por similitud, clustering, etc.

El cambio fue radical e instantáneo. La indexación pasó de ser un proceso monolítico que cargaba todo en memoria a un pipeline por lotes que procesaba 150 archivos a la vez, generaba sus embeddings y los almacenaba directamente en ChromaDB. Esto permitió indexar los 451GB de documentos en múltiples sesiones, con checkpoints, sin perder progreso ante interrupciones, sin datos corruptos. Además, era realmente fácil hacer copias de seguridad y restaurar el índice en caso de fallos (basta con copiar el archivo SQLite).

Ya tenía listo el sistema final. Con un benchmark rápido, descubrí que necesitaría varios meses para indexar todo el contenido con mi portátil. Ahora el cuello de botella no era ni la RAM, ni el sistema de indexación, ni los archivos, sino la GPU.

Problema 4: mi tarjeta gráfica no es un cohete

Mi portátil tiene una tarjeta gráfica integrada. Procesar por CPU 500 Mb de documentos le requiere 4-5 horas, no son buenos números. Sí o sí necesitaba una GPU potente. En una reunión de seguimiento, se decidió alquilarme una máquina virtual con una NVIDIA RTX 4000 SFF Ada, que posee 20GB de VRAM. Este tipo de alquileres no son precisamente baratos. Ahora trabajaba bajo más presión.

Modifiqué mis contenedores y el sistema quedó optimizado para aprovechar la GPU. Arranqué mi script. Después de varias semanas, entre 2 o 3, el proceso de indexación terminó sin fallos. 738.470 vectores, 54GB de índice en ChromaDB, y un sistema RAG listo para responder preguntas. Copié la base de datos de ChromaDB, un archivo SQLite, a mi máquina local y listo. Para alivio de mi Sysadmin y Project Manager, ya podíamos apagar la máquina virtual. Se gastaron 3 cifras, no recuerdo la cantidad exacta, pero no fue barato.

Era el momento de montar el backend y el frontend.

Problema 5: la experiencia de usuario

Con Flask hice una sencilla API para acceder a LlamaIndex, que a su vez consultaba a ChromaDB y Ollama.

Soy bastante fan de Streamlit para realizar proyectos internos de todo tipo, por lo que sería mi frontend (y así seguiría usando Python). Además posee un widget nativo para realizar preguntas y respuestas, al estilo de cualquier chat actual para interactuar con una IA.

En un par de horas ya tenía toda la parte visual trabajando. El resto eran detalles a pulir: mostrar el logo de la empresa, un spinner mientras se procesa la consulta, guardar sesiones, etc.

El esquema de cómo se comunican los diferentes componentes del sistema es el siguiente:

flowchart TD
    U["👤 Usuario"]:::user --> E["Streamlit (Web UI)"]:::web
    E <-->|HTTP| D["API Flask"]:::api
    D --> F["Backend Python"]:::backend
    F <--> C["Ollama (LLM + Embeddings)"]:::llm
    C <--> B["RAG (LlamaIndex)"]:::rag
    B <--> G["ChromaDB"]:::chroma

    classDef user fill:#37474F,stroke:#263238,stroke-width:2px,color:#fff
    classDef web fill:#8E24AA,stroke:#6A1B9A,stroke-width:2px,color:#fff
    classDef api fill:#D32F2F,stroke:#B71C1C,stroke-width:2px,color:#fff
    classDef backend fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff
    classDef rag fill:#7CB342,stroke:#558B2F,stroke-width:2px,color:#fff
    classDef chroma fill:#4CAF50,stroke:#388E3C,stroke-width:2px,color:#fff
    classDef llm fill:#FF6F00,stroke:#E65100,stroke-width:2px,color:#fff

Ajusté la plantilla de cómo se debía formatear la respuesta del LLM. Para cada respuesta, el sistema debe mostrar las fuentes de información, es decir, los documentos que se han utilizado para generar la respuesta.

Pregunta: ¿Cuáles son los proyectos de wind farms que se han hecho en la empresa? Respuesta: Se han realizado varios proyectos relacionados con wind farms, incluyendo:
- Proyecto A: Análisis de viabilidad para un parque eólico offshore en el Mar. Referencia: https://.../project_a_wind_farm_report.pdf
- Proyecto B: Simulación de aerogeneradores utilizando OrcaFlex. Referencia: https://.../project_b_wind_farm_simulation.sim

Pero claro, tengo que guardar para ello todos los datos originales en el disco, junto a la base de datos vectorial, el LLM y el backend. Y no tenía espacio para ello. Mi producción era una máquina virtual muy limitada en recursos, y mucho más en disco (100 Gb). No podía permitirme tener medio terabyte de documentos en el servidor.

Problema 6: servir documentos sin llenar el disco

Debemos recordar que tener la base de datos vectorial no implica que podamos prescindir de los documentos originales. Sin embargo puedo tener por un lado el índice vectorial con los embeddings de los documentos, y en otro lugar los documentos originales (sea físicamente en el mismo servidor o en la nube).

La solución fue servir los documentos originales directamente desde Azure Blob Storage (hubiera sido posible en cualquier otro sistema). Para cada documento citado en una respuesta, el sistema genera un enlace de descarga con un token SAS que permite al usuario descargarlo directamente desde la nube.

%%{init: {'theme':'default'}}%%
flowchart LR
    U["👤 Usuario"]:::user -->|Pregunta| S["Servidor (VM)"]:::server
    S -->|Respuesta + enlaces| U
    U -->|Descarga directa con token SAS| A["Azure Blob Storage
(451 GB de documentos)"]:::azure classDef user fill:#37474F,stroke:#263238,stroke-width:2px,color:#fff classDef server fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff classDef azure fill:#0078D4,stroke:#005A9E,stroke-width:2px,color:#fff

Lo que sí o sí necesitaba espacio era para el índice vectorial de ChromaDB, que con 54GB es perfectamente manejable en un disco local, el LLM, que ocupa unos 10GB, el backend (unos pocos megabytes) y el frontend (otros pocos megabytes). El resto de documentos se quedan en Azure, accesibles bajo demanda.

Arquitectura final

La arquitectura final del sistema quedó así:

flowchart LR
    A["Azure Blob Storage"]:::azure -- Documentos --> B["RAG (LlamaIndex)"]:::rag
    B <--> G["ChromaDB"]:::chroma
    B <--> C["Ollama (LLM + Embeddings)"]:::llm
    D["API Flask"]:::api <-- HTTP --> E["Streamlit (Web UI)"]:::web
    C <--> F["Backend Python"]:::backend
    D -- Llamada --> F

    classDef azure fill:#0078D4,stroke:#005A9E,stroke-width:2px,color:#fff
    classDef rag fill:#7CB342,stroke:#558B2F,stroke-width:2px,color:#fff
    classDef chroma fill:#4CAF50,stroke:#388E3C,stroke-width:2px,color:#fff
    classDef llm fill:#FF6F00,stroke:#E65100,stroke-width:2px,color:#fff
    classDef api fill:#D32F2F,stroke:#B71C1C,stroke-width:2px,color:#fff
    classDef web fill:#8E24AA,stroke:#6A1B9A,stroke-width:2px,color:#fff
    classDef backend fill:#00897B,stroke:#00695C,stroke-width:2px,color:#fff
Capa Tecnología Propósito
LLM Ollama + llama3.2:3b Generación de respuestas en local
Embeddings nomic-embed-text Vectorización de documentos
Base vectorial ChromaDB (HNSW) Almacenamiento y búsqueda de similitud
Framework RAG LlamaIndex Orquestación del pipeline RAG
API Flask + Gunicorn Servicio HTTP REST
UI Web Streamlit Interfaz conversacional
Contenedores Docker Compose Orquestación de servicios
GPU NVIDIA Container Toolkit Aceleración por hardware
Storage Service Azure Blob Storage Persistencia en la nube

Lecciones aprendidas

  • Gestiona la memoria: Cargar todos los documentos en memoria y procesarlos de golpe es peligroso. Implementa procesamiento por lotes, yo usaba 150 en cada iteración, con llamadas explícitas al recolector de basura entre lotes. Cada lote se procesa, se embebe, se almacena en ChromaDB, y se libera memoria antes de continuar.
  • Archivos problemáticos: Incluso después de las capas de filtrado, algunos archivos pasaban pero fallaban durante el parseo como los PDFs corruptos, documentos Word con macros rotos, hojas de cálculo con formatos inesperados... La estrategia fue la tolerancia a errores. Si un archivo falla, se registra en el log y se continúa con el siguiente. Nunca un archivo problemático detiene un lote completo. Después, manualmente lo puedo revisar, descargar o asignar a otro lote.
  • Checkpoints: Cada lote puede durar horas, un corte de corriente o un reinicio no puede significar empezar de cero. Implementa un sistema de checkpoints que guarda el último lote completado, el total de nodos procesados y una marca temporal. Al reiniciar, la indexación continúa exactamente donde se quedó.
  • Monitorización: Añade scripts de monitorización y de información para diferentes estados: index-progress, index-watch, index-speed, index-checkpoint, index-failed... Cuando está el RAG trabajando durante horas, necesitas saber qué está pasando.

Conclusión

No es un sistema perfecto, pero sí suficiente. Hubiera sido genial si hubiera podido lanzar una instancia de OrcaFlex para que el LLM pudiera ejecutar los proyectos o realizar sus propias simulaciones bajo demanda. Pero ello requería más tiempo y recursos de los que me podía proporcionar. Sin embargo estoy muy contento con el resultado final. El sistema es rápido, fiable, y sobre todo útil para mis compañeros.

Mi humilde consejo, y si estás considerando construir algo similar: dedica tiempo a construir los mejores datos posibles. Si la fuente es poco relevante, el LLM no podrá generar buenas respuestas.

Este trabajo está bajo una licencia Attribution-NonCommercial-NoDerivatives 4.0 International.

¿Me invitas a un café?

Comentarios

Todavía no hay ningún comentario.

Escrito por Andros Fenollosa

marzo 17, 2026

11 min de lectura

Sigue leyendo

Visitantes en tiempo real

Estás solo: 🐱