Módulo 8: Proyecto Integrador RAG con ChromaDB
Cápsula 02: Arquitectura del Proyecto
Descripción de la cápsula
Definirás la arquitectura del sistema antes de codificar. Esta decisión evita retrabajo y facilita pruebas, monitoreo y mantenimiento. Un diseño claro permite escalar ingestion y serving de forma independiente, migrar componentes sin romper todo y mantener contratos estables entre partes.
Componentes principales
El sistema RAG se compone de cinco bloques lógicos:
| Componente | Responsabilidad | Entrada | Salida |
|---|---|---|---|
| ingestion | Carga, limpieza, chunking y embeddings | Documentos raw (PDF, TXT, MD) | Vectores + metadata en ChromaDB |
| vector_store | Persistencia y consulta de vectores | Vectores, IDs, metadata | Similarity search results |
| retrieval | Top-k + filtros de metadata | Query de usuario | Documentos rankeados con scores |
| generation | Ensamblado de contexto y respuesta del LLM | Query + documentos recuperados | Respuesta generada + citas |
| api | Endpoints para ingest, search, ask | HTTP requests | JSON responses |
Responsabilidades detalladas por componente
Ingestion
- Carga: Soporta TXT, MD, RST (y PDF con pypdf). Detecta encoding (UTF-8, Latin-1).
- Limpieza: Normalización de espacios, manejo de caracteres especiales.
- Chunking: RecursiveCharacterTextSplitter con separadores
\n\n,\n,.,. - Metadata: Extrae y asigna
doc_id,source,chunk_index,titlea cada chunk. - Embeddings: Llama a OpenAI en batches; maneja rate limits y retries.
- Persistencia: Add a ChromaDB en batches; no debe mezclar lógica de negocio de retrieval.
Vector Store (ChromaDB)
- Almacenamiento: Persistencia en disco (
PersistentClient). - Indexación: HNSW por defecto; no requiere configuración adicional para 1K-10K vectores.
- Query: Similarity search con
query_embeddings,n_results,wherepara filtros. - Sin lógica de negocio: Solo recibe vectores y devuelve resultados; no decide qué es "relevante".
Retrieval
- Embedding de query: Misma función/modelo que ingestion para consistencia.
- Búsqueda: Llama a ChromaDB con top_k y filtros opcionales.
- Post-procesado: Aplica score threshold; descarta resultados por debajo del umbral.
- Formato de salida: Estructura estandarizada para que generation no dependa de ChromaDB.
Generation
- Context assembly: Concatena chunks con separadores y metadatos de fuente.
- Prompt engineering: Instrucciones claras: "responde solo con el contexto", "no inventes".
- LLM call: GPT-3.5-turbo con temperatura baja (0.2) para consistencia.
- Post-proceso: Formatea fuentes para el contrato de respuesta; no modifica el contenido generado.
API
- Routing: FastAPI con routers por dominio (ask, search, ingest, collections).
- Validación: Pydantic para request bodies; límites de longitud.
- Respuestas: Contrato JSON estable; trace_id en toda respuesta.
- Documentación: Swagger y ReDoc automáticos.
Diagrama de arquitectura (ASCII)
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ SISTEMA RAG - ARQUITECTURA │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌─────────────────────────────────────────────┐ ┌──────────────┐
│ Documentos │ │ INGESTION PIPELINE │ │ ChromaDB │
│ (PDF, TXT, │────▶│ Load → Chunk (512) → Embed → Metadata │────▶│ (vector │
│ MD, etc) │ │ Batch: 1K-2K docs, progress, error handling│ │ store) │
└──────────────┘ └─────────────────────────────────────────────┘ └──────┬───────┘
│
┌──────────────┐ ┌─────────────────────────────────────────────┐ │
│ Usuario │ │ RETRIEVAL PIPELINE │ │
│ (cliente) │────▶│ Query → Embed → Similarity Search → Top-K │◀────────┘
│ │ │ Metadata filtering, score threshold │
└──────────────┘ └────────────────────┬────────────────────────┘
│
▼
┌──────────────┐ ┌─────────────────────────────────────────────┐
│ Respuesta │ │ GENERATION PIPELINE │
│ + fuentes │◀────│ Context assembly → Prompt → GPT-3.5-turbo │
│ + trace_id │ │ Citation generation, fallback si no hay ctx │
└──────────────┘ └─────────────────────────────────────────────┘
▲
│
┌──────┴───────────────────────────────────────────────────────────┐
│ FastAPI REST API │
│ POST /ingest | GET /search | POST /ask | GET /collections | ... │
└──────────────────────────────────────────────────────────────────┘
Flujo de datos detallado
Flujo de Ingestion (offline o bajo demanda)
Documentos fuente
→ Document Loader (PDF, TXT, MD según tipo)
→ Text cleaning (normalización, encoding)
→ RecursiveCharacterTextSplitter (chunk_size=512, overlap=50)
→ Chunks con metadata (source, doc_id, chunk_index)
→ OpenAI text-embedding-3-small (batch 100-200)
→ ChromaDB add (batch 1000-2000)
→ Validación: conteo, metadata obligatoria, sin duplicados
Flujo de Query (online)
Pregunta del usuario
→ Validación de input (longitud, formato)
→ Embedding de la query (OpenAI text-embedding-3-small)
→ ChromaDB query (top_k=5, where filters opcionales)
→ Score threshold (descartar < 0.5 o similar)
→ Ensamblado de contexto (concatenar chunks)
→ Prompt engineering (instrucciones + contexto + pregunta)
→ GPT-3.5-turbo (temperature=0.2)
→ Post-proceso: extraer citas, formato de fuentes
→ Respuesta JSON: answer, sources, confidence, trace_id
Decisiones de diseño y justificación
¿Por qué ChromaDB?
- Sin coste de infraestructura: Open-source, corre localmente.
- Rápido para prototipado: Persistencia en disco con
PersistentClient, no requiere servidor externo. - Suficiente para 1K-10K documentos: HNSW por defecto, metadata filtering.
- Base de aprendizaje: Los conceptos (CRUD, similarity search, metadata) se transfieren a Pinecone, Weaviate, Qdrant.
Para producción a gran escala (millones de vectores, multi-tenant), migrarías a Pinecone o Weaviate; ChromaDB es óptimo para este proyecto.
Comparación rápida:
| Criterio | ChromaDB | Pinecone | Weaviate |
|---|---|---|---|
| Costo infra | Gratis (self-hosted) | Por uso (serverless) o plan | Self-hosted o cloud |
| Escala típica | 1K-100K vectores | Millones | Millones |
| Setup | pip install | Cuenta, API key | Docker o cloud |
| Uso en este proyecto | ✅ Learning, portfolio | Guía #8 | Alternativa |
¿Por qué chunk_size=512 (tokens aproximados)?
- Balance contexto vs granularidad: Chunks muy grandes diluyen la relevancia; muy pequeños pierden contexto.
- Embedding model: text-embedding-3-small soporta hasta 8191 tokens; 512 deja margen y evita truncado.
- Retrieval: 5 chunks × 512 ≈ 2500 tokens de contexto para el LLM, dentro del límite razonable de GPT-3.5 (4K context window para generación).
- Estándar en RAG: Muchos tutoriales usan 500-1000; 512 es un punto medio probado.
¿Por qué chunk_overlap=50?
- Continuidad semántica: Evita cortar frases o párrafos a la mitad; el overlap permite que una idea que cruza el boundary siga siendo recuperable.
- 50 tokens: Suficiente para no perder contexto, sin duplicar demasiado contenido (menos vectores, menor costo de storage).
¿Por qué GPT-3.5-turbo?
- Costo: Más económico que GPT-4 para un proyecto de aprendizaje y portfolio.
- Latencia: Más rápido que GPT-4.
- Suficiente para RAG: Con buen retrieval y prompt engineering, GPT-3.5-turbo produce respuestas correctas la mayoría del tiempo.
- Upgrade path: Puedes cambiar a GPT-4 o GPT-4o por variable de entorno sin cambiar el código.
Cuándo usar GPT-4: Si el dominio es muy técnico o necesitas razonamiento más profundo, sube a GPT-4. Para la mayoría de RAG con buen retrieval, GPT-3.5-turbo es suficiente.
¿Por qué separar ingestion de serving?
- Escalado independiente: Ingestion puede ser batch/job pesado; serving debe ser low-latency.
- Recursos: Ingestion consume CPU/memoria para chunking y llamadas a embedding API; serving es más I/O-bound (ChromaDB query + LLM).
- Deployment: Puedes correr ingestion como script o worker separado; la API solo sirve queries.
Contratos entre componentes
Ingestion → Vector Store
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
ids | list[str] | Sí | IDs únicos y determinísticos (ej: {doc_id}_{chunk_idx}) |
documents | list[str] | Sí | Texto del chunk |
embeddings | list[list[float]] | Sí | Vectores de text-embedding-3-small |
metadatas | list[dict] | Sí | Al menos source, doc_id, chunk_index |
Retrieval → Generation
| Campo | Tipo | Descripción |
|---|---|---|
documents | list[str] | Chunks recuperados |
metadatas | list[dict] | source, doc_id, score |
ids | list[str] | IDs de ChromaDB |
API → Cliente
{
"answer": "string",
"sources": [
{"doc_id": "string", "source": "string", "title": "string", "score": 0.82}
],
"confidence": 0.78,
"trace_id": "uuid",
"fallback_reason": null
}
Anti-patrones a evitar
| Anti-patrón | Problema | Solución |
|---|---|---|
| Mezclar retrieval y generation en una función monolítica | Difícil de testear, acoplado | Funciones separadas: retrieve() y generate() |
| Configuración sensible hardcodeada | Seguridad, no portable | Variables de entorno: OPENAI_API_KEY, CHROMA_PATH |
| Cambiar esquema de metadata sin versionado | Rompe ingestion previa, inconsistencia | Documentar schema, usar versionado si cambias |
| IDs aleatorios o no determinísticos | Duplicados al re-ingestar | f"{doc_id}_{chunk_index}" |
| Sin fallback cuando retrieval devuelve poco | Respuestas inventadas | Umbral de score, mensaje explícito "no tengo información" |
Puntos de fallo y mitigación
| Punto de fallo | Síntoma | Mitigación |
|---|---|---|
| OpenAI API caída | Timeout o 5xx en embeddings/generation | Retry con backoff; mensaje claro al usuario |
| ChromaDB disco lleno | Error al add | Monitorear espacio; alertas |
| Rate limit OpenAI | 429 en embeddings | Batch size adecuado; cola con backoff |
| Documentos corruptos | Algunos chunks vacíos o inválidos | Validar antes de embed; skip y log |
| Query muy larga | Token limit exceeded | Limitar longitud en API; truncar si es necesario |
| Sin documentos en DB | Retrieval vacío siempre | Health check que valide count > 0; mensaje en /ask |
Alternativas de arquitectura
Monolítico: Un script que hace load → chunk → embed → add. Más simple, pero difícil de escalar.
Microservicios: Ingestion, API y ChromaDB como servicios separados. Overkill para 1K docs.
Librería + CLI + API: Código reutilizable; CLI para ingestion; API para serving. Recomendable si planeas publicar.
Para este módulo: Usa los 5 componentes como capas lógicas dentro del mismo repo. No necesitas microservicios físicos.
Consideraciones de testing por componente
| Componente | Qué testear | Cómo |
|---|---|---|
| Ingestion | Chunking correcto, metadata completa | Unit test con doc de ejemplo; assert chunk count y keys |
| Ingestion | Batch add sin errores | Integration test con ChromaDB en memoria |
| Retrieval | Query devuelve resultados ordenados | Mock ChromaDB o fixture con datos conocidos |
| Generation | Respuesta con contexto, sin inventar | Mock retrieval; assert que respuesta contiene fragmentos del contexto |
| API | /ask end-to-end | Test client FastAPI; mock OpenAI si es necesario |
| API | Fallback cuando retrieval vacío | Llamar /ask con DB vacía; assert fallback_reason |
La separación de componentes permite testear cada uno de forma aislada con mocks, y luego un test de integración end-to-end con servicios reales (o emulados).
Configuración por entorno
| Variable | Desarrollo | Producción |
|---|---|---|
CHROMA_PATH | ./chroma_data (local) | /var/data/chroma o volumen Docker |
OPENAI_API_KEY | .env local | Secrets manager, env inyectado |
TOP_K | 5-10 (explorar más) | 5 (balance recall/latencia) |
SCORE_THRESHOLD | 0.3 (más permisivo) | 0.5 (evitar ruido) |
LLM_MODEL | gpt-3.5-turbo | gpt-3.5-turbo o gpt-4 según budget |
| Log level | DEBUG | INFO o WARN |
Centraliza todo en un módulo config.py que lee de os.getenv. Nunca hardcodees valores que cambien entre entornos.
Plantilla de Decision Log
Para documentar decisiones de diseño, usa una estructura como esta en DESIGN.md:
## Decision Log
### DL-001: ChromaDB como vector store
**Fecha:** 2025-03
**Contexto:** Necesitamos vector store para RAG, presupuesto limitado.
**Decisión:** Usar ChromaDB self-hosted.
**Alternativas:** Pinecone (coste), Weaviate (más setup).
**Consecuencias:** Gratis, fácil; escalar a 100K+ requerirá evaluación.
### DL-002: chunk_size=512
**Contexto:** Balance entre granularidad y contexto para LLM.
**Decisión:** 512 caracteres (~128 tokens).
**Consecuencias:** Ajustar si retrieval es demasiado fragmentado o demasiado largo.
Mantener un log así facilita explicar "por qué" a otros desarrolladores o a tu yo futuro.
Variables de entorno del proyecto
Lista completa de variables que usarás (documenta en README):
| Variable | Descripción | Default |
|---|---|---|
OPENAI_API_KEY | API key de OpenAI | (requerido) |
CHROMA_PATH | Ruta de persistencia ChromaDB | ./chroma_data |
COLLECTION_NAME | Nombre de la colección | rag_docs |
CHUNK_SIZE | Tamaño de chunk en caracteres | 512 |
CHUNK_OVERLAP | Solapamiento entre chunks | 50 |
BATCH_SIZE_CHROMADB | Docs por batch en add | 1000 |
BATCH_SIZE_EMBEDDINGS | Textos por llamada a OpenAI | 100 |
EMBEDDING_MODEL | Modelo de embeddings | text-embedding-3-small |
LLM_MODEL | Modelo para generación | gpt-3.5-turbo |
TOP_K | Resultados en retrieval | 5 |
SCORE_THRESHOLD | Umbral mínimo de score | 0.5 |
Ejercicios de diseño
Ejercicio 1: Dibuja tu arquitectura
Con lápiz y papel o herramienta de diagramas, dibuja:
- Los 5 componentes (ingestion, vector_store, retrieval, generation, api).
- Flujos de datos con flechas (documentos → ingestion → ChromaDB; query → retrieval → generation → response).
- Puntos de fallo (¿qué pasa si OpenAI falla? ¿si ChromaDB está caído?).
- Estrategia de observabilidad por componente (logs, métricas, trace_id).
Solución: Usa el diagrama ASCII de esta cápsula como base. Añade:
- Punto de fallo: Si OpenAI embedding falla → retry con backoff, luego fallar con mensaje claro.
- ChromaDB caído → health check devuelve 503.
- Por componente: ingestion loguea docs procesados/errores; retrieval loguea query + top_k; generation loguea tokens usados; api loguea trace_id en cada request.
Ejercicio 2: Define el esquema de metadata
Lista los campos de metadata que guardarás por chunk. Justifica cada uno.
Solución sugerida:
source: ruta o identificador del documento original (para citas).doc_id: ID único del documento (para filtrar por doc).chunk_index: índice del chunk dentro del doc (para ordenar).title: título del documento si está disponible (para mostrar en fuentes).
Opcional: created_at, category según dominio.
Ejercicio 3: Diseña la estrategia de ids
¿Cómo generar IDs para que la ingestion sea idempotente (re-ejecutar no duplique)?
Solución: doc_id = hash del path o identificador estable del documento. chunk_id = f"{doc_id}_chunk_{chunk_index}". Al re-ingestar, usas upsert (si ChromaDB lo soporta) o borras la colección antes. Alternativamente: verificar existencia antes de add.
Ejercicio 4: Fallback cuando retrieval es débil
Define reglas: ¿Qué hacer si el score máximo es < 0.5? ¿Si solo hay 1 documento? ¿Si hay 0?
Solución:
- Score máximo < 0.5: no usar contexto, responder "No encontré información relevante para tu pregunta".
- 1 documento: usar si score > 0.4; indicar baja confidence.
- 0 documentos: siempre fallback, "No tengo datos para responder".
Ejercicio 5: Prioriza observabilidad
De logs, métricas y traces, ¿qué implementarías primero y por qué?
Solución:
- Logs estructurados (JSON): request_id, componente, mensaje. Mínimo esfuerzo, máximo valor para debugging.
- trace_id en respuesta: permite correlacionar log del backend con lo que vio el usuario.
- Métricas de latencia: p50, p95 por endpoint; cuello de botella típico en /ask.
- Traces distribuidos: opcional para v2; con un solo servicio puede bastar trace_id en logs.
Ejercicio 6: Escalado de ingestion
Si tuvieras 100,000 documentos, ¿cómo adaptarías el pipeline?
Solución:
- Batch más grande (2000-5000) para reducir overhead de ChromaDB.
- Paralelización: múltiples workers para embedding (respetando rate limits de OpenAI).
- Queue (Redis, SQS) para ingestion asíncrona.
- Checkpointing: guardar progreso para reanudar si falla.
- Considerar ChromaDB en modo servidor o migrar a Pinecone para esa escala.
Ejercicio 7: Diagrama de secuencia para /ask
Dibuja un diagrama de secuencia (Usuario → API → Retrieval → ChromaDB; API → Generation → OpenAI) mostrando el orden de llamadas para un request a /ask. Incluye qué pasa cuando retrieval devuelve 0 resultados.
Solución (texto):
- Usuario envía POST /ask con
{"question": "..."}. - API valida input, genera trace_id.
- API llama a Retrieval con query.
- Retrieval hace embed de query (OpenAI).
- Retrieval llama a ChromaDB.query con embedding.
- ChromaDB devuelve resultados (o vacío).
- Si vacío: Retrieval devuelve []; API devuelve fallback sin llamar a Generation.
- Si hay resultados: Retrieval devuelve docs; API llama a Generation con docs.
- Generation construye prompt, llama a OpenAI, recibe respuesta.
- API formatea respuesta con sources, trace_id, la devuelve al usuario.
Ejercicio 8: Interfaz abstracta para Vector Store
Define una interfaz (ABC o Protocol en Python) que encapsule las operaciones que necesitas del vector store: add(ids, documents, embeddings, metadatas) y query(embedding, top_k, where). ¿Qué cambiaría si mañana usas Pinecone?
Solución:
from abc import ABC, abstractmethod
from typing import List, Optional
class VectorStoreProtocol(ABC):
@abstractmethod
def add(self, ids: List[str], documents: List[str], embeddings: List[List[float]], metadatas: List[dict]) -> None: ...
@abstractmethod
def query(self, embedding: List[float], top_k: int = 5, where: Optional[dict] = None) -> dict: ...
Implementas ChromaDBStore(VectorStoreProtocol) para ChromaDB. Para Pinecone, implementas PineconeStore(VectorStoreProtocol). El retrieval solo depende de la interfaz, no del backend concreto.
Troubleshooting de arquitectura
"Todo funciona, pero no sabemos dónde falla"
Causa: Falta separación de responsabilidades e instrumentación por etapa.
Solución: Separa claramente ingestion, retrieval y generation. Añade logs en cada frontera (ej: "retrieve returned 5 docs", "generate called with 3 chunks"). Usa trace_id para seguir un request por todo el pipeline.
"Cada cambio rompe otra parte"
Causa: Contratos implícitos, acoplamiento alto.
Solución: Define contratos explícitos (esquemas, tipos). Escribe pruebas de integración por flujo (ingestion → retrieval → generate). Si cambias un contrato, actualiza tests y documentación.
"No sabemos cómo escalar ingestion"
Causa: Ingestion acoplada al serving, sin batches ni paralelismo.
Solución: Desacopla: ingestion como script/job independiente. Usa batches de 1K-2K, mide throughput. Considera workers paralelos para embeddings (con rate limiting).
"ChromaDB se llena o va lento"
Causa: Demasiados vectores, metadata pesada, o sin índices adecuados.
Solución: Revisa chunk_size (más grande = menos vectores). Reduce metadata a lo esencial. ChromaDB usa HNSW por defecto; para millones de vectores, evalúa migrar a Pinecone/Weaviate.
"Respuestas inventadas cuando no hay contexto"
Causa: Generation no valida calidad del retrieval, sigue generando sin evidencia.
Solución: Umbral de score en retrieval (ej: descartar < 0.5). En generation, si no hay chunks válidos, devolver mensaje explícito y no llamar al LLM para inventar.
Resumen
- La arquitectura tiene 5 componentes: ingestion, vector_store, retrieval, generation, api.
- El flujo de datos es lineal: documentos → ingestion → ChromaDB → retrieval → generation → API response.
- Decisiones clave: ChromaDB (gratis, aprendizaje), chunk_size=512 (balance), chunk_overlap=50 (continuidad), GPT-3.5-turbo (costo/latencia).
- Contratos explícitos entre componentes facilitan pruebas y evolución.
- Evita anti-patrones: funciones monolíticas, config hardcodeada, IDs no determinísticos, respuestas sin fallback.
- Documenta el diseño para facilitar migración a Guía #8 y onboarding de otros desarrolladores.
- Considera testing por componente: cada capa se testea aislada antes del test end-to-end.
- Usa configuración por entorno (dev vs prod) y un Decision Log para trazabilidad.
Checklist antes de implementar
Antes de pasar a la cápsula 03 (ingestion), verifica:
- Tienes el diagrama de arquitectura (papel o digital).
- Definiste el esquema de metadata (doc_id, source, chunk_index, title).
- Sabes qué hacer cuando retrieval devuelve 0 o scores bajos.
- Tienes config externalizada (variables de entorno).
- Documentaste al menos 2-3 decisiones en un DESIGN.md o README.
Si cumples los 5 puntos, estás listo para implementar el pipeline de ingestion.
Diagrama de dependencias entre módulos Python
api/
main.py → routes (ask, search, ingest, collections)
routes/
ask.py → services.retrieval, services.generation
search.py → services.retrieval
ingest.py → ingestion.pipeline
services/
retrieval.py → chromadb, openai
generation.py → openai
ingestion/
pipeline.py → loaders, chunking, embeddings, chromadb
loaders.py → (solo pathlib, pypdf si PDF)
chunking.py → langchain_text_splitters
embeddings.py → openai
Regla: Los servicios (retrieval, generation) no importan de ingestion. La API orquesta ambos. Así ingestion y serving pueden evolucionar de forma independiente.
Ejemplo de flujo de un request /ask (pseudocódigo)
request = {"question": "¿Qué es X?"}
trace_id = uuid4()
# 1. Validar
validate_length(question, max=2000)
# 2. Retrieval
query_embedding = openai.embeddings.create(input=[question])
results = chroma.query(query_embedding[0], n_results=5)
# 3. Filtrar por score
filtered = [r for r in results if r.score >= 0.5]
if not filtered:
return fallback_response(trace_id)
# 4. Generation
context = build_context(filtered.documents, filtered.metadatas)
prompt = f"Contexto:\n{context}\n\nPregunta: {question}"
answer = openai.chat.completions.create(messages=[...], prompt=prompt)
# 5. Respuesta
return {
"answer": answer,
"sources": format_sources(filtered),
"confidence": avg(filtered.scores),
"trace_id": trace_id,
}
Este pseudocódigo resume lo que implementarás en la cápsula 04. La separación en pasos claros facilita debugging y tests.
Próximos pasos
En la cápsula 03 implementarás el pipeline de ingestion: loaders para TXT/MD/RST, chunking con RecursiveCharacterTextSplitter, embeddings con OpenAI y almacenamiento en ChromaDB. Usa la arquitectura que definiste aquí como guía; si algún componente no encaja, ajusta el diseño antes de codear en exceso. La coherencia entre diseño e implementación evita refactors costosos.
Recursos adicionales
- Martin Fowler - Microservices — Principios de diseño desacoplado
- ChromaDB Collections — Estructura de datos
- OpenAI Embeddings — Modelos y límites
- RAG Best Practices — Patrones de RAG
- FastAPI Project Structure — Organización modular
- C4 Model — Diagramas de arquitectura por niveles
- Twelve-Factor App — Configuración, logs, deployment
- Vector DB Comparison (esta guía)
Tiempo estimado: 25-30 minutos
Siguiente: 03-pipeline-ingestion-1000-docs.md