Inicio / Artículos

Un proyecto RAG no es un único archivo gigante en Python: una estructura de carpetas que se adapta a diferentes tamaños

Un proyecto RAG no debería ser un único archivo gigante en Python. Se debe utilizar una estructura de carpetas para la ingesta, el particionamiento, las embeddings, el almacén de vectores, la recuperación, las instrucciones, la API y las pruebas.

Editor: REACTAPP.TOP9 min de lectura2182 palabras

Fuentes: **Your RAG Project Should Not Be One Giant Python File**

Una demostración de RAG lleva toda una tarde: cargar un PDF, dividirlo, incrustar las partes, insertarlas en una base de datos vectorial, hacer una consulta y leer la respuesta. Eso es suficiente para un experimento de fin de semana. Como base para trabajo real, falla rápidamente, y la razón rara vez es el modelo o incluso el almacén vectorial. La causa es la estructura.

Los proyectos para principiantes suelen comenzar como un único archivo app.py que contiene la carga de archivos, la división en fragmentos, las incrustaciones, el almacenamiento, la recuperación, las plantillas de prompts, las llamadas al modelo, las rutas de API, el registro de logs y la configuración. Abrir un solo archivo parece conveniente hasta que el proyecto crece. Llega una nueva fuente de documentos, se necesita probar una estrategia diferente de división en fragmentos, ChromaDB se reemplaza por Pinecone, el prompt cambia, aparece un endpoint de FastAPI, la calidad de la recuperación resulta ser deficiente y alguien quiere pruebas. Para entonces, tocar ese único archivo se vuelve complicado.

Un sistema mantenible asigna a cada tarea su propio espacio específico. La fase de ingestión carga los datos, la división en fragmentos separa los documentos, las representaciones vectoriales convierten el texto en vectores, la base de datos vectorial los almacena, la recuperación encuentra el contexto útil, los prompts dan forma a la solicitud, la capa de LLM realiza las llamadas al modelo y la API expone todo a los usuarios. Esa separación es lo que convierte una demostración en un sistema de ingeniería.

Un modelo mental: pipeline versus aplicación

Piénselo de esta manera: el pipeline prepara el conocimiento, y la aplicación lo utiliza. Manténgalos separados.

La parte del pipeline se encarga de cargar documentos, limpiar texto, dividir el contenido, crear representaciones vectoriales y almacenarlas. La parte de la aplicación se ocupa de recibir preguntas, recuperar fragmentos, crear prompts, llamar al modelo, devolver respuestas a través de una API y registrar el comportamiento en entornos de producción.

Al combinarlos, todo cambio conlleva riesgo. Probar un nuevo tamaño de bloqueo no debería requerir editar una ruta API. Pasar de un proveedor de modelos a otro no debería significar reescribir la lógica de recuperación. Agregar la incorporación de sitios web no debería afectar a las plantillas de prompts.

Un esquema inicial

Aquí hay un esquema que funciona como boceto inicial. No es el único válido, pero cada carpeta tiene una razón clara para existir.

rag-project/
|-- README.md
|-- requirements.txt
|-- .env
|-- .gitignore
|-- config.yaml
|-- main.py
|-- src/
|   |-- ingestion/
|   |   |-- __init__.py
|   |   `-- loader.py
|   |-- chunking/
|   |   |-- __init__.py
|   |   `-- chunker.py
|   |-- embeddings/
|   |   |-- __init__.py
|   |   `-- embedder.py
|   |-- vectordb/
|   |   |-- __init__.py
|   |   `-- vector_store.py
|   |-- retrieval/
|   |   |-- __init__.py
|   |   `-- retriever.py
|   |-- prompts/
|   |   |-- __init__.py
|   |   `-- prompt_templates.py
|   |-- llm/
|   |   |-- __init__.py
|   |   `-- llm_client.py
|   |-- api/
|   |   |-- __init__.py
|   |   `-- routes.py
|   `-- utils/
|       |-- __init__.py
|       `-- helpers.py
|-- tests/
|   `-- test_app.py
`-- logs/
    `-- app.log

README.md

El README es la puerta de entrada. Debe indicar qué hace el sistema, cómo configurarlo y ejecutarlo, y para qué sirve cada carpeta principal. Los principiantes suelen posponer su elaboración, pero uno bueno les ayuda a comprender su propio proyecto incluso un mes después. Como mínimo, incluya el propósito, los pasos de configuración, las variables de entorno necesarias, cómo ingresar documentos, cómo ejecutar la API, ejemplos de preguntas y las limitaciones conocidas. Para un proyecto de portafolio es aún más importante, ya que un revisor podría echarle un vistazo rápido antes de decidir si ejecuta algo o no.

requirements.txt

Este archivo enumera los paquetes de Python que necesita el proyecto. Un servicio RAG sencillo podría incluir algo similar a lo siguiente.

fastapi
uvicorn
python-dotenv
pydantic
langchain
chromadb
sentence-transformers
openai
pypdf

Tu lista depende de tu conjunto de tecnologías. La regla es no dejar las dependencias ocultas en tu propia máquina. Cualquiera que clone el proyecto debería poder instalar todo con un solo comando.

pip install -r requirements.txt

Eso hace que el proyecto sea reproducible y más fácil de desplegar y revisar.

.env y .gitignore

El archivo .env contiene valores sensibles como las claves de API. Uno típico se ve así.

OPENAI_API_KEY=your_key_here
VECTOR_DB_URL=your_vector_db_url

Nunca debe llegar a GitHub, por lo que su .gitignore debería excluirlo junto con los registros y el bytecode.

.env
logs/
__pycache__/
*.pyc

El uso de claves codificadas directamente en los archivos de Python puede funcionar localmente, pero no es seguro. Guarde los datos confidenciales fuera del código.

config.yaml

Los ajustes que no son confidenciales deben encontrarse en config.yaml, por ejemplo:

chunking:
  chunk_size: 800
  chunk_overlap: 120

retrieval:
  top_k: 5

models:
  embedding_model: text-embedding-3-small
  llm_model: gpt-4.1-mini

vector_db:
  provider: chromadb
  collection_name: company_docs

Los sistemas RAG necesitan ajustes, y será necesario modificar el tamaño de los fragmentos, el grado de superposición, la cantidad de resultados a obtener, el nombre del modelo, la temperatura o las opciones de la base de datos vectorial. Cuando esos valores están dispersos en archivos aleatorios, realizar ajustes se vuelve molesto. Los parámetros deberían ser fáciles de encontrar y cambiar.

Las carpetas de código

ingestion/

Esta carpeta lee contenido de PDF, sitios web, archivos CSV, APIs, bases de datos, Markdown, exportaciones de Notion, carpetas de Drive o cualquier otro recurso que tengas. Su función no es responder preguntas, sino ingresar contenido en bruto en un formato limpio y predecible. Un loader.py podría incluir funciones como load_pdf(path), load_website(url), load_csv(path) y load_database_table(connection, table_name), cada una de las cuales devuelve documentos con una estructura estándar, generalmente texto más metadatos.

Los metadatos merecen más atención de la que suelen recibir. Un fragmento de texto es útil, pero uno que incluya su nombre de origen, número de página, URL, tipo de documento, autor y marca de tiempo es mucho más útil, ya que esos datos permiten la filtración, las citaciones, la depuración y la construcción de confianza.

División en fragmentos/

La división en fragmentos parece algo sencillo, pero tiene un impacto enorme en la calidad. Los fragmentos demasiado grandes generan secciones extensas llenas de texto irrelevante, mientras que los demasiado pequeños eliminan el contexto que el modelo necesita. El objetivo es preservar el significado, por lo que en lugar de dividir ciegamente cada 500 caracteres, considere dividir por encabezados, párrafos, secciones, páginas, bloques de código, tablas o límites semánticos. Un chunker.py convierte documentos completos en fragmentos listos para su uso en procesos de embedding.

Aislar esta lógica rinde frutos, ya que es una de las formas más rápidas de mejorar un sistema RAG. Al separar el proceso de fragmentación, se pueden probar nuevas estrategias sin afectar nada más.

embeddings/

Los embeddings son representaciones numéricas del texto que permiten al sistema comparar significados: la pregunta se convierte en un embedding y se compara con los chunks también convertidos en embeddings. Un archivo embedder.py debe encargarse de cargar el modelo, generar embeddings para entradas individuales y lotes, gestionar los límites de velocidad, intentar nuevamente en caso de fallos y registrar qué versión del modelo generó los vectores.

Esta separación es importante porque los modelos de embeddings cambian con el tiempo. Puede que comiences con un sentence-transformer local, luego pases a una API alojada y, posteriormente, cambies de proveedor por motivos de costo o calidad. Al tener toda esa lógica en un solo lugar, cada cambio sigue siendo manejable.

vectordb/

Esta carpeta alberga los embeddings y los metadatos. Las opciones típicas son FAISS para experimentos locales, ChromaDB para configuraciones locales simples o de producción pequeña, Pinecone para búsquedas en la nube gestionadas, y Weaviate, Qdrant o Milvus para necesidades más grandes o especializadas.

Un vector_store.py debe ocultar los detalles específicos de la base de datos al resto del proyecto. El código de recuperación no debe conocer los detalles de Pinecone o ChromaDB; en su lugar, debe utilizar métodos a nivel de proyecto como add_documents(chunks), similarity_search(query, top_k), delete_collection(name) y get_document_by_id(id). Si alguna vez reemplaza la base de datos, solo debe modificar esta capa y nada más.

retrieval/

Aquí surge una pregunta y se seleccionan los fragmentos más relevantes. La recuperación básica consiste en una búsqueda simple por similitud, mientras que las versiones más avanzadas añaden filtros de metadatos, búsqueda híbrida, reclasificación, reescritura de consultas o recuperación de múltiples consultas. Esto es de suma importancia, ya que el modelo solo puede responder bien cuando recibe el contexto adecuado, y una recuperación deficiente conlleva respuestas deficientes.

Un retriever.py puede incrustar la consulta, buscar en el almacenamiento, aplicar filtros, reclasificar los resultados, eliminar duplicados y devolver un contexto limpio para la solicitud. Separarlos facilita enormemente la depuración, ya que se puede determinar si el modelo falló o si la recuperación envió el contexto incorrecto. Esa pregunta no tiene una respuesta clara cuando ambos elementos están entrelazados.

prompts/

Las solicitudes no deberían estar dispersas por todo el código. Un prompt_templates.py puede contener plantillas como esta.

You are a helpful assistant answering questions using the provided context.

Use only the context below. If the answer is not in the context, say you do not know.

Context:
{context}

Question:
{question}

Answer:

Guardar los prompts en un archivo propio es un hábito que vale la pena adoptar desde temprano. Estos cambian con frecuencia a medida que experimentas con instrucciones, formatos de salida, estilos de citación, reglas de seguridad o comportamientos de rechazo. Al quedar ocultos dentro de rutas o llamadas al modelo, resulta difícil versionarlos y probarlos. Trátalos como parte del diseño y asignales un archivo dedicado.

llm/

Todas las llamadas a los modelos de lenguaje deben estar en un mismo lugar, ya sea que el proveedor sea OpenAI, Claude, Gemini, un modelo local de Ollama o cualquier otro. El resto de la aplicación no debería preocuparse por ello. Un llm_client.py puede encargarse de la inicialización, las llamadas a la API, los intentos de reintentar, los tiempos de espera, la temperatura, el análisis de las respuestas, el registro del uso de tokens y los errores específicos del proveedor. Los modelos cambian rápidamente: hoy GPT, mañana Claude, un modelo más pequeño para consultas sencillas y uno más potente para las complejas el mes que viene. Las llamadas centralizadas facilitan el cambio de modelo.

api/

Esta capa permite que otras personas y aplicaciones utilicen el sistema, generalmente a través de FastAPI o Flask. Debe recibir y validar las solicitudes, llamar al proceso de recuperación y generación, devolver respuestas estructuradas y manejar los errores de manera adecuada. No debe contener la lógica en sí. Una ruta debe ser sencilla: aceptar la solicitud, llamar al servicio correspondiente y devolver el resultado. Si un endpoint incluye tareas como la ingesta de datos, su división en fragmentos, el embedding, la recuperación de información, la creación de prompts y las llamadas al modelo, el proyecto se vuelve difícil de mantener. La API debe ser la puerta de entrada, no toda la estructura del sistema.

utils/

Los pequeños auxiliares utilizados en todo el proyecto se encuentran aquí, como la carga de configuraciones, el formato de los registros, la limpieza de texto, la validación de rutas, el control de tiempos y el manejo de errores comunes. Sea disciplinado, ya que esta carpeta puede convertirse fácilmente en un lugar de desecho. Un auxiliar que pertenezca claramente a la fase de ingestión debe ir en esa carpeta, y uno destinado a la recuperación, en la correspondiente. Utilice utils únicamente para aquello que sea realmente compartido.

tests/

Las pruebas separan los proyectos serios de las demostraciones frágiles, y los sistemas RAG están compuestos por muchas partes pequeñas que se pueden probar una por una. Puede verificar que los PDFs se carguen correctamente, que los tamaños de los fragmentos sean adecuados, que los metadatos se conserven, que se creen las representaciones vectoriales, que la recuperación devuelva documentos relevantes, que las instrucciones incluyan tanto el contexto como la pregunta y que la API devuelva la estructura esperada.

No es necesario contar con una cobertura perfecta desde el primer día, pero se necesita suficiente información para saber cuándo algo falla. Las pruebas de recuperación son especialmente valiosas: mantenga un conjunto pequeño de documentos y preguntas de ejemplo, defina qué fuente debe devolverse para cada una y vuelva a ejecutarlas después de cualquier cambio en la configuración de segmentación o incrustación. Eso proporciona una señal básica de calidad antes de que los usuarios detecten problemas.

logs/

Los registros de producción pueden integrarse en una herramienta de monitoreo, pero un archivo de registro simple resulta útil durante el desarrollo. Los registros deben indicar qué preguntó el usuario, qué segmentos se recuperaron, qué fuentes se utilizaron, cuánto tiempo tardó la recuperación y qué modelo se llamó. Cuando un usuario afirma que la respuesta fue incorrecta, es necesario ver qué se recuperó realmente y qué prompt se envió.

Mantener short a main.py

El punto de entrada debe mantenerse sencillo. Inicia el servidor API, carga la configuración o llama a una función principal del pipeline, y nada más. Un punto de entrada simple indica que el trabajo real se encuentra en los módulos adecuados.

Qué te aporta esto

  • Depuración: aislar el fallo. Si la respuesta es incorrecta, revisar la recuperación de datos, luego el procesamiento en bloques y las incrustaciones. Si la API falla, examinar la capa de rutas.
  • Pruebas: verificar cada componente por separado, de modo que para comprobar el cargador de PDF no sea necesario ejecutar toda la aplicación.
  • Escalabilidad: mejorar una capa sin tener que reescribir el resto, ya sea añadiendo procesamiento por lotes, tareas en segundo plano, caché, reclasificación o una nueva base de datos vectorial.
  • Colaboración: una persona mejora el proceso de ingreso de datos mientras otra trabaja en los prompts o las rutas.
  • Despliegue: la configuración, los secretos, las dependencias y los puntos de entrada están claros.

Sobre todo, hace que el proyecto sea comprensible, y un proyecto fácil de entender es más fácil de confiar.

Una regla para los primeros proyectos

Utilice una carpeta por responsabilidad y no busque una arquitectura perfecta. Simplemente evite tener un único archivo enorme. Comience con la ingesta de datos, su división en partes, las representaciones vectoriales, una base de datos vectorial, la recuperación de información, los prompts, las llamadas a modelos de lenguaje grande, una API, las pruebas y los registros. Eso es suficiente para la mayoría de los proyectos de principiantes y de portafolio. Se pueden añadir capas adicionales como el reordenamiento de resultados, la evaluación, el caché, la autenticación, las tareas en segundo plano, la versiónación de documentos y los paneles de control a medida que surjan necesidades, pero la idea básica nunca cambia: mantener el pipeline separado de la aplicación. Un pequeño hábito que ayuda es anotar, en el README, qué carpeta modificaría para cada tipo de cambio, ya que si la respuesta resulta poco clara, es probable que la estructura haya comenzado a volverse confusa.

Una demostración muestra que una idea puede funcionar. Un proyecto mantenible demuestra que el sistema puede crecer. Los carpetas por sí solas no son el objetivo; lo importante es asignar un lugar claro a cada responsabilidad.

Leer más

  • Spring AI en la empresa: Un agente de soporte con herramientas, RAG y revisión — Crea un asistente para la triaje de incidentes con Spring AI: prompts como contratos, salida estructurada, llamadas a herramientas, MCP, Qdrant RAG, capacidad de observabilidad y bucles de agente limitados.