Desmitificando un Sistema RAG Completo: Del Código a la Respuesta Inteligente

Parte 1: Introducción (El «Por Qué»)
¿Alguna vez has buscado algo en un montón de documentos y pensado: «Debe haber una mejor manera»?
Imagina esto: Eres el nuevo gerente de Recursos Humanos en una empresa mediana. Tu primer día, un empleado te pregunta: «¿Puedo tomar 3 días de vacaciones la próxima semana?»
Tienes que revisar:
- El manual del empleado 📕
- Las políticas de la empresa 📋
- El contrato del empleado 📄
- Las leyes laborales locales ⚖️
¡Podrías tardar horas! ¿Y si en lugar de buscar manualmente, pudieras preguntar a un asistente que ya conoce TODOS tus documentos?
El Problema Real que Este Código Resuelve
Las empresas hoy tienen un problema de información:
- Manuales de 200 páginas que nadie lee completamente
- Políticas actualizadas que pocos conocen
- Contratos con cláusulas específicas que se olvidan
- Documentación técnica que solo entienden los expertos
Resultado común:
- Empleados haciendo preguntas repetitivas
- Errores por desconocimiento de procedimientos
- Tiempo perdido buscando información
- Decisiones basadas en información incompleta
Analogía Simple: Tu «Google Personal» para Documentos Internos
Piensa en este código como el motor de búsqueda de Google, pero solo para TU información:
| Este Sistema RAG | |
|---|---|
| Busca en toda la web | Busca solo en TUS documentos |
| Resultados genéricos | Respuestas específicas a TU contexto |
| No sabe tus políticas internas | Conoce cada detalle de tu empresa |
| Público para todos | Privado y seguro para tu organización |
Ejemplo práctico:
- En Google: «política de vacaciones» → Resultados genéricos
- Con este sistema: «¿Cuántos días tengo?» → Respuesta EXACTA basada en TUS políticas
¿Qué Significa Realmente «RAG»?
RAG = Retrieval-Augmented Generation (Generación Aumentada por Recuperación)
graph LR
A[📚 Tus Documentos] --> B[🤖 Sistema RAG]
C[❓ Tu Pregunta] --> B
B --> D[✅ Respuesta Precisa<br/>+ Fuentes]Es más fácil entenderlo en 3 pasos:
La magia está en el «Aumentado»:
- No inventa respuestas (como haría un ChatGPT solo)
- No busca palabra por palabra (como un buscador tradicional)
- Encuentra lo RELEVANTE y lo EXPLICA en contexto
Impacto Real en Organizaciones
Para una PYME:
- Antes: Dueño gasta 2 horas buscando cláusulas en contratos
- Después: Pregunta y recibe respuesta en 30 segundos
Para un equipo legal:
- Antes: Abogados revisando manualmente cientos de páginas
- Después: Asistente que cita exactamente las leyes aplicables
Para soporte técnico:
- Antes: Técnicos buscando en manuales desactualizados
- Después: Sistema que siempre usa la versión más reciente
El valor no está en la tecnología, sino en lo que permite: tomar mejores decisiones más rápido con menos esfuerzo.
El «Efecto Multiplicador» del Conocimiento
Piensa en el conocimiento de tu organización como un tesoro enterrado:
- 💎 Documentos importantes = Diamantes en bruto
- 👷 Trabajadores = Mineros buscando a ciegas
- 🔦 Este sistema RAG = La lámpara que ilumina los diamantes
Sin el sistema: Cada empleado busca con una linterna pequeña, encontrando solo lo que está justo delante.
Con el sistema: Tienes reflectores que iluminan TODO el tesoro a la vez.
¿Por Qué Este Código Es Especial?
Muchos sistemas prometen «búsqueda inteligente», pero este código destaca por:
1. Inteligencia práctica:
No es solo buscar, es entender el contexto. Si preguntas por «vacaciones», entiende que te refieres a «días libres remunerados», no a «vacaciones escolares».
2. Resiliencia industrial:
Tiene sistemas de seguridad incorporados:
- Si falla el servicio de re-ranking, sigue funcionando
- Si la base de datos está lenta, usa caché
- Si hay muchos usuarios, se protege automáticamente
3. Transparencia total:
No es una «caja negra». Puedes ver exactamente:
- ¿Qué documentos encontró?
- ¿Por qué los consideró relevantes?
- ¿De dónde sacó cada parte de la respuesta?
graph LR
A[📚 Tus Documentos] --> B[🤖 Sistema RAG]
C[❓ Tu Pregunta] --> B
B --> D[✅ Respuesta Precisa<br/>+ Fuentes]Un Ejemplo que Todos Entendemos
Situación: Estás en el hospital y el doctor pregunta: «¿Este paciente es alérgico a algún medicamento?»
Sin el sistema:
- Enfermera busca en archivos físicos
- Revisa historiales en diferentes sistemas
- Pregunta a familiares (si están disponibles)
- Riesgo: Podría pasar algo por alto
Con el sistema:
- Doctor hace la pregunta
- Sistema busca en TODOS los registros simultáneamente
- Responde: «Sí, alergia grave a penicilina (registrado en ingreso 2023, página 4)»
- Resultado: Decisión más segura, más rápida
Lo Que NO Es Este Sistema
Para evitar malentendidos:
❌ NO es un reemplazo de empleados → Es una herramienta para que trabajen MEJOR
❌ NO sabe cosas que no le has enseñado → Solo conoce tus documentos
❌ NO toma decisiones por ti → Te da información para que decidas tú
❌ NO es mágico → Es tecnología que puedes entender y controlar
La Promesa: Democratizar el Acceso al Conocimiento
En esencia, este código nivela el campo de juego:
| Antes | Después |
|---|---|
| Solo los expertos saben dónde está todo | Cualquiera puede encontrar información |
| El conocimiento está en «silos» | El conocimiento fluye libremente |
| Se pierde tiempo buscando | Se gana tiempo actuando |
| Decisiones con información parcial | Decisiones con información completa |
La verdadera transformación: Pasar de «No sé dónde está esa información» a «Déjame preguntarle al sistema».
¿Listo para Ver Cómo Funciona por Dentro?
Ahora que entendemos POR QUÉ este sistema es valioso, estamos listos para explorar CÓMO funciona técnicamente.
En la siguiente parte, desarmaremos esta «caja de herramientas inteligente» y veremos cada componente, desde cómo almacena los documentos hasta cómo genera respuestas que parecen mágicas (pero son pura ingeniería bien hecha).
Pregunta para reflexionar: ¿Cuántas horas a la semana pierdes tú o tu equipo buscando información que YA existe en algún documento?
Diagrama asociado:
graph LR
A[📝 Pregunta] --> B[🔍 Buscar]
B --> C[📊 Re-rankear]
C --> D[🎯 Filtrar]
D --> E[📚 Completos]
E --> F[👁️ Inspeccionar]
F --> G[📋 Formatear]
G --> H[💬 Prompt]
H --> I[🤖 LLM]
I --> J[✂️ Parsear]
J --> K[✅ Respuesta]Parte 2: Arquitectura General – El «Esqueleto» de Nuestro Asistente Inteligente
Antes de Ver los Engranajes: Entendamos la Máquina Completa
Imagina que quieres entender cómo funciona un automóvil. No empiezas con el pistón número 3 – primero miras el coche completo: motor aquí, transmisión allá, ruedas abajo. Así funciona también nuestro sistema RAG.
Esta es la vista de pájaro que nos permite entender cómo todas las piezas trabajan juntas:
Diagrama principal:
graph TB
subgraph "Capa de Almacenamiento"
DB1[(PostgreSQL<br/>PGVector)] -.->|Embeddings| V[Vector Store]
DB2[(PostgreSQL<br/>ByteStore)] -.->|Documentos completos| BS[Byte Store]
end
subgraph "Capa de Procesamiento"
V -->|k=10, fetch_k=20| RET[Retriever MMR]
RET -->|Documentos iniciales| RR[Cohere Rerank]
RR -->|Documentos ordenados| FILT[Filtro por score]
FILT -->|IDs documentos| BS
BS -->|Documentos completos| FMT[Formateador]
end
subgraph "Capa de IA"
FMT -->|Contexto formateado| PROMPT[Plantilla Prompt]
PROMPT -->|Prompt final| LLM[Modelo Deepseek]
LLM -->|Respuesta| OUT[Output Parser]
end
USER[Usuario/Agente] -->|Pregunta| TOOL[Tool RAG]
TOOL --> RET
OUT -->|Respuesta| USER
style DB1 fill:#e1f5fe
style DB2 fill:#f3e5f5
style RET fill:#fff3e0
style RR fill:#e8f5e8
style LLM fill:#fce4ecLas Tres Capas Fundamentales: Una Arquitectura en Capas de Cebolla
Capa 1: 📚 Almacenamiento – La Memoria del Sistema
Piensa en esto como la biblioteca de una universidad:
| En una Biblioteca | En Nuestro Sistema |
|---|---|
| Catálogo de tarjetas | PGVector (índice inteligente) |
| Estantes con libros | ByteStore (documentos completos) |
| Bibliotecario que organiza | PostgreSQL (gestor de bases de datos) |
PGVector: Es el índice superinteligente. No busca por palabras exactas, sino por significado. Si buscas «automóvil», también encuentra «coche», «vehículo», «auto».
ByteStore: Aquí viven los documentos completos. El índice solo tiene fragmentos (como las reseñas en el catálogo), pero cuando necesitas el contenido completo, vas a los estantes.
¿Por qué dos almacenes diferentes?
- PGVector es rápido para buscar (como Google)
- ByteStore es completo para leer (como Wikipedia)
Capa 2: ⚙️ Procesamiento – La Línea de Ensamblaje
Esta es la «cocina» donde preparamos la información:
graph LR
A[Materia prima<br/>Documentos crudos] --> B[Lavado<br/>Retriever MMR]
B --> C[Cortado<br/>Re-ranking]
C --> D[Selección<br/>Filtrado]
D --> E[Cocción<br/>Formateo]
E --> F[Plato listo<br/>Contexto]
style A fill:#FFE0B2
style F fill:#A5D6A7El Proceso en Detalle:
- Retriever MMR (Maximal Marginal Relevance)
- Meta: Encontrar documentos relevantes PERO diversos
- Ejemplo: Si buscas «manzana», te da:
- 2 documentos sobre la fruta
- 1 sobre la empresa Apple
- 1 sobre la manzana en mitología
- Configuración clave:
lambda_mult=0.75(75% relevancia, 25% diversidad)
- Cohere Rerank – El «Chef Experto»
- Problema: La búsqueda inicial es buena, pero no perfecta
- Solución: Un modelo especializado que reordena resultados
- Analogía: Como cuando buscas «Python» y Google sabe si quieres la serpiente o el lenguaje de programación
- Filtro por Score – El Control de Calidad
- Umbral: 0.15 (como una nota mínima de 1.5/10)
- Límite máximo: 6 documentos (para no sobrecargar al LLM)
- Por qué: No todos los documentos «buenos» son «suficientemente buenos»
- Formateador – El Presentador
- Trabajo: Tomar documentos crudos y prepararlos para el LLM
- Ejemplo de transformación:textANTES: {content: «Los empleados…», metadata: {source: «doc.pdf», page: 15}} DESPUÉS: «Source: doc.pdf, Page: 15\n—\nLos empleados tienen derecho a…»
Capa 3: 🤖 Inteligencia – El Cerebro que Explica
Aquí es donde la información se convierte en conocimiento:
La Plantilla de Prompt – Las Instrucciones del Chef
python
# Ejemplo simplificado de lo que recibe el LLM:
"""
Eres un experto en recursos humanos. Responde basándote solo en esta información:
CONTEXTO:
{contexto_formateado}
PREGUNTA:
{pregunta_usuario}
Responde de manera clara y cita las fuentes.
"""
El Modelo Deepseek – El Chef Ejecutivo
- No inventa recetas: Solo usa los ingredientes que le damos
- Combina sabores: Sintetiza información de múltiples fuentes
- Presenta bien: Da respuestas claras y estructuradas
Output Parser – El Camarero que Sirve
- Limpia la presentación: Asegura formato consistente
- Verifica calidad: Detecta problemas en la respuesta
- Entrega al cliente: Formato listo para usar
El Flujo de Datos: Siguiendo la «Pregunta» en su Viaje
Vamos a seguir una pregunta real a través del sistema:
sequenceDiagram
participant U as 👤 Usuario
participant T as 🛠️ Tool RAG
participant PV as 🗃️ PGVector
participant CR as 🔄 Cohere
participant BS as 📦 ByteStore
participant LLM as 🧠 Deepseek
U->>T: "¿Cuántos días de vacaciones?"
Note over T,PV: CAPA 1: Almacenamiento
T->>PV: Busca embeddings similares
PV-->>T: 15 fragmentos relevantes
Note over T,CR: CAPA 2: Procesamiento
T->>CR: Reordena por relevancia real
CR-->>T: Top 5 documentos mejor ordenados
T->>T: Filtra (score ≥ 0.15, max=3)
T->>BS: Pide documentos completos por ID
BS-->>T: Contenido completo de 3 docs
T->>T: Formatea para LLM
Note over T,LLM: CAPA 3: Inteligencia
T->>LLM: Prompt + Contexto + Pregunta
LLM-->>T: "Según política página 15: 20 días..."
T-->>U: ✅ Respuesta clara con fuentesPunto clave: Cada capa agrega valor a los datos:
- Almacenamiento: Los tiene listos y organizados
- Procesamiento: Los selecciona y prepara
- Inteligencia: Los explica y contextualiza
Componentes de Soporte: Los «Ayudantes» Invisibles
El Sistema de Caché – La Memoria a Corto Plazo
python
_retriever_cache = {} # Guarda buscadores ya creados
_bytestore_cache = {} # Guarda almacenes ya conectados
Beneficio: Si 10 usuarios preguntan sobre «vacaciones», el sistema no recrea 10 veces las conexiones a la base de datos.
Circuit Breaker – El Fusible de Seguridad
Problema: ¿Y si Cohere (servicio externo) falla o está sobrecargado?
Solución: Un «fusible» que:
- Detecta el problema
- Se desconecta temporalmente (180 segundos por defecto)
- Usa un plan B (documentos sin re-ranking)
- Se reconecta automáticamente después
Analogía: Como cuando se va la luz y enciendes las velas en lugar de seguir intentando prender las luces.
Configuración por Entorno – Los «Botones de Ajuste»
bash
# .env file RAG_FILTER_THRESHOLD=0.15 # Qué tan estricto eres RAG_FILTER_MAX_DOCS=6 # Cuántos documentos usar COHERE_RERANK_COOLDOWN_SEC=180 # Tiempo de espera si falla
Ventaja: Puedes ajustar el sistema sin cambiar código.
Diseño Modular: El Principio LEGO™
Una de las bellezas de esta arquitectura es su modularidad:
graph TD
A[Módulo Retriever] -->|Puede reemplazarse| B[Azure AI Search]
A -->|O| C[Pinecone]
A -->|O| D[Qdrant]
E[Módulo Reranker] -->|Puede reemplazarse| F[Sin reranking]
E -->|O| G[Otro servicio]
H[Módulo LLM] -->|Puede reemplazarse| I[GPT-4]
H -->|O| J[Claude]
H -->|O| K[Gemini]
style A fill:#FFF3E0
style E fill:#E8F5E9
style H fill:#E3F2FD¿Por qué esto es importante?
- No estás atado a un proveedor específico
- Puedes actualizar partes sin romper el todo
- Puedes experimentar con diferentes combinaciones
- Mantenimiento más fácil: Arreglas una pieza, no todo el sistema
Patrones de Diseño Clave
Patrón Pipeline (Tubería)
Cada etapa recibe datos, los procesa, y los pasa a la siguiente. Como una línea de ensamblaje de automóviles.
Patrón Cache-Aside (Caché al Lado)
«Primero mira en la caché, si no está, créalo y guarda en caché para después.»
Patrón Circuit Breaker (Cortacircuitos)
«Si algo falla mucho, déjalo descansar un rato antes de intentar otra vez.»
Patrón Decorator (Decorador)
rag_tool_decorator envuelve funcionalidad básica para crear herramientas específicas.
Escalabilidad: Creciendo sin Dolor
Esta arquitectura escala en tres dimensiones:
1. Escalabilidad Vertical (Usuarios)
- 1 usuario → 1000 usuarios
- Cómo: Más instancias del servicio RAG
2. Escalabilidad Horizontal (Documentos)
- 100 documentos → 1,000,000 documentos
- Cómo: PostgreSQL ya maneja esto bien
3. Escalabilidad Funcional (Características)
- Solo búsqueda → +re-ranking → +filtrado → +análisis
- Cómo: Agregas módulos al pipeline
Resumen: Por Qué Esta Arquitectura Funciona
| Principio | Ejemplo en el Código | Beneficio |
|---|---|---|
| Separación de preocupaciones | 3 capas claras | Mantenimiento más fácil |
| Fail-fast (falla rápido) | Circuit breaker | Resiliencia ante fallos |
| Caching inteligente | _retriever_cache | Rendimiento mejorado |
| Configuración externa | Variables .env | Flexibilidad operativa |
| Modularidad | Piezas intercambiables | Futura adaptabilidad |
La Belleza del Diseño: Simplicidad en la Complejidad
Lo más impresionante de esta arquitectura es que oculta complejidad. Como usuario, solo ves:
ENTRADA: ❓ Pregunta
SALIDA: ✅ Respuesta útil
Pero detrás de esa simpleza hay un sistema orquestado de búsqueda, filtrado, procesamiento y síntesis que trabaja en armonía.
En la próxima parte: Bajaremos un nivel y veremos cómo funciona exactamente cada componente del pipeline. Empezaremos siguiendo una pregunta real a través de cada etapa, desde que toca la puerta hasta que sale como respuesta clara y útil.
Reflexión: ¿Cuántos sistemas en tu trabajo son tan modularmente diseñados que puedes cambiar piezas sin romper todo?
Parte 3: El Pipeline Paso a Paso – La Línea de Ensamblaje Inteligente
Diagrama de transformación:
graph TD
subgraph "Estado Inicial"
S1["Input: Pregunta del usuario"]
end
S1 --> S2["retrieved_docs: 10 documentos"]
S2 --> S3["reranked_docs: 5 documentos ordenados"]
S3 --> S4["filtered_docs: 3 documentos filtrados"]
S4 --> S5["full_docs: Documentos completos"]
S5 --> S6["context: Texto formateado"]
S6 --> S7["Prompt: Contexto + Pregunta"]
S7 --> S8["Respuesta: Generada por LLM"]
S8 --> S9["Output: Respuesta final"]
style S1 fill:#E1F5FE
style S2 fill:#F3E5F5
style S3 fill:#FFF3E0
style S4 fill:#E8F5E9
style S5 fill:#FFF8E1
style S6 fill:#F1F8E9
style S7 fill:#FCE4EC
style S8 fill:#E3F2FD
style S9 fill:#C8E6C9La Pregunta de Ejemplo: Un Caso Real
Seguiremos esta pregunta de un empleado ficticio:
«Tengo 3 años en la empresa, ¿cuántos días de vacaciones me corresponden este año?»
Contexto: La empresa tiene documentos sobre políticas de vacaciones, contratos laborales, manuales del empleado, y leyes locales.
graph TD
subgraph "ESTACIÓN 1<br/>Recibiendo el Pedido"
S1["📥 Pregunta Entrante:<br/>'¿Cuántos días de vacaciones?'"]
end
S1 --> S2["🔍 ESTACIÓN 2<br/>Búsqueda inicial<br/>10 documentos encontrados"]
S2 --> S3["📊 ESTACIÓN 3<br/>Re-ordenamiento<br/>5 documentos rankeados"]
S3 --> S4["🎯 ESTACIÓN 4<br/>Control de calidad<br/>3 documentos filtrados"]
S4 --> S5["📚 ESTACIÓN 5<br/>Material completo<br/>Documentos íntegros"]
S5 --> S6["👁️ ESTACIÓN 6<br/>Inspección<br/>'Todo en orden'"]
S6 --> S7["📋 ESTACIÓN 7<br/>Embalaje<br/>Contexto formateado"]
S7 --> S8["🏷️ ESTACIÓN 8<br/>Etiquetado<br/>Prompt preparado"]
S8 --> S9["🤖 ESTACIÓN 9<br/>Fabricación<br/>Respuesta generada"]
S9 --> S10["✂️ ESTACIÓN 10<br/>Acabado<br/>Respuesta pulida"]
S10 --> S11["📦 Producto Final:<br/>Respuesta clara con fuentes"]
style S1 fill:#E1F5FE
style S11 fill:#C8E6C9Estación 1: La Recepción – {"question": RunnablePassthrough}
python
# Lo que sucede aquí:
entrada = {"question": "Tengo 3 años en la empresa, ¿cuántos días de vacaciones me corresponden este año?"}
# RunnablePassthrough simplemente pasa esto al siguiente paso
Analogía: Como cuando llegas a McDonald’s y dices tu pedido. El cajero lo escribe en su sistema para que la cocina lo vea.
Estado del dato:
text
{
"question": "Tengo 3 años en la empresa, ¿cuántos días de vacaciones..."
}
¿Por qué es importante?:
- Establece el formato estándar que usará todo el pipeline
- Es el punto de entrada único para cualquier pregunta
- Garantiza que todos los pasos reciban los datos en el mismo formato
Estación 2: La Búsqueda – retrieved_docs = itemgetter("question") | retriever
Aquí es donde la magia comienza. El sistema busca documentos relevantes:
python
# El código llama a:
retriever.get_relevant_documents("Tengo 3 años...")
¿Cómo busca realmente?
| Lo que BUSCA el usuario | Lo que ENTIENDE el sistema |
|---|---|
| «días de vacaciones» | → «tiempo libre remunerado», «descanso anual», «licencia» |
| «3 años en la empresa» | → «antigüedad», «tiempo de servicio», «permanencia» |
Resultado típico:
python
retrieved_docs = [
Document(page_content="Política: 15 días para 1-3 años...", metadata={...}),
Document(page_content="Contrato: vacaciones según antigüedad...", metadata={...}),
Document(page_content="Manual: proceso para solicitar...", metadata={...}),
# ... 7 documentos más
]
Problema encontrado:
- Documento 1: Política actual (score: 0.85)
- Documento 4: Política antigua del 2020 (score: 0.82)
- Documento 7: Ley general (score: 0.79)
La búsqueda inicial es buena pero no perfecta – encuentra cosas relevantes, pero no siempre en el orden correcto.
Estado actual del dato:
text
{
"question": "Tengo 3 años...",
"retrieved_docs": [Document1, Document2, ..., Document10]
}
Estación 3: El Re-ordenamiento – reranked_docs = lambda x: safe_rerank(...)
El «segundo filtro» inteligente:
python
# Cohere Rerank analiza cada documento vs la pregunta: # Pregunta: "Tengo 3 años... días de vacaciones" # # Documento 1: "15 días para 1-3 años de antigüedad" → Score: 0.95 ✓ # Documento 4: "20 días según política 2020" → Score: 0.65 ✗ (obsoleto) # Documento 7: "Ley: mínimo 15 días" → Score: 0.80 ✓
¿Qué hace Cohere que el buscador inicial no hace?
| Buscador Vectorial | Cohere Rerank |
|---|---|
| Busca por similitud semántica | Busca por relevancia contextual |
| «vacaciones» ≈ «tiempo libre» | Entiende que «3 años» es clave |
| Basado en embeddings estáticos | Analiza relación pregunta-documento |
| Más rápido, menos preciso | Más lento, más preciso |
Resultado después del re-ranking:
python
reranked_docs = [
# ORDEN CORREGIDO:
Document1 (política actual, score: 0.95), # ↑ Subió del lugar 1
Document7 (ley general, score: 0.80), # ↑ Subió del lugar 7
Document2 (contrato, score: 0.78), # ↓ Bajó del lugar 2
# Document4 (política antigua) desaparece del top
]
El Circuit Breaker en acción:
python
# ¿Y si Cohere está caído?
if cohere_falló_recientemente:
print("⚠️ Usando documentos sin re-ranking (fallback activado)")
return retrieved_docs # Sigue con lo que tenía
else:
return documentos_rerankeados
Estado actual:
text
{
"question": "...",
"retrieved_docs": [...],
"reranked_docs": [Doc1(0.95), Doc7(0.80), Doc2(0.78), ...]
}
Estación 4: El Control de Calidad – filtered_docs = lambda x: filter_reranked_docs_local(...)
La «prueba de estrés» de los documentos:
python
# Parámetros configurables:
UMBRAL_MINIMO = 0.15 # "Si no sacas al menos 1.5/10, no pasas"
MAXIMO_DOCUMENTOS = 6 # "No podemos procesar más de 6"
# Proceso de filtrado:
for documento in reranked_docs:
if documento.score >= UMBRAL_MINIMO:
documentos_aprobados.append(documento)
if len(documentos_aprobados) == MAXIMO_DOCUMENTOS:
break # ¡Basta! No más documentos
Lo que realmente sucede:
| Documento | Score | Decisión | Razón |
|---|---|---|---|
| Doc1 | 0.95 | ✅ APROBADO | Excelente relevancia |
| Doc7 | 0.80 | ✅ APROBADO | Buena relevancia |
| Doc2 | 0.78 | ✅ APROBADO | Aceptable |
| Doc3 | 0.45 | ✅ APROBADO | Cumple el mínimo |
| Doc4 | 0.12 | ❌ RECHAZADO | Score demasiado bajo |
| Doc5 | 0.60 | ✅ APROBADO | |
| Doc6 | 0.55 | ✅ APROBADO | (último, límite 6) |
| Doc8 | 0.50 | ❌ RECHAZADO | Límite alcanzado |
Por qué este filtrado es CRÍTICO:
- Evita «contaminación»: Documentos poco relevantes pueden confundir al LLM
- Controla costos: Menos documentos = menos tokens = respuesta más barata
- Mejora calidad: El LLM se enfoca en lo realmente importante
Estado actual:
text
{
"question": "...",
"retrieved_docs": [...],
"reranked_docs": [...],
"filtered_docs": [Doc1, Doc7, Doc2, Doc3, Doc5, Doc6] # 6 documentos
}
Estación 5: El Almacén Central – full_docs = lambda x: get_full_documents(...)
¡Momento de la verdad! Hasta ahora solo teníamos fragmentos, ahora obtenemos los documentos completos:
python
# Problema: Los documentos del vectorstore son FRAGMENTOS cortos # Ejemplo fragmento: "Política: 15 días para 1-3 años de antigüedad..." # Solución: Buscar en ByteStore la versión COMPLETA documentos_completos = bytestore.mget([id_doc1, id_doc7, id_doc2, ...])
La transformación:
python
# ANTES (fragmento):
fragmento = Document(
page_content="...15 días para 1-3 años...",
metadata={"source": "politica.pdf", "page": "15", "doc_id": "123"}
)
# DESPUÉS (documento completo):
documento_completo = Document(
page_content="""CAPÍTULO 4: VACACIONES
Artículo 15: Los empleados con 1 a 3 años de antigüedad...
Tienen derecho a 15 días hábiles de vacaciones anuales...
Procedimiento: Solicitar con 30 días de anticipación...
Excepciones: Casos especiales...""",
metadata={"source": "politica.pdf", "page": "15-18", "doc_id": "123"}
)
¿Por qué necesitamos documentos completos?
- Contexto más rico: El LLM entiende mejor
- Respuestas más precisas: Menos malentendidos
- Citas exactas: Puede referenciar secciones específicas
Estado actual:
text
{
"question": "...",
"retrieved_docs": [...],
"reranked_docs": [...],
"filtered_docs": [...],
"full_docs": [DocCompleto1, DocCompleto7, ...] # ¡Completos!
}
Estación 6: La Inspección – RunnableLambda(inspect)
La «ventana de control» donde miramos qué está pasando:
python
def inspect(input_dict):
print("\n--- Documentos Rerankeados ---")
for i, doc in enumerate(reranked_docs):
print(f" Doc {i+1}:")
print(f" Source: {doc.metadata.get('source')}")
print(f" Page: {doc.metadata.get('page')}")
print(f" Score: {doc.metadata.get('score'):.4f}")
print(f" Content: {doc.page_content[:100]}...")
Output real que vería un desarrollador:
text
--- Documentos Rerankeados ---
Doc 1:
Source: politica_vacaciones_2024.pdf
Page: 15
Score: 0.9543
Content: CAPÍTULO 4: VACACIONES Artículo 15: Los empleados con 1 a 3 años...
Doc 2:
Source: ley_laboral.pdf
Page: 42
Score: 0.8012
Content: Artículo 42: Todo trabajador tiene derecho a un período anual...
¿Para qué sirve esta inspección?
- Debugging: Si algo falla, sabemos dónde
- Optimización: Podemos ver si el filtrado es muy estricto/laxo
- Transparencia: Sabemos exactamente qué documentos se usaron
El dato NO cambia aquí, solo se muestra información.
Estación 7: El Formateo – context = lambda x: format_docs_for_llm_prompt_with_links()
Preparamos la «comida» para el LLM:
python
# Transformamos documentos en texto estructurado
contexto_formateado = ""
for doc in full_docs:
fuente = doc.metadata.get("source", "N/A")
pagina = doc.metadata.get("page", "N/A")
contenido = doc.page_content
# Formato final:
contexto_formateado += f"Source: {fuente}, Page: {pagina}\n"
contexto_formateado += "---\n"
contexto_formateado += f"{contenido}\n\n"
Resultado:
text
Source: politica_vacaciones_2024.pdf, Page: 15 --- CAPÍTULO 4: VACACIONES Artículo 15: Los empleados con 1 a 3 años de antigüedad tienen derecho a 15 días hábiles de vacaciones anuales. El período debe solicitarse con 30 días de anticipación. Source: ley_laboral.pdf, Page: 42 --- Artículo 42: Todo trabajador tiene derecho a un período anual de vacaciones remuneradas de al menos 15 días hábiles. Las empresas pueden ofrecer beneficios superiores.
Característica inteligente: Si la fuente es una URL, la convierte en enlace Markdown:
text
Source: [politicas_empresa.com](politicas_empresa.com), Page: 15
Estado actual:
text
{
"question": "...",
"retrieved_docs": [...],
"reranked_docs": [...],
"filtered_docs": [...],
"full_docs": [...],
"context": "Source: politica.pdf, Page: 15\n---\nCAPÍTULO 4..." # ¡Formateado!
}
Estación 8: La Preparación Final – {"context": itemgetter("context"), "question": itemgetter("question")}
Últimos ajustes antes de la cocina:
python
# Creamos el diccionario FINAL que recibirá el prompt
datos_para_prompt = {
"context": contexto_formateado, # Extraído con itemgetter
"question": pregunta_original # Extraído con itemgetter
}
¿Por qué este paso parece trivial pero es importante?
- Estructura consistente: El prompt espera EXACTAMENTE estas claves
- Limpieza: Nos aseguramos que solo van estos dos campos
- Prevención: Evitamos que campos internos lleguen al LLM
Estado final antes del LLM:
text
{
"context": "Source: politica.pdf...\n---\nCAPÍTULO 4...",
"question": "Tengo 3 años en la empresa, ¿cuántos días de vacaciones me corresponden?"
}
Todo listo para el cerebro artificial ✅
Estación 9: La Generación – ChatPromptTemplate | model
¡El momento de la creación!:
python
# Plantilla del prompt (simplificada):
plantilla = """
Eres un asistente experto en recursos humanos.
Responde la pregunta del usuario basándote SOLO en la información proporcionada.
INFORMACIÓN DISPONIBLE:
{context}
PREGUNTA DEL USUARIO:
{question}
Responde de manera clara y concisa. Si la información no está disponible,
di que no lo sabes. Siempre cita las fuentes específicas.
"""
# El LLM recibe:
prompt_completo = plantilla.format(
context=datos_para_prompt["context"],
question=datos_para_prompt["question"]
)
# Y genera:
respuesta_bruta = llm.generate(prompt_completo)
Lo que realmente «ve» el LLM:
text
Eres un asistente experto en recursos humanos... INFORMACIÓN DISPONIBLE: Source: politica_vacaciones_2024.pdf, Page: 15 --- CAPÍTULO 4: VACACIONES Artículo 15: Los empleados con 1 a 3 años de antigüedad tienen derecho a 15 días hábiles de vacaciones anuales... Source: ley_laboral.pdf, Page: 42 --- Artículo 42: Todo trabajador tiene derecho a un período anual de vacaciones remuneradas de al menos 15 días hábiles... PREGUNTA DEL USUARIO: Tengo 3 años en la empresa, ¿cuántos días de vacaciones me corresponden? Responde de manera clara y concisa...
Estación 10: El Acabado Final – StrOutputParser()
Últimos retoques para una respuesta perfecta:
python
# El LLM podría devolver algo como: respuesta_sucia = """ Basándome en la información proporcionada: Según la política de vacaciones 2024 (página 15), los empleados con 1 a 3 años de antigüedad tienen derecho a 15 días hábiles de vacaciones anuales. Adicionalmente, la ley laboral (artículo 42) establece un mínimo de 15 días. Por lo tanto, te corresponden 15 días de vacaciones este año. """ # StrOutputParser se encarga de: # 1. Limpiar espacios extra # 2. Asegurar encoding correcto # 3. Formato consistente respuesta_limpia = clean_response(respuesta_sucia)
Resultado final para el usuario:
text
Según la política de vacaciones 2024 (página 15) y la ley laboral (artículo 42), como tienes 3 años de antigüedad en la empresa, te corresponden 15 días hábiles de vacaciones anuales. Fuentes: - politica_vacaciones_2024.pdf, página 15 - ley_laboral.pdf, página 42
Resumen del Viaje Completo
graph LR
A["📥 Pregunta cruda"] --> B["🔍 10 documentos encontrados"]
B --> C["📊 5 documentos rankeados"]
C --> D["🎯 3 documentos filtrados"]
D --> E["📚 Documentos completos"]
E --> F["👁️ Inspección (debug)"]
F --> G["📋 Contexto formateado"]
G --> H["🏷️ Prompt preparado"]
H --> I["🤖 Respuesta generada"]
I --> J["✂️ Respuesta pulida"]
J --> K["📦 Respuesta final + fuentes"]
style A fill:#E1F5FE
style K fill:#C8E6C9Lo más importante: En cada etapa, los datos se transforman y mejoran:
- De pregunta → a documentos relevantes
- De documentos → a documentos ordenados por relevancia
- De documentos ordenados → a documentos filtrados
- De fragmentos → a documentos completos
- De documentos → a contexto estructurado
- De contexto + pregunta → a respuesta inteligente
Puntos Clave del Pipeline
1. Progresión incremental
Cada paso depende del anterior, pero también puede funcionar si el anterior falla (graceful degradation).
2. Múltiples capas de calidad
- Búsqueda inicial (MMR)
- Re-ranking (Cohere)
- Filtrado por score
- Selección de documentos completos
3. Transparencia total
En cualquier momento puedes inspeccionar qué está pasando.
4. Resiliencia integrada
Si algo falla, el sistema continúa funcionando (quizás con calidad reducida, pero funcionando).
En la próxima parte: Veremos un caso práctico completo donde seguiremos esta misma pregunta desde que el usuario la formula hasta que recibe la respuesta, mostrando exactamente qué sucede en cada componente y cómo se toman las decisiones en tiempo real.
Parte 4: Componentes Clave Explicados – Los Engranajes de la Máquina
Estructura: Tarjetas de conceptos
Diagrama de conceptos:
mindmap
root((Sistema RAG))
Recuperación
:::highlight
PGVector
Embeddings
Búsqueda MMR
Retriever Cache
Optimización
Reutilización
Re-ranking
:::highlight
Cohere Rerank
Modelo multilingüe
Reordenamiento
Circuit Breaker
Protección
Fallback seguro
Almacenamiento
PostgresByteStore
Documentos completos
Recuperación por ID
Caché distribuido
Eficiencia
Velocidad
Generación
LLM Deepseek
Modelo de lenguaje
Síntesis
Prompt Engineering
Contexto
InstruccionesDesmontando el Sistema: Conoce a los «Trabajadores Especializados»
Ahora que hemos visto el flujo completo, es momento de conocer a los protagonistas individuales. Piensa en esto como conocer al equipo detrás de una obra de teatro: cada uno tiene un rol específico, una especialidad única, y juntos crean la magia.
Vamos a explorar cada componente como si fuera un miembro de un equipo de élite:
mindmap
root((Equipo RAG de Élite))
RECUPERADORES
:::specialist
PGVector
:::expert
El Buscador Semántico
Trabaja con embeddings
Búsqueda por significado
Retriever Cache
:::optimizer
El Memorioso
Evita trabajo repetido
Acelera respuestas
RE-RANKEADORES
:::analyst
Cohere Rerank
:::expert
El Crítico Experto
Modelo multilingüe v3.0
Reordenamiento inteligente
Circuit Breaker
:::protector
El Guardián
Protege APIs externas
Sistema anti-colapso
ALMACENADORES
:::librarian
PostgresByteStore
:::archivist
El Archivista Completo
Guarda documentos íntegros
Recuperación por ID
OpenAI Embeddings
:::translator
El Traductor Universal
text-embedding-3-large
Convierte texto a números
GENERADORES
:::brain
LLM Deepseek
:::thinker
El Sintetizador
Explica y contextualiza
Respuestas naturales
Prompt Engineering
:::director
El Director de Orquesta
Instrucciones precisas
Contexto estructurado
AUXILIARES
:::support
Filtro por Score
:::quality
El Control de Calidad
Umbral mínimo: 0.15
Límite máximo: 6 docs
Inspector de Debug
:::monitor
El Supervisor
Muestra proceso interno
Transparencia total1. PGVector – El Buscador Semántico Inteligente
¿Qué problema resuelve?
Búsqueda tradicional: Si buscas «coche», solo encuentra documentos con la palabra exacta «coche».
PGVector: Si buscas «coche», también encuentra documentos sobre «automóvil», «vehículo», «auto» – ¡incluso si nunca mencionan la palabra «coche»!
Cómo funciona realmente:
python
# Paso 1: Convertir texto a números (embeddings)
texto = "Los empleados tienen 15 días de vacaciones"
embedding = [0.12, -0.45, 0.78, ...] # 1536 números que representan el significado
# Paso 2: Guardar en PostgreSQL con extensión vector
INSERT INTO documentos (contenido, embedding)
VALUES ('texto...', '[0.12, -0.45, 0.78, ...]')
# Paso 3: Buscar similitud
SELECT contenido
FROM documentos
ORDER BY embedding <=> '[0.11, -0.44, 0.79, ...]' # Busca vectores similares
LIMIT 10;
La Magia de los Embeddings:
Un embedding es como un «ADN numérico» del significado de un texto:
| Texto | Embedding (simplificado) | Lo que representa |
|---|---|---|
| «coche» | [0.8, 0.1, -0.3, 0.4] | Vehículo, transporte, 4 ruedas |
| «automóvil» | [0.7, 0.2, -0.4, 0.3] | Similar a coche, pero más formal |
| «bicicleta» | [0.2, 0.9, -0.1, 0.5] | Transporte, 2 ruedas, ejercicio |
| «manzana» | [-0.3, 0.1, 0.9, 0.2] | Fruta, comida, roja |
PGVector calcula la «distancia» entre estos vectores. «Coche» y «automóvil» están cerca. «Coche» y «manzana» están lejos.
Configuración MMR (Maximal Marginal Relevance):
python
search_kwargs = {
"k": 10, # 10 resultados finales
"fetch_k": 20, # Considera 20 candidatos primero
"lambda_mult": 0.75 # 75% relevancia, 25% diversidad
}
Ejemplo práctico:
Buscar «Python»:
- Solo relevancia: 10 documentos sobre el lenguaje de programación
- Con MMR: 8 sobre programación + 1 sobre la serpiente + 1 sobre Monty Python
Ventaja: Evita resultados duplicados y da perspectiva más amplia.
2. Cohere Rerank – El Crítico Experto
El Problema del «Bueno pero no Perfecto»
Imagina que buscas recetas de «tarta de manzana saludable»:
PGVector encuentra:
- Tarta de manzana tradicional (score: 0.90)
- Tarta de manzana con azúcar (0.88)
- Manzanas al horno (0.85)
- Tarta de manzana sin azúcar (0.82) ← ¡Lo que realmente querías!
PGVector lo puso 4to porque «saludable» no aparecía explícitamente.
Cohere Rerank reordena:
- Tarta de manzana sin azúcar (0.95) ← ¡Ahora primero!
- Manzanas al horno (0.85)
- Tarta tradicional (0.70)
Cómo funciona técnicamente:
python
class CohereRerank:
def __init__(self):
self.model = "rerank-multilingual-v3.0" # Entiende múltiples idiomas
self.top_n = 5 # Solo los 5 mejores
def compress_documents(self, documents, query):
# Envía a Cohere API: documentos + pregunta
# Cohere analiza CADA documento vs la pregunta
# Devuelve nuevos scores y orden
return documentos_ordenados_por_relevancia_real
El Circuit Breaker – El Sistema Anti-Colapso:
python
# Variables globales que actúan como "fusibles"
COHERE_DISABLED_UNTIL = 0.0 # Hasta cuándo está desactivado
COHERE_FAIL_COUNT = 0 # Cuántas veces ha fallado
COHERE_RERANK_COOLDOWN_SEC = 180 # 3 minutos de "descanso"
def safe_rerank(docs, query, reranker):
# ¿Estamos en "tiempo de espera"?
if time.now() < COHERE_DISABLED_UNTIL:
return docs # Usa documentos sin re-ranking
try:
# Intenta re-rankear
resultado = reranker.compress_documents(docs, query)
COHERE_FAIL_COUNT = 0 # ¡Éxito! Reinicia contador
return resultado
except Exception as e:
# ¡Error! Incrementa contador
COHERE_FAIL_COUNT += 1
# ¿Es error 429 (demasiadas peticiones)?
if "429" in str(e):
print("⚠️ Cohere sobrecargado, descansando 3 minutos")
# Activa el circuit breaker
COHERE_DISABLED_UNTIL = time.now() + 180
return docs # Fallback: documentos originales
Por qué esto es brillante:
- Evita cascadas de fallos: Un problema no colapsa todo el sistema
- Auto-recuperación: Después de 3 minutos, intenta otra vez
- Experiencia consistente: El usuario sigue recibiendo respuestas (quizás de menor calidad, pero respuestas)
3. PostgresByteStore – El Archivista Completo
El Problema de los Fragmentos:
PGVector solo guarda fragmentos cortos (100-200 palabras) para buscar rápido. Es como tener solo los resúmenes de los libros.
Problema: Cuando el LLM necesita responder, ¡necesita el libro completo!
La Solución de Dos Niveles:
graph TD
A[Documento Completo<br/>50 páginas] --> B
subgraph "Nivel 1: PGVector (Búsqueda rápida)"
B[Fragmento 1: Páginas 1-2] --> C[🔍 Buscar aquí]
D[Fragmento 2: Páginas 15-16] --> C
E[Fragmento 3: Páginas 42-43] --> C
end
subgraph "Nivel 2: ByteStore (Contenido completo)"
F[📦 Almacena documento COMPLETO] --> G[ID: doc_123]
end
C -->|Encuentra fragmento relevante| H[Fragmento ID: doc_123-p15]
H -->|"Pide: 'dame doc_123 completo'"| G
G --> I[📄 Entrega 50 páginas completas]
style F fill:#E1F5FE
style I fill:#C8E6C9Cómo funciona el ByteStore:
python
class PostgresByteStore:
def __init__(self, conninfo, collection_name):
# Conexión a PostgreSQL (la MISMA base, tabla diferente)
self.connection = psycopg.connect(conninfo)
def mget(self, doc_ids):
# Recupera MÚLTIPLES documentos a la vez
query = "SELECT content FROM documents WHERE id IN %s"
resultados = self.connection.execute(query, (tuple(doc_ids),))
return [Document(content=row[0]) for row in resultados]
Ventajas clave:
- Eficiencia: Una consulta para todos los documentos necesarios
- Consistencia: Misma base de datos que PGVector
- Completitud: Acceso a TODO el contenido, no solo fragmentos
4. El Sistema de Caché – El Memorioso Eficiente
El Problema de la Reinvención:
Imagina que 100 usuarios preguntan sobre «vacaciones». Sin caché:
- Crear retriever para «recursos_humanos» → 100 veces
- Conectar a ByteStore → 100 veces
- Configurar embeddings → 100 veces
¡Gasto inmenso de recursos!
La Solución de Caché:
python
# Diccionarios globales que actúan como "memoria"
_retriever_cache = {} # {"recursos_humanos": retriever_ya_creado}
_bytestore_cache = {} # {"recursos_humanos": bytestore_ya_conectado}
def get_retriever(collection_name):
# PRIMERO: ¿Ya existe en caché?
if collection_name in _retriever_cache:
print(f"✅ Retriever para '{collection_name}' obtenido del caché")
return _retriever_cache[collection_name]
# SEGUNDO: Si no existe, créalo
print(f"🔄 Creando nuevo retriever para '{collection_name}'")
retriever = crear_retriever_complejo(collection_name)
# TERCERO: Guarda en caché para el futuro
_retriever_cache[collection_name] = retriever
return retriever
Impacto Real:
| Sin Caché | Con Caché |
|---|---|
| 1000 ms por petición | 10 ms después del primero |
| 100 conexiones a DB | 1 conexión reutilizada |
| Alto consumo CPU | CPU mínima |
| Lento para usuarios | Instantáneo para todos |
Beneficio oculto: También cachea embeddings. Si dos preguntas usan palabras similares, no recalcula embeddings desde cero.
5. Filtro por Score – El Control de Calidad Estricto
La Línea Roja de la Calidad:
python
def filter_reranked_docs_local(reranked_docs, threshold=0.15, max_docs=6):
documentos_filtrados = []
for documento in reranked_docs:
# Extrae el score (diferentes nombres posibles)
score = (documento.metadata.get("relevance_score") or
documento.metadata.get("score") or
0.0)
# PRUEBA 1: ¿Supera el umbral mínimo?
if score < threshold:
continue # ❌ Rechazado
# PRUEBA 2: ¿Ya alcanzamos el límite?
if len(documentos_filtrados) >= max_docs:
break # 🛑 Basta, no más documentos
documentos_filtrados.append(documento) # ✅ Aprobado
return documentos_filtrados
La Psicología del Umbral 0.15:
¿Por qué 0.15 y no 0.50 o 0.00?
| Umbral | Efecto | Analogía |
|---|---|---|
| 0.00 | Todo pasa | «Aprobado a todos» → Caos |
| 0.15 | Filtro ligero | «Solo los que prestan atención» |
| 0.50 | Muy estricto | «Solo los mejores de la clase» |
| 0.80 | Ultra estricto | «Solo genios» → Muy pocos documentos |
0.15 es el punto dulce: Elimina lo claramente irrelevante sin ser tan estricto que deje al LLM sin información.
El Límite de 6 Documentos:
¿Por qué 6 y no 20 o 3?
python
# Cálculo de tokens (aproximado): 1 documento promedio = 500 tokens 6 documentos = 3,000 tokens + Pregunta = 50 tokens + Prompt = 200 tokens + Respuesta = 300 tokens ---------------------- TOTAL ≈ 3,550 tokens # Límites típicos de modelos: Deepseek contexto = 32,000 tokens # ¡Nos quedamos en solo ~11%!
Beneficios:
- Respuestas más rápidas: Menos tokens = menos tiempo de procesamiento
- Costos más bajos: Menos tokens = menos dinero en APIs
- Respuestas más enfocadas: El LLM no se distrae con información marginal
- Calidad consistente: Siempre la misma cantidad de contexto
6. El Inspector de Debug – Los Ojos del Sistema
La Ventana a la «Cocina»:
python
def inspect(input_dict):
print("\n" + "="*50)
print("🔍 DEBUG: Documentos que pasaron el filtro")
print("="*50)
documentos = input_dict.get("reranked_docs", [])
if not documentos:
print("❌ No se encontraron documentos relevantes")
return input_dict
for i, doc in enumerate(documentos[:3]): # Solo muestra top 3
metadata = doc.metadata or {}
print(f"\n📄 Documento #{i+1}:")
print(f" 📍 Fuente: {metadata.get('source', 'Desconocida')}")
print(f" 📄 Página: {metadata.get('page', 'N/A')}")
# Score formateado profesionalmente
score = metadata.get('relevance_score', 0)
print(f" ⭐ Score: {score:.3f} {'✓' if score >= 0.15 else '✗'}")
# Vista previa del contenido
contenido = doc.page_content[:150] + "..." if len(doc.page_content) > 150 else doc.page_content
print(f" 📝 Contenido: {contenido}")
print("\n" + "="*50)
return input_dict # Importante: Devuelve los datos sin cambios
Lo que ve un desarrollador:
text
================================================== 🔍 DEBUG: Documentos que pasaron el filtro ================================================== 📄 Documento #1: 📍 Fuente: politica_vacaciones_2024.pdf 📄 Página: 15 ⭐ Score: 0.954 ✓ 📝 Contenido: CAPÍTULO 4: VACACIONES. Los empleados con 1 a 3 años de antigüedad tienen derecho a 15 días hábiles... 📄 Documento #2: 📍 Fuente: ley_laboral.pdf 📄 Página: 42 ⭐ Score: 0.801 ✓ 📝 Contenido: Artículo 42: Todo trabajador tiene derecho a un período anual de vacaciones remuneradas... 📄 Documento #3: 📍 Fuente: contrato_tipo.pdf 📄 Página: 8 ⭐ Score: 0.112 ✗ 📝 Contenido: Cláusula 3.2: Las vacaciones se acumulan anualmente...
Información valiosa para debugging:
- Documento 3 tiene score 0.112 → ¡Está por debajo del umbral 0.15! Será filtrado.
- Todos de fuentes diferentes → Bueno, diversidad de información.
- Scores altos en general → La búsqueda funcionó bien.
7. El Formateador – El Presentador Profesional
De Documentos Crudos a Texto Estructurado:
python
def format_docs_for_llm_prompt_with_links(full_docs):
texto_formateado = ""
for doc in full_docs:
meta = doc.metadata or {}
# Extraer información
fuente = meta.get("source", "Documento")
pagina = meta.get("page", "N/A")
contenido = doc.page_content
# INTELIGENCIA: Si es URL, crear enlace Markdown
if isinstance(fuente, str) and fuente.startswith("http"):
fuente_display = f"[{fuente}]({fuente})"
else:
fuente_display = fuente
# Construir formato consistente
texto_formateado += f"📄 Fuente: {fuente_display}"
if pagina != "N/A":
texto_formateado += f", Página: {pagina}"
texto_formateado += "\n"
texto_formateado += "─" * 40 + "\n"
texto_formateado += f"{contenido}\n\n"
return texto_formateado
La Transformación:
ANTES (documento crudo):
python
Document(
page_content="Los empleados con 1-3 años: 15 días...",
metadata={"source": "https://empresa.com/politicas.pdf", "page": "15"}
)
DESPUÉS (texto formateado):
text
📄 Fuente: [https://empresa.com/politicas.pdf](https://empresa.com/politicas.pdf), Página: 15 ──────────────────────────────────────── Los empleados con 1 a 3 años de antigüedad tienen derecho a 15 días hábiles de vacaciones anuales. El período debe solicitarse con 30 días de anticipación mediante el formulario VAC-2024.
Por qué este formato es efectivo:
- Estructura clara: El LLM entiende dónde termina un documento y empieza otro
- Metadatos visibles: Sabe la fuente exacta para citar
- Enlaces activos (en Markdown): Para respuestas que podrían mostrarse en interfaces ricas
- Separadores visuales: Líneas que ayudan al parsing
8. Prompt Template – El Director de Orquesta
Las Instrucciones que Guían al LLM:
python
template_str = """
Eres un asistente especializado que responde preguntas basándose
EXCLUSIVAMENTE en la información proporcionada.
INFORMACIÓN DE CONTEXTO:
{context}
PREGUNTA DEL USUARIO:
{question}
INSTRUCCIONES:
1. Responde usando SOLO la información del contexto
2. Si la información no está en el contexto, di "No tengo esa información"
3. Sé claro y conciso
4. Cita las fuentes específicas (documento y página)
5. Si hay contradicciones, menciona ambas perspectivas
RESPUESTA:
"""
La Psicología detrás del Prompt:
Partes clave del prompt:
- Rol definido: «Eres un asistente especializado» → Establece identidad
- Restricción clara: «EXCLUSIVAMENTE en la información proporcionada» → Previene alucinaciones
- Estructura explícita: Contexto → Pregunta → Instrucciones → Respuesta
- Reglas específicas: Los 5 puntos dan guía concreta
- Formato esperado: «RESPUESTA:» indica dónde empezar
Evolución del prompt (versiones anteriores vs actual):
| Versión Antigua | Versión Actual | Mejora |
|---|---|---|
| «Responde la pregunta» | «Responde usando SOLO la información…» | Menos alucinaciones |
| Sin reglas | 5 reglas específicas | Más consistencia |
| Sin formato | «Cita fuentes específicas» | Más verificable |
Resumen: El Equipo Perfectamente Orquestado
Cada componente tiene un rol específico y complementario:
graph TD
subgraph "FASE 1: BÚSQUEDA"
A[PGVector<br/>Buscador rápido] --> B[Cohere Rerank<br/>Ordenador inteligente]
end
subgraph "FASE 2: FILTRADO"
B --> C[Filtro Score<br/>Control calidad]
C --> D[ByteStore<br/>Archivista completo]
end
subgraph "FASE 3: PREPARACIÓN"
D --> E[Formateador<br/>Presentador]
E --> F[Prompt Template<br/>Director]
end
subgraph "FASE 4: SOPORTE"
G[Caché<br/>Memorioso] -.-> A
G -.-> D
H[Circuit Breaker<br/>Guardián] -.-> B
I[Inspector<br/>Supervisor] -.-> C
end
F --> J[LLM Deepseek<br/>Sintetizador]
J --> K[✅ Respuesta perfecta]
style A fill:#FFF3E0
style B fill:#E8F5E9
style C fill:#FCE4EC
style D fill:#E1F5FE
style G fill:#FFF8E1Interdependencias clave:
- PGVector depende de Caché para ser rápido
- Cohere depende de Circuit Breaker para ser resiliente
- Filtro depende de Inspector para ser transparente
- Todos dependen de ByteStore para contenido completo
La lección más importante: Ningún componente es «mágico» por sí solo. La magia está en cómo trabajan juntos, cada uno haciendo exactamente lo que mejor sabe hacer, confiando en los demás para lo demás.
En la próxima parte: Veremos todo esto en acción con un caso práctico completo, donde seguiremos una pregunta real a través de cada componente, mostrando decisiones en tiempo real y resultados tangibles.
Parte 5: Caso Práctico – Recorrido Completo de una Pregunta Real
Siguiendo la Aventura de una Pregunta: Desde la Boca del Usuario hasta la Respuesta Perfecta
Vamos a hacer algo emocionante: seguiremos una pregunta real segundo a segundo a través de todo el sistema. Es como poner una cámara GoPro en una pregunta y ver su viaje épico a través de la «fábrica de respuestas inteligentes».
Nuestro protagonista: María, una empleada de RRHH con una pregunta urgente.
📋 El Escenario: Un Viernes por la Tarde en Recursos Humanos
Personajes:
- 👩💼 María: Especialista en RRHH, necesita información rápida
- 🤖 Sistema RAG: Nuestro asistente inteligente
- 📚 Documentos disponibles:
politica_vacaciones_2024.pdf(15 páginas)contratos_colectivos.xlsxley_laboral_actualizada.pdfmanual_empleado_v3.docxfaq_rrhh_2024.md
Contexto: Es viernes a las 4:30 PM. Un empleado acaba de preguntar si puede tomar vacaciones la próxima semana. María necesita verificar rápido las reglas.
🎬 Acto 1: La Pregunta Entra al Sistema
Escena: El Teclado de María
python
# Viernes, 16:32:15 - María escribe: pregunta_maria = "¿Puede un empleado con 6 meses de antigüedad tomar 5 días de vacaciones la próxima semana?"
Estado mental de María:
- «Necesito respuesta RÁPIDO»
- «No recuerdo exactamente la política para <6 meses»
- «¿Hay algún período de preaviso especial?»
La Invocación del Sistema:
python
# 16:32:17 - María hace clic en "Consultar"
from agent_service_toolkit import rag_tool_decorator
# Herramienta preconfigurada para RRHH
herramienta_rrhh = rag_tool_decorator(
db_name="recursos_humanos",
persona="especialista en políticas laborales",
template=template_rrhh,
tool_name="consultar_politicas_rrhh",
description="Consulta políticas de RRHH y leyes laborales"
)
# ¡Acción!
resultado = herramienta_rrhh.func(pregunta_maria)
Primer registro en logs:
text
[16:32:17] [RAG TOOL] Invoking RAG tool for DB='recursos_humanos' with question: '¿Puede un empleado con 6 meses de antigüedad...'
🔍 Acto 2: La Búsqueda Inicial (Estación 1-2)
16:32:18 – get_retriever("recursos_humanos") se Activa
python
# PRIMERO: ¿Ya existe en caché?
if "recursos_humanos" in _retriever_cache:
print("✅ Retriever obtenido del caché (2ms)")
retriever = _retriever_cache["recursos_humanos"]
else:
# Conexión a PostgreSQL con embeddings
print("🔄 Creando nuevo retriever para RRHH...")
Output del sistema:
text
[16:32:18] Retriever para 'recursos_humanos' obtenido del caché. Tiempo ahorrado: ~800ms
16:32:19 – Búsqueda con MMR
El retriever busca con estos parámetros:
python
search_kwargs = {
"k": 10, # 10 resultados finales
"fetch_k": 20, # Considera 20 primero
"lambda_mult": 0.75 # 75% relevancia, 25% diversidad
}
Lo que realmente busca el sistema:
- Términos clave identificados:
["empleado", "6 meses", "antigüedad", "vacaciones", "5 días", "próxima semana"] - Búsqueda semántica: «vacaciones» también busca «tiempo libre», «descanso anual»
- «próxima semana» también considera «corto plazo», «inmediato»
16:32:21 – Resultados Iniciales (10 documentos)
El sistema encuentra estos fragmentos (simplificado):
| # | Documento | Contenido (fragmento) | Score MMR |
|---|---|---|---|
| 1 | politica_vacaciones.pdf | «Período de prueba: 6 meses sin vacaciones…» | 0.88 |
| 2 | contrato_colectivo.xlsx | «Vacaciones: mínimo 1 año para goce completo» | 0.85 |
| 3 | ley_laboral.pdf | «Derecho a vacaciones después de 6 meses trabajados» | 0.82 |
| 4 | manual_empleado.docx | «Solicitud vacaciones: 15 días preaviso mínimo» | 0.79 |
| 5 | politica_vacaciones.pdf | «Vacaciones proporcionales por meses trabajados» | 0.76 |
| 6 | faq_rrhh_2024.md | «¿Vacaciones durante período de prueba? No permitidas» | 0.72 |
| … | … | … | … |
¡Primer problema detectado!:
- Documento 1 dice «6 meses sin vacaciones» (score: 0.88)
- Documento 3 dice «derecho después de 6 meses» (score: 0.82)
- ¿Contradicción? Necesitamos re-ranking.
📊 Acto 3: El Re-ranking Inteligente (Estación 3)
16:32:22 – Cohere Rerank Entra en Acción
python
# ¿Cohere está disponible?
now = time.time()
if now < COHERE_DISABLED_UNTIL:
print("⚠️ Cohere en cooldown, usando resultados originales")
else:
print("🔍 Enviando a Cohere para re-ranking...")
Suerte: Cohere está disponible hoy.
16:32:23 – Cohere Analiza la Pregunta vs Cada Documento
Cohere hace un análisis mucho más profundo:
text
PREGUNTA: "¿Puede un empleado con 6 meses de antigüedad tomar 5 días de vacaciones la próxima semana?" ANÁLISIS COHERE: 1. Documento 1: "Período de prueba: 6 meses sin vacaciones" - Cohere score: 0.95 ✓ - Razón: Respuesta DIRECTA a la pregunta 2. Documento 3: "Derecho a vacaciones después de 6 meses trabajados" - Cohere score: 0.65 ✗ - Razón: Habla de "derecho" general, no de "tomar la próxima semana" 3. Documento 4: "Solicitud vacaciones: 15 días preaviso mínimo" - Cohere score: 0.85 ✓ - Razón: Importante para "próxima semana" 4. Documento 5: "Vacaciones proporcionales por meses trabajados" - Cohere score: 0.70 → - Razón: Relevante pero secundario
16:32:25 – Nuevo Orden (después de Cohere)
| # | Documento | Score Original | Score Cohere | Cambio |
|---|---|---|---|---|
| 1 | politica_vacaciones.pdf | 0.88 | 0.95 | ⬆️ Sube al #1 |
| 2 | manual_empleado.docx | 0.79 | 0.85 | ⬆️ Sube del #4 al #2 |
| 3 | faq_rrhh_2024.md | 0.72 | 0.80 | ⬆️ Sube del #6 al #3 |
| 4 | contrato_colectivo.xlsx | 0.85 | 0.75 | ⬇️ Baja del #2 al #4 |
| 5 | ley_laboral.pdf | 0.82 | 0.65 | ⬇️ ¡Baja del #3 al #5! |
| 6 | politica_vacaciones.pdf | 0.76 | 0.60 | ⬇️ Baja del #5 al #6 |
Insight clave de Cohere:
- Documento 3 (ley laboral) bajó mucho porque aunque habla de «derecho después de 6 meses», no menciona período de prueba ni preaviso.
- Documento 4 (manual empleado) subió porque menciona «15 días preaviso», crucial para «próxima semana».
🎯 Acto 4: Filtrado por Calidad (Estación 4)
16:32:26 – Aplicando el Filtro de Score
Parámetros de filtrado (desde variables de entorno):
python
UMBRAL_MINIMO = 0.15 # Configurado en .env MAXIMO_DOCUMENTOS = 6 # Configurado en .env
Proceso de filtrado:
python
documentos_filtrados = []
for doc in documentos_rerankeados:
score = doc.metadata.get("relevance_score", 0)
# ¿Supera el umbral?
if score >= 0.15: # ¡Todos superan 0.15!
documentos_filtrados.append(doc)
# ¿Alcanzamos el límite?
if len(documentos_filtrados) >= 6:
break # No más documentos
Resultado del filtrado:
- Documentos que pasan: Todos 6 (todos con score > 0.15)
- Límite alcanzado: Sí, 6 documentos (el máximo configurado)
Estado actual:
text
6 documentos aprobados para procesamiento Scores: [0.95, 0.85, 0.80, 0.75, 0.65, 0.60]
📚 Acto 5: Obtención de Documentos Completos (Estación 5)
16:32:27 – Del Fragmento al Documento Completo
Problema actual: Solo tenemos fragmentos de 150-200 palabras. ¡Necesitamos el CONTEXTO COMPLETO!
python
# Extraer IDs de los documentos aprobados
doc_ids = []
for doc in documentos_filtrados:
doc_id = _extract_doc_id(doc.metadata)
if doc_id:
doc_ids.append(doc_id)
# IDs obtenidos: ["pol_vac_2024_p15", "manual_emp_p8", "faq_rrhh_q42", ...]
# Buscar en ByteStore
print("📦 Recuperando documentos completos del ByteStore...")
documentos_completos = bytestore.mget(doc_ids)
La Transformación Mágica:
Fragmento del Documento 1 (antes):
text
"Período de prueba: 6 meses sin vacaciones según política..."
Documento Completo 1 (después):
text
POLÍTICA DE VACACIONES 2024 - CAPÍTULO 3: PERÍODO DE PRUEBA Artículo 3.1: Durante los primeros 6 meses (período de prueba), los empleados NO tienen derecho a tomar vacaciones pagadas. Artículo 3.2: Excepciones: - Hospitalización del empleado o familiar directo - Casos de fuerza mayor debidamente justificados - Requiere aprobación de Director de RRHH Artículo 3.3: Después del período de prueba, las vacaciones se calculan proporcionalmente: 1.25 días por mes trabajado.
¡Información crucial descubierta!:
- Excepciones que no estaban en el fragmento
- Cálculo proporcional para después del período
- Proceso de aprobación específico
👁️ Acto 6: Inspección y Debugging (Estación 6)
16:32:29 – La Ventana de Control se Abre
python
# Función inspect() se ejecuta
print("\n" + "="*60)
print("🔍 INSPECCIÓN: Documentos que llegarán al LLM")
print("="*60)
Output real que vería un desarrollador:
text
============================================================ 🔍 INSPECCIÓN: Documentos que llegarán al LLM ============================================================ 📄 Documento #1 (Score: 0.950 ✓): 📍 Fuente: politica_vacaciones_2024.pdf 📄 Página: 15-18 🏷️ ID: pol_vac_2024_p15 📝 Vista previa: "POLÍTICA DE VACACIONES 2024 - CAPÍTULO 3: PERÍODO DE PRUEBA. Artículo 3.1: Durante los primeros 6 meses..." 📄 Documento #2 (Score: 0.850 ✓): 📍 Fuente: manual_empleado_v3.docx 📄 Página: 8 🏷️ ID: manual_emp_p8 📝 Vista previa: "CAPÍTULO 4: SOLICITUD DE VACACIONES. 4.1 Preaviso mínimo: 15 días hábiles para solicitud..." 📄 Documento #3 (Score: 0.800 ✓): 📍 Fuente: faq_rrhh_2024.md 📄 Página: Q42 🏷️ ID: faq_rrhh_q42 📝 Vista previa: "PREGUNTA 42: ¿Puedo tomar vacaciones durante mi período de prueba? RESPUESTA: No, excepto en casos..." [Mostrando 3 de 6 documentos totales...] ============================================================
Lo que María NO ve pero el desarrollador SÍ:
- ✅ Scores altos → Buena recuperación
- ✅ Fuentes diversas → Perspectiva completa
- ✅ Páginas específicas → Fácil verificación
- ✅ IDs únicos → Trazabilidad total
📋 Acto 7: Formateo para el LLM (Estación 7)
16:32:30 – Preparando la «Comida» del LLM
python
contexto_formateado = format_docs_for_llm_prompt_with_links(documentos_completos)
Resultado formateado (extracto):
text
📄 Fuente: politica_vacaciones_2024.pdf, Páginas: 15-18 ──────────────────────────────────────── POLÍTICA DE VACACIONES 2024 - CAPÍTULO 3: PERÍODO DE PRUEBA Artículo 3.1: Durante los primeros 6 meses (período de prueba), los empleados NO tienen derecho a tomar vacaciones pagadas. Artículo 3.2: Excepciones: Hospitalización del empleado o familiar directo, casos de fuerza mayor debidamente justificados. Requiere aprobación de Director de RRHH. Artículo 3.3: Después del período de prueba, las vacaciones se calculan proporcionalmente: 1.25 días por mes trabajado. 📄 Fuente: manual_empleado_v3.docx, Página: 8 ──────────────────────────────────────── CAPÍTULO 4: SOLICITUD DE VACACIONES 4.1 Preaviso mínimo: 15 días hábiles para solicitud de vacaciones. 4.2 Excepciones al preaviso: Situaciones de emergencia documentadas. 4.3 Proceso: Completar formulario VAC-2024, aprobación del supervisor. 📄 Fuente: faq_rrhh_2024.md, Página: Q42 ──────────────────────────────────────── PREGUNTA 42: ¿Puedo tomar vacaciones durante mi período de prueba? RESPUESTA: No, excepto en casos excepcionales (hospitalización, fuerza mayor). Incluso en esos casos, se requiere aprobación especial del Director de RRHH y documentación justificativa.
Calidad del formateo:
- ✅ Estructura clara: Separadores entre documentos
- ✅ Metadatos visibles: Fuente + páginas
- ✅ Contenido completo: No solo fragmentos
- ✅ Jerarquía respetada: Títulos, artículos, secciones
🤖 Acto 8: El LLM Genera la Respuesta (Estación 8-10)
16:32:32 – El Prompt Final
python
prompt_final = f"""
Eres un especialista en políticas de RRHH. Responde basándote SOLO en esta información:
INFORMACIÓN DISPONIBLE:
{contexto_formateado}
PREGUNTA:
{pregunta_maria}
INSTRUCCIONES:
1. Responde usando SOLO la información proporcionada
2. Sé claro y conciso
3. Cita las fuentes específicas (documento y página)
4. Si hay excepciones o matices, menciónalos
5. Si la información es contradictoria, señálalo
RESPUESTA:
"""
16:32:33 – Deepseek Procesa
Lo que Deepseek «ve» y «piensa»:
- Análisis de la pregunta: «empleado con 6 meses» → período de prueba
- Consulta Documento 1: «6 meses sin vacaciones» (política p.15)
- Consulta Documento 4: «15 días preaviso mínimo» (manual p.8)
- Consulta Documento 3: «excepciones con aprobación» (FAQ Q42)
- Síntesis: Combinar regla general + excepciones + procedimiento
16:32:35 – Generación de Respuesta
python
respuesta_generada = llm.generate(prompt_final)
16:32:36 – Parseo y Limpieza
python
respuesta_final = StrOutputParser().parse(respuesta_generada)
🎉 Acto 9: La Respuesta Llega a María
16:32:37 – Resultado Final
En la pantalla de María aparece:
text
✅ RESPUESTA: Basándome en la documentación de la empresa: 📌 **Regla general:** Un empleado con 6 meses de antigüedad (período de prueba) NO PUEDE tomar vacaciones pagadas, según la Política de Vacaciones 2024, Capítulo 3, Artículo 3.1 (páginas 15-18). 📌 **Excepciones posibles (muy limitadas):** - Hospitalización del empleado o familiar directo - Casos de fuerza mayor debidamente justificados - Requiere aprobación ESPECIAL del Director de RRHH (Fuente: Política de Vacaciones, Artículo 3.2 y FAQ RRHH Q42) 📌 **Procedimiento (si aplica excepción):** 1. Completar formulario VAC-2024 2. Aprobación del supervisor inmediato 3. Aprobación especial del Director de RRHH 4. Presentar documentación justificativa (Fuente: Manual del Empleado, página 8) 📌 **Sobre el preaviso:** Para cualquier solicitud de vacaciones (incluso excepciones), se requiere mínimo 15 días hábiles de preaviso. Solicitar "la próxima semana" no cumple este requisito. (Fuente: Manual del Empleado, Capítulo 4.1, página 8) 💡 **Recomendación:** El empleado debería esperar a completar el período de prueba (6 meses) para tener derecho a vacaciones. Después, le corresponderían aproximadamente 7.5 días (1.25 días/mes × 6 meses). ¿Necesitas más detalles sobre algún punto específico?
📊 Acto 10: Análisis Post-Mortem
16:32:40 – Lo que Ocurrió Detrás de Escenas
Tiempos de procesamiento:
text
00:00 - Pregunta ingresada 00:02 - Retriever desde caché 00:04 - Búsqueda MMR completada (10 documentos) 00:07 - Cohere re-ranking terminado 00:09 - Filtrado por score aplicado 00:11 - Documentos completos obtenidos 00:13 - Inspección de debug mostrada 00:15 - Contexto formateado 00:18 - LLM generó respuesta 00:20 - Respuesta parseada y entregada ──────────────────── TOTAL: 20 segundos
Decisiones clave del sistema:
- ✅ Usó caché: Ahorró ~800ms en recrear conexiones
- ✅ Cohere re-ranking: Identificó que «preaviso» era crucial
- ✅ Filtrado adecuado: 6 documentos, todos relevantes
- ✅ Documentos completos: Descubrió excepciones no en fragmentos
- ✅ LLM bien guiado: Prompt evitó alucinaciones
Lo que el sistema hizo BIEN:
- Identificó contradicciones: «sin vacaciones» vs «derecho después de 6 meses»
- Priorizó correctamente: Política interna > ley general
- Incluyó matices: «No, excepto en casos muy específicos»
- Citó fuentes: Cada afirmación con documento y página
- Dio contexto completo: Regla + excepciones + procedimiento
Lo que María valoró:
- ⚡ Rápido: 20 segundos vs horas buscando manualmente
- 📋 Completo: No solo «sí/no», sino matices y procedimientos
- 🔍 Verificable: Cada punto con fuente específica
- 💡 Práctico: Incluyó recomendación concreta
- 🎯 Preciso: Entendió «próxima semana» vs «15 días preaviso»
🎯 Lecciones Aprendidas de Este Caso
1. La Importancia del Contexto Completo
Fragmento original: «6 meses sin vacaciones»
Documento completo: «6 meses sin vacaciones, EXCEPTO en casos de hospitalización…»
Sin ByteStore: María hubiera dicho «NO» categóricamente.
Con ByteStore: María puede decir «NO, excepto en estos casos muy específicos…»
2. El Valor del Re-ranking Inteligente
MMR solo: Ley laboral en posición #3
Cohere + MMR: Ley laboral bajó a #5 (correcto, es menos relevante)
Resultado: Respuesta más precisa basada en políticas internas, no solo leyes generales.
3. La Efectividad del Filtrado por Score
6 documentos fue el punto dulce:
- Suficiente para cobertura completa
- No demasiado para sobrecargar al LLM
- Todos con score > 0.80 → Alta relevancia garantizada
4. La Transparencia como Característica, no Bug
María podría preguntar: «¿De dónde sacaste eso?»
Respuesta del sistema: «De la Política de Vacaciones 2024, página 15, artículo 3.2»
🚀 Conclusión del Caso: De la Teoría a la Práctica
Este caso demostró que nuestro sistema RAG no es solo técnicamente sofisticado, sino prácticamente útil:
| Necesidad de María | Solución del Sistema |
|---|---|
| Respuesta RÁPIDA | 20 segundos vs horas |
| Información CONFIABLE | Basada en documentos oficiales |
| Contexto COMPLETO | No solo fragmentos |
| Fuentes VERIFICABLES | Cada punto documentado |
| Recomendación PRÁCTICA | Sugerencia concreta |
El resultado real:
- María responde al empleado en 2 minutos (20s sistema + preparación)
- La respuesta es precisa y matizada
- El empleado entiende por qué no puede y qué alternativas tiene
- Todos contentos: Empleado informado, María eficiente, empresa cumpliendo políticas
En la próxima parte: Veremos los patrones de diseño y mejores prácticas que hacen que este sistema no solo funcione, sino que sea mantenible, escalable y adaptable a diferentes necesidades.
Parte 6: Patrones y Mejores Prácticas – El Arte de Construir Sistemas Resilientes
Diagrama de patrones:
graph TD
A[Patrón] --> B[Cache]
A --> C[Circuit Breaker]
A --> D[Fallback]
A --> E[Pipeline]
B --> B1[Evita recrear conexiones]
B --> B2[Mejora rendimiento]
C --> C1[Protege APIs externas]
C --> C2[Evita cascada de fallos]
D --> D1[Continúa funcionando]
D --> D2[Experiencia degradada]
E --> E1[Flujo claro]
E --> E2[Transformaciones paso a paso]
style A fill:#FFEB3B
style B fill:#4CAF50
style C fill:#2196F3
style D fill:#F44336
style E fill:#9C27B0Más Allá del Código: Los Principios que Hacen que Este Sistema Funcione (y Sobreviva)
Construir un sistema RAG que funcione en un entorno controlado es una cosa. Construir uno que sobreviva al mundo real — con usuarios impacientes, APIs que fallan, picos de tráfico y datos desordenados — es un arte completamente diferente.
Aquí desvelamos los patrones de diseño y mejores prácticas que transforman este código de «funcional» a «industrialmente robusto»:
graph TD
A[Patrón Clave] --> B[Caché Inteligente]
A --> C[Circuit Breaker]
A --> D[Diseño Modular]
A --> E[Graceful Degradation]
A --> F[Transparencia]
B --> B1["`**Problema:** Recrear conexiones
**Solución:** _retriever_cache
**Impacto:** 1000x más rápido`"]
C --> C1["`**Problema:** APIs externas fallan
**Solución:** COHERE_DISABLED_UNTIL
**Impacto:** Resiliencia 99.9%`"]
D --> D1["`**Problema:** Cambios rompen todo
**Solución:** Capas separadas
**Impacto:** Mantenimiento fácil`"]
E --> E1["`**Problema:** Un componente falla
**Solución:** Fallbacks elegantes
**Impacto:** Usuarios felices siempre`"]
F --> F1["`**Problema:** Caja negra inconfiable
**Solución:** Función inspect()
**Impacto:** Confianza y debug fácil`"]
style A fill:#4CAF50
style B fill:#2196F3
style C fill:#FF9800
style D fill:#9C27B0
style E fill:#F44336
style F fill:#00BCD4🏗️ Patrón 1: Caché Inteligente – La Memoria que Ahorra Millones
El Problema del «Groundhog Day» Computacional
Imagina este escenario sin caché:
python
# 10 usuarios preguntan sobre "vacaciones" en 1 minuto
for usuario in range(10):
# CADA usuario causa:
retriever = crear_retriever_complejo("recursos_humanos") # 800ms
bytestore = crear_bytestore_complejo("recursos_humanos") # 500ms
embeddings = inicializar_embeddings() # 300ms
# TOTAL por usuario: 1.6 segundos × 10 = 16 segundos PERDIDOS
Resultado: 16 segundos de CPU desperdiciada, 10 conexiones duplicadas a DB, usuarios esperando innecesariamente.
La Solución: Diccionarios que Recuerdan
python
# SIMPLE pero BRILLANTE
_retriever_cache: Dict[str, BaseRetriever] = {}
_bytestore_cache: Dict[str, PostgresByteStore] = {}
def get_retriever(collection_name: str):
# La línea mágica que ahorra 800ms
if collection_name in _retriever_cache:
return _retriever_cache[collection_name] # ⚡ Instantáneo
# Solo si NO existe, crea (una sola vez)
nuevo_retriever = ... # 800ms de creación
_retriever_cache[collection_name] = nuevo_retriever
return nuevo_retriever
Mejores Prácticas Implementadas:
1. Cache-Aside Pattern (Caché al Lado)
python
# Patrón correcto:
def obtener_con_cache(clave):
# 1. PRIMERO verifica caché
if clave in cache:
return cache[clave]
# 2. Si no está, CALCULA
valor = calcular_valor_costoso(clave)
# 3. GUARDA en caché
cache[clave] = valor
return valor
# VS patrón incorrecto (cache-through):
def obtener_mal(clave):
# Siempre calcula, luego guarda
valor = calcular_valor_costoso(clave) # ⚠️ Innecesario si ya existe
cache[clave] = valor
return valor
2. Claves Significativas
python
# BIEN: Usa collection_name como clave _retriever_cache["recursos_humanos"] = retriever # MAL: Usaría algo como: _retriever_cache["a1b2c3"] = retriever # ¿Qué significa esto?
3. Vida Útil Implícita
python
# En este sistema: Caché vive mientras la aplicación corre # Alternativas consideradas y rechazadas: # - TTL (Time To Live): Demasiado complejo para este caso # - LRU (Least Recently Used): No necesario, pocas colecciones # - Invalidación manual: Los desarrolladores la manejan # DECISIÓN: Simple es mejor. Si se reinicia la app, se pierde caché. # Razón: Recuperar desde DB toma ~800ms, aceptable en reinicios raros.
Impacto en el mundo real:
- Primera petición: 1.6 segundos (crea todo)
- Peticiones 2-1000: 0.01 segundos (usa caché)
- Ahorro total: 99.4% del tiempo de inicialización
⚡ Patrón 2: Circuit Breaker – El Fusible que Salva el Sistema
El Problema de las «Cascadas de Fracaso»
Escenario catastrófico sin circuit breaker:
python
# Cohere API comienza a fallar (quizás mantenimiento)
for peticion in range(100):
try:
respuesta = cohere.rerank(documentos, pregunta) # ❌ Fallo
# Cada fallo tarda 10 segundos en timeout
except Exception:
pass # Seguimos intentando...
# Resultado: 100 peticiones × 10 segundos = 16 MINUTOS de bloqueo
# Usuarios esperando, sistema inútil, frustración total
La Solución: El Fusible Inteligente
python
# Variables globales que actúan como "estado del sistema"
COHERE_DISABLED_UNTIL = 0.0 # "Hasta cuándo está roto"
COHERE_FAIL_COUNT = 0 # "Cuántas veces ha fallado"
COHERE_RERANK_COOLDOWN_SEC = 180 # "Tiempo de descanso"
def safe_rerank(docs, query, reranker):
# ¿Estamos en "tiempo muerto"?
if time.time() < COHERE_DISABLED_UNTIL:
# ⚠️ Sistema HERIDO pero FUNCIONAL
return docs # Fallback: sin re-ranking
try:
# Intenta la operación riesgosa
resultado = reranker.compress_documents(docs, query)
COHERE_FAIL_COUNT = 0 # ¡Éxito! Reinicia contador
return resultado
except Exception as e:
# ❌ ¡Fallo! Incrementa contador de fracasos
COHERE_FAIL_COUNT += 1
# Activa el circuit breaker
COHERE_DISABLED_UNTIL = time.time() + COHERE_RERANK_COOLDOWN_SEC
# Log para debugging (pero no falla toda la app)
print(f"⚠️ Cohere falló. Desactivado por {COHERE_RERANK_COOLDOWN_SEC}s")
return docs # Degradación elegante
Los Tres Estados del Circuit Breaker:
stateDiagram-v2
[*] --> Cerrado : Inicio
Cerrado --> Abierto : Fallos consecutivos > umbral
Abierto --> SemiAbierto : Tiempo de espera cumplido
SemiAbierto --> Cerrado : Prueba exitosa
SemiAbierto --> Abierto : Prueba fallida
state Cerrado {
[*] --> OperandoNormal
OperandoNormal --> [*]
}
state Abierto {
[*] --> RechazandoPeticiones
RechazandoPeticiones --> UsandoFallback
UsandoFallback --> [*]
}
state SemiAbierto {
[*] --> ProbandoServicio
ProbandoServicio --> [*]
}Implementación en nuestro código:
- Estado Cerrado: Cohere funciona, todo normal
- Estado Abierto:
COHERE_DISABLED_UNTIL > now, usando fallback - Transición Semi-Abierta: Implícita cuando
time.time() >= COHERE_DISABLED_UNTIL
Mejores Prácticas del Circuit Breaker:
1. Fallback Significativo
python
# BIEN: Degradación útil
def safe_rerank(docs, query, reranker):
if circuit_breaker_activo:
return docs # ✅ Al menos tenemos documentos relevantes
# MAL: Degradación inútil
def safe_rerank_mal(docs, query, reranker):
if circuit_breaker_activo:
return [] # ❌ ¡Ahora no tenemos NADA!
2. Configuración por Entorno
python
# .env file - Ajustable sin código COHERE_RERANK_COOLDOWN_SEC=180 # 3 minutos para APIs externas # Podría ser diferente para otros servicios: DATABASE_RETRY_COOLDOWN_SEC=30 # 30 segundos para DB local
3. Logging Informativo pero No Alarmante
python
# BIEN: Informa pero no asusta
print(f"[RERANK FALLBACK] Cohere 429. Deshabilitando reranking durante {cooldown}s.")
# MAL: Asusta a los operadores
print(f"❌❌❌ EMERGENCIA: COHERE CAÍDO! SISTEMA COMPROMETIDO! ❌❌❌")
# (Los sysadmins odian falsas alarmas)
Impacto en resiliencia:
- Sin circuit breaker: 1 fallo de Cohere = Sistema caído 16 minutos
- Con circuit breaker: 1 fallo de Cohere = Calidad reducida 3 minutos
- Diferencia: Usuarios siempre obtienen respuestas
🧩 Patrón 3: Diseño Modular – El LEGO™ de la Ingeniería de Software
El Problema del «Monolito Acoplado»
Código mal diseñado (acoplado):
python
def responder_pregunta(pregunta):
# Todo mezclado, imposible cambiar una parte
documentos = buscar_en_postgres(pregunta)
documentos = rerankear_con_cohere(documentos, pregunta)
respuesta = generar_con_openai(documentos, pregunta)
return respuesta
# ¿Cambiar de OpenAI a Anthropic? ¡Reescribe TODO!
# ¿Usar Pinecone en vez de Postgres? ¡Reescribe TODO!
La Solución: Componentes Intercambiables
python
# DISEÑO MODULAR: Cada componente es independiente
# 1. Retriever (intercambiable)
def get_retriever(collection_name):
# Podría ser PGVector, Pinecone, Weaviate, Chroma...
return PGVector(...) # Hoy usamos esto
# 2. Reranker (intercambiable)
def get_reranker():
# Podría ser Cohere, sin reranking, propio...
return CohereRerank(...) if co_api_key else None
# 3. LLM (intercambiable)
def get_model(model_name):
# Podría ser Deepseek, GPT-4, Claude, Llama...
return DeepseekChat() if model_name == "Deepseek" else ...
# 4. Pipeline los conecta
chain = create_rag_chain_with_bytestore(
retriever=get_retriever(...), # ♻️ Intercambiable
bytestore=get_bytestore(...), # ♻️ Intercambiable
reranker=get_reranker(), # ♻️ Intercambiable
model=get_model("Deepseek"), # ♻️ Intercambiable
...
)
El Principio de Responsabilidad Única (SRP)
Cada componente hace UNA cosa bien:
| Componente | Responsabilidad Única |
|---|---|
get_retriever() | Conectar y buscar en vector DB |
safe_rerank() | Mejorar orden de resultados |
filter_reranked_docs_local() | Control de calidad por score |
get_full_documents() | Obtener contenido completo |
format_docs_for_llm_prompt_with_links() | Preparar para LLM |
Beneficio: Si filter_reranked_docs_local tiene un bug, solo ese componente necesita arreglo.
Interfaces Claras entre Módulos
python
# Interface bien definida entre búsqueda y filtrado:
documentos_filtrados = filter_reranked_docs_local(
reranked_docs, # List[Document] - Entrada clara
rag_threshold=0.15, # float - Parámetro claro
rag_max_docs=6 # int - Parámetro claro
) # List[Document] - Salida clara
# VS interface mala (acoplada):
def filtrar_mal(todo_mezclado):
# ¿Qué necesita? ¿Qué devuelve? ¡Misterio!
documentos = todo_mezclado["docs"]
config = todo_mezclado["config"]
estado = todo_mezclado["estado"]
# ... caos total
Impacto en mantenibilidad:
- Nuevo desarrollador entiende cada componente en 5 minutos
- Cambiar proveedor (Cohere → otro) afecta solo 1 archivo
- Testing unitario fácil para cada componente
- Debugging aislado cuando algo falla
📉 Patrón 4: Graceful Degradation – La Elegancia al Fallar
El Arte de Fallar Bien
Sistema pobre: «Todo o nada»
python
def sistema_fragil():
# Si CUALQUIERA falla, TODO falla
paso1 = paso_critico_que_puede_fallar() # ❌ Si falla...
paso2 = otro_paso_critico() # ⚠️ Nunca se ejecuta
paso3 = paso_final() # ⚠️ Nunca se ejecuta
return "TODO falló" # 😞 Usuario decepcionado
Nuestro sistema: «Algo es mejor que nada»
python
def sistema_resiliente():
paso1 = paso_critico() # ✅ Obtiene documentos
# paso2 PUEDE fallar, pero no mata todo
try:
paso2 = paso_opcional_mejora() # ⚠️ Podría fallar
except:
paso2 = paso1 # 🔄 Usa resultado anterior
paso3 = paso_final_con_lo_que_haya() # ✅ Siempre funciona
return "Respuesta (quizás no perfecta)" # 😊 Usuario satisfecho
Ejemplos de Degradación Elegante en el Código:
1. Cohere Rerank Fallback
python
# Cuando Cohere falla, usamos documentos sin re-ranking reranked_docs = safe_rerank(docs, query, reranker) # Resultado: Respuesta 95% buena en vez de 100% perfecta
2. ByteStore Fallback
python
def get_full_documents(reranked_docs, bytestore):
try:
return bytestore.mget(doc_ids) # ✅ Intenta documentos completos
except Exception as e:
print(f"[BYTESTORE FALLBACK] Error: {e}. Usando documentos rerankeados.")
return reranked_docs # 🔄 Fallback a fragmentos
# Resultado: Respuesta con contexto limitado > No respuesta
3. Filtro Tolerante a Fallos
python
def filter_reranked_docs_local(reranked_docs, threshold=None, max_docs=None):
filtered = []
for d in reranked_docs:
try:
score = obtener_score(d) # Podría fallar
if score >= threshold:
filtered.append(d)
except Exception:
# ⚠️ Si no podemos evaluar UN documento...
continue # ... lo saltamos PERO continuamos procesando
return filtered[:max_docs] # ✅ Devuelve lo que pudo procesar
La Jerarquía de Fallbacks:
Filosofía: Cualquier respuesta > Ninguna respuesta
graph TD
A[Calidad Óptima<br/>Todos los componentes funcionan] --> B
subgraph "Nivel 1: Cohere falla"
B[Re-ranking perdido<br/>Búsqueda MMR básica] --> C
end
subgraph "Nivel 2: ByteStore falla"
C[Documentos completos perdidos<br/>Solo fragmentos] --> D
end
subgraph "Nivel 3: Filtro falla"
D[Sin filtrado por score<br/>Todos los documentos] --> E
end
E[Calidad Mínima Aceptable<br/>Al menos responde algo]
style A fill:#4CAF50
style B fill:#FFC107
style C fill:#FF9800
style D fill:#F44336
style E fill:#9E9E9E🔍 Patrón 5: Transparencia por Diseño – Nada de Cajas Negras
El Problema del «Funciona, No sé Por Qué»
Sistema opaco:
python
def sistema_magico(pregunta):
# ✨ ¡Magia ocurre aquí! ✨
resultado = realizar_magia(pregunta)
return resultado # "Confía en mí"
# Usuario: "¿Por qué dijiste eso?"
# Sistema: "🤷♂️"
La Solución: Debugging Integrado
python
def inspect(input_dict: Dict[str, Any]) -> Dict[str, Any]:
"""VENTANA A LA COCINA del sistema"""
print("\n--- Documentos Rerankeados ---")
if not reranked_docs:
print(" [No se recuperaron documentos]")
print(" POSIBLES CAUSAS:")
print(" - ¿La colección existe?")
print(" - ¿Hay documentos indexados?")
print(" - ¿La pregunta es muy específica?")
for i, doc in enumerate(reranked_docs):
print(f" Doc {i+1}:")
print(f" Source: {doc.metadata.get('source')}")
print(f" Page: {doc.metadata.get('page')}")
# Score con contexto
score = doc.metadata.get('relevance_score')
if score >= 0.15:
print(f" Score: {score:.4f} ✅ (pasa filtro)")
else:
print(f" Score: {score:.4f} ❌ (será filtrado)")
print(f" Preview: {doc.page_content[:100]}...")
return input_dict # Importante: NO modifica, solo muestra
Qué Hace Transparente a Este Sistema:
1. Explica sus Decisiones
text
Score: 0.954 ✅ (pasa filtro) Score: 0.112 ❌ (será filtrado)
2. Muestra sus Fuentes
text
Source: politica_vacaciones_2024.pdf Page: 15
3. Revela su Proceso Interno
text
--- Documentos Rerankeados --- [Se encontraron 8 documentos] [3 pasaron el filtro de score >= 0.15] [2 serán usados (límite de 6 documentos)]
4. Sugiere Soluciones a Problemas
text
[No se recuperaron documentos] POSIBLES CAUSAS: - ¿La colección existe? - ¿Hay documentos indexados? - ¿La pregunta es muy específica?
Beneficios de la Transparencia:
- Confianza del Usuario: «Veo de dónde saca la información»
- Debugging Rápido: «Ah, el documento no pasó porque score=0.12»
- Optimización Informada: «Los scores son bajos, quizás mejorar embeddings»
- Compliance: «Podemos auditar cada decisión»
- Mejora Continua: «Veo patrones en lo que falla»
Ejemplo real:
text
Usuario: "¿Por qué no mencionaste la ley laboral?" Desarrollador: *revisa logs* "Ah, Documento 'ley_laboral.pdf' tenía score 0.12, no pasó el filtro 0.15" "Solución: Podemos bajar el threshold o mejorar cómo indexamos leyes"
🎯 Patrón 6: Configuración Externa – Los «Botones de Control»
El Problema del Código Rígido
Código duro (hardcoded):
python
def filtrar_documentos(documentos):
threshold = 0.15 # ⚠️ ¿Cambiar? Recompilar código
max_docs = 6 # ⚠️ ¿Ajustar? Recompilar código
# ...
Operaciones en producción:
text
Sysadmin: "El sistema está muy lento, ¿podemos reducir documentos?" Dev: "Sí, dame 2 horas para cambiar código, testear y desplegar" CEO: "¡Necesitamos esto en 5 minutos!"
La Solución: Variables de Entorno
python
# .env file - Cambia SIN tocar código RAG_FILTER_THRESHOLD=0.15 # Umbral de relevancia RAG_FILTER_MAX_DOCS=6 # Máximo documentos COHERE_RERANK_COOLDOWN_SEC=180 # Tiempo de cooldown
python
# Código que LEE configuración
def filter_reranked_docs_local(reranked_docs, threshold=None, max_docs=None):
# 1. Intenta parámetro de función
# 2. Si no, lee de entorno
# 3. Si no, usa default
thr = (threshold or
float(os.getenv("RAG_FILTER_THRESHOLD", "0.15")))
md = (max_docs or
int(os.getenv("RAG_FILTER_MAX_DOCS", "6")))
# Ahora usa thr y md...
Jerarquía de Configuración Inteligente:
python
# NIVELES de configuración (más específico gana):
# 1. 🥇 Parámetro de función (más específico)
# 2. 🥈 Variable de entorno (ajuste dinámico)
# 3. 🥉 Valor por defecto (fallback seguro)
def configurar(par_funcion=None):
valor = (par_funcion or # Nivel 1
os.getenv("VAR_ENV") or # Nivel 2
VALOR_POR_DEFECTO) # Nivel 3
return valor
Escenarios de Uso en Producción:
Escenario 1: Pico de Tráfico
text
17:00 - Sistema lento, muchos usuarios Sysadmin: *edita .env* RAG_FILTER_MAX_DOCS=3 # ← De 6 a 3 documentos 17:01 - Sistema responde 2x más rápido
Escenario 2: Cohere Muy Lento
text
Sysadmin: *monitorea logs* "Cohere está tardando 5 segundos por petición" *edita .env* COHERE_RERANK_COOLDOWN_SEC=300 # ← 5 minutos de cooldown
Escenario 3: Documentos de Baja Calidad
text
Analista: "Las respuestas son malas últimamente" *edita .env* RAG_FILTER_THRESHOLD=0.25 # ← Más estricto con calidad
Impacto operacional:
- Cambios en segundos vs horas/días
- Sin downtime (solo reiniciar workers)
- Sin riesgo de bugs (no se toca código)
- Rollback instantáneo (cambiar .env otra vez)
📊 Resumen: El Equilibrio Perfecto
Estos patrones no son solo «buenas ideas» — son lecciones aprendidas de sistemas en producción:
| Patrón | Resuelve | Principio | Ejemplo en Código |
|---|---|---|---|
| Caché | Latencia inicial | «No recalcules» | _retriever_cache |
| Circuit Breaker | Fallos en cascada | «Protege el sistema» | COHERE_DISABLED_UNTIL |
| Modularidad | Rigidez y acoplamiento | «Una cosa bien» | Funciones separadas |
| Graceful Degradation | Fallos catastróficos | «Algo > Nada» | safe_rerank fallback |
| Transparencia | Cajas negras | «Muestra tu trabajo» | Función inspect() |
| Config Externa | Cambios costosos | «Separa código/config» | Variables .env |
La Filosofía Subyacente:
- Anticipa Fallos: Todo fallará eventualmente
- Diseña para Debugging: Si puede fallar, debe ser debuggable
- Separa Preocupaciones: Cada componente, una responsabilidad
- Configura, No Codifiques: Lo que cambia, va fuera del código
- Degrada con Elegancia: Nunca dejes al usuario sin respuesta
En la próxima parte: Veremos cómo llevar este sistema a producción, considerando escalabilidad, monitoreo, costos y mantenimiento a largo plazo.
Parte 7: Diagrama Final de Integración – El Sistema en Armonía
Diagrama completo de interacciones:
sequenceDiagram
participant U as Usuario
participant T as Tool RAG
participant R as Retriever
participant C as Cohere Rerank
participant B as ByteStore
participant L as LLM Deepseek
U->>T: "¿Días de vacaciones?"
T->>R: Buscar en recursos_humanos
R-->>T: 15 documentos
T->>C: Reordenar por relevancia
C-->>T: 5 documentos ordenados
T->>T: Filtrar (score ≥ 0.15)
T->>B: Obtener documentos completos
B-->>T: Contenido completo
T->>L: Prompt + Contexto
L-->>T: Respuesta generada
T-->>U: "Tienes 20 días según política..."
Note over T,C: Circuit breaker activo<br/>si Cohere fallaViendo el Bosque Completo: Cómo Todas las Piezas Bailan Juntas
Hemos analizado cada componente individualmente. Ahora es momento de ver el sistema completo en acción — cómo todos esos engranajes independientes se sincronizan perfectamente para crear una experiencia de usuario fluida y poderosa.
Este diagrama final muestra la coreografía completa del sistema, desde que el usuario formula una pregunta hasta que recibe una respuesta verificable:
sequenceDiagram
participant U as 👤 Usuario/Agente
participant TD as 🛠️ Tool Decorator
participant RC as 🗄️ Retriever Cache
participant PV as 🔍 PGVector Retriever
participant CR as 📊 Cohere Reranker
participant CB as ⚡ Circuit Breaker
participant FLT as 🎯 Filtro de Score
participant BS as 📦 PostgresByteStore
participant FMT as 📋 Formateador
participant INS as 👁️ Inspector Debug
participant LLM as 🧠 Modelo Deepseek
participant OUT as ✂️ Output Parser
Note over U,OUT: 🕐 TIEMPO 0-2s: PREPARACIÓN
U->>TD: "¿Puede un empleado con 6 meses...?"
TD->>RC: get_retriever("recursos_humanos")
alt Retriever en caché
RC-->>TD: ✅ Retriever cacheado (2ms)
else Retriever no en caché
RC->>PV: 🔄 Crear nuevo retriever
PV-->>RC: Retriever creado (800ms)
RC->>RC: Guardar en _retriever_cache
end
TD->>RC: get_bytestore("recursos_humanos")
alt Bytestore en caché
RC-->>TD: ✅ Bytestore cacheado (1ms)
else Bytestore no en caché
RC->>BS: 🔄 Crear nuevo bytestore
BS-->>RC: Bytestore creado (500ms)
RC->>RC: Guardar en _bytestore_cache
end
Note over U,OUT: 🕑 TIEMPO 2-5s: BÚSQUEDA
TD->>PV: get_relevant_documents(pregunta)
PV-->>TD: 10 documentos (MMR search)
Note over U,OUT: 🕒 TIEMPO 5-8s: RE-RANKING INTELIGENTE
TD->>CB: ¿Cohere disponible?
alt Cohere activo
CB-->>TD: ✅ Sí, disponible
TD->>CR: compress_documents(docs, pregunta)
CR-->>TD: 5 documentos re-rankeados
TD->>CB: ✅ Éxito, reset fail count
else Cohere en cooldown
CB-->>TD: ⚠️ En cooldown hasta X
TD->>TD: Usar documentos sin re-ranking
end
Note over U,OUT: 🕓 TIEMPO 8-10s: FILTRADO Y OBTENCIÓN
TD->>FLT: filter_reranked_docs_local(docs)
FLT->>FLT: Aplicar threshold=0.15, max=6
FLT-->>TD: 3 documentos filtrados
TD->>BS: mget([doc_ids...])
BS-->>TD: Documentos completos
Note over U,OUT: 🕔 TIEMPO 10-12s: PREPARACIÓN PARA LLM
TD->>FMT: format_docs_for_llm_prompt_with_links()
FMT-->>TD: Contexto formateado con fuentes
TD->>INS: inspect() - Debugging
INS-->>TD: Logs mostrados (no modifica datos)
Note over U,OUT: 🕕 TIEMPO 12-18s: GENERACIÓN DE RESPUESTA
TD->>LLM: Prompt + Contexto + Pregunta
LLM-->>TD: Respuesta generada con citas
TD->>OUT: StrOutputParser()
OUT-->>TD: Respuesta limpia y formateada
Note over U,OUT: 🕖 TIEMPO 18-20s: ENTREGA FINAL
TD-->>U: ✅ "Basándome en la política página 15..."<br/>📚 [Fuentes verificables]<br/>⚡ [20 segundos total]
Note right of CB: 📊 MÉTRICAS FINALES:<br/>• Cache hit: 2/2 (100%)<br/>• Cohere: activo<br/>• Docs iniciales: 10<br/>• Docs finales: 3<br/>• Tiempo total: ~20s🎵 La Sinfonía de Componentes: Armonizando Velocidad y Calidad
Este diagrama muestra más que un flujo secuencial — muestra un sistema que toma decisiones inteligentes en tiempo real:
La Coreografía en Tres Movimientos
Movimiento 1: La Obertura de Optimización (0-2s)
graph LR
A[Pregunta entra] --> B{Caché disponible?}
B -- Sí --> C[⚡ 2ms - Usa existente]
B -- No --> D[🔄 800ms - Crea nuevo]
C --> E[Siguiente paso]
D --> F[💾 Guarda en caché] --> E
style C fill:#C8E6C9
style D fill:#FFE0B2Decisión clave: El sistema evalúa antes de actuar. Si ya tiene las herramientas listas, las usa inmediatamente. Si no, las crea una sola vez y las guarda para el futuro.
Impacto del caché:
- Primer usuario del día: ~1.3 segundos de inicialización
- Usuario 100 del día: ~0.003 segundos (solo búsqueda)
- Beneficio acumulado: Miles de segundos ahorrados
Movimiento 2: El Ballet de Búsqueda y Refinamiento (2-10s)
graph TD
A[Búsqueda MMR] --> B{¿Cohere activo?}
B -- Sí --> C[📊 Re-ranking inteligente]
B -- No --> D[⚠️ Usa resultados básicos]
C --> E[🎯 Filtrado por score]
D --> E
E --> F{¿Docs pasan threshold?}
F -- Sí --> G[✅ 3-6 documentos]
F -- No --> H[🔍 Búsqueda expandida]
G --> I[📦 Obtener completos]
H --> A
style C fill:#E8F5E9
style D fill:#FFF3E0
style G fill:#C8E6C9
style H fill:#FFCDD2El baile de decisiones:
- MMR primero: Búsqueda rápida y diversa
- Cohere segundo (si está disponible): Refinamiento inteligente
- Filtro tercero: Control de calidad estricto
- ByteStore cuarto: Contexto completo
Cada paso pregunta: «¿Vale la pena continuar?» Si los documentos no pasan el filtro, el sistema podría:
- Expandir la búsqueda (más documentos)
- Relajar el threshold (configuración)
- Continuar con lo que tiene (degradación elegante)
Movimiento 3: La Sinfonía de Síntesis (10-20s)
graph LR
A[Documentos completos] --> B[📋 Formateo]
B --> C[👁️ Inspección debug]
C --> D[🤖 LLM con contexto]
D --> E[✂️ Parseo limpio]
E --> F[✅ Respuesta final]
subgraph "Feedback loop oculto"
C --> G[📝 Logs para optimización]
G --> H[🔄 Ajustar thresholds]
H --> A
end
style C fill:#E3F2FD
style G fill:#F3E5F5La magia final: No es solo enviar datos al LLM. Es:
- Formatear para máxima comprensión
- Inspeccionar para transparencia
- Guiar al LLM con instrucciones precisas
- Refinar la salida para claridad
- Aprender de cada interacción (logs)
⚙️ Los Mecanismos de Coordinación Ocultos
Lo que el diagrama de secuencia muestra es la coreografía visible. Pero hay mecanismos más profundos que coordinan todo:
1. El Sistema de Estado Compartido
python
# Variables globales que actúan como "memoria del sistema"
COHERE_DISABLED_UNTIL = 0.0 # Todos los componentes respetan esto
COHERE_FAIL_COUNT = 0 # Historia compartida de fallos
_retriever_cache = {} # Memoria compartida de conexiones
_bytestore_cache = {} # Memoria compartida de almacenes
Cómo funciona: Cuando un componente actualiza el estado, todos los demás lo ven. Si Cohere falla y activa el circuit breaker, la próxima petición (incluso de otro usuario) lo respetará.
2. La Cadena de Responsabilidad (Chain of Responsibility)
python
# Cada paso puede "decidir" modificar el flujo
chain = (
{"question": RunnablePassthrough()}
| RunnablePassthrough.assign(retrieved_docs=...)
| RunnablePassthrough.assign(reranked_docs=...)
| RunnablePassthrough.assign(filtered_docs=...)
# Cada paso recibe, procesa, pasa al siguiente
# Cualquier paso puede: transformar, filtrar, enriquecer
)
Patrón: Como una línea de ensamblaje donde cada estación:
- Recibe trabajo del anterior
- Hace su tarea específica
- Pasa al siguiente (quizás modificado)
3. El Patrón Observer para Debugging
python
def inspect(input_dict):
# No modifica, solo OBSERVA y REGISTRA
print("\n--- Documentos Rerankeados ---")
for doc in input_dict.get("reranked_docs", []):
# Registra estado actual
print(f"Score: {doc.score}")
return input_dict # Pasa igual que llegó
Beneficio: Puedes agregar múltiples «observadores» sin afectar el flujo principal:
- Uno para logging
- Otro para métricas
- Otro para alertas
- Todos no bloqueantes
🌐 Integración con el Ecosistema Externo
El sistema no vive aislado. Se integra con:
1. Agentes de IA (LangChain, AutoGPT, etc.)
python
# Nuestra herramienta es USADA por agentes mayores
from langchain.agents import initialize_agent
agente = initialize_agent(
tools=[
rag_tool_decorator("recursos_humanos", ...),
rag_tool_decorator("legal", ...),
rag_tool_decorator("tecnico", ...),
# Otras herramientas no-RAG
],
llm=llm_model,
agent_type="zero-shot-react-description"
)
# El agente DECIDE cuándo usar RAG vs otras herramientas
2. Sistemas de Monitoreo
graph LR
A[Sistema RAG] --> B[📈 Métricas]
A --> C[📝 Logs estructurados]
A --> D[🚨 Alertas]
B --> E[Grafana Dashboard]
C --> F[ELK Stack]
D --> G[PagerDuty/Slack]
E --> H[📊 Tiempo respuesta]
E --> I[📊 Hit rate caché]
E --> J[📊 Fallos Cohere]
style H fill:#4CAF50
style I fill:#2196F3
style J fill:#F44336Métricas clave monitoreadas:
rag_request_duration_seconds(histograma)rag_cache_hit_total(contador)rag_cohere_failures_total(contador)rag_documents_processed(contador)
3. Pipeline de CI/CD
yaml
# .github/workflows/rag-deploy.yml
name: Deploy RAG System
on:
push:
paths:
- 'agent_service_toolkit/**'
- 'requirements.txt'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Run RAG tests
run: pytest tests/test_rag_integration.py
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: ./deploy.sh staging
- name: Run smoke tests
run: ./smoke_test_rag.sh
- name: Deploy to production
if: success()
run: ./deploy.sh production
🔄 Flujo de Datos Completo: La Transformación Paso a Paso
Para entender realmente la integración, sigamos los datos a través del sistema:
graph TD
subgraph "Transformación de Datos"
A["Pregunta cruda<br/>'¿Vacaciones con 6 meses?'"] --> B
subgraph "Paso 1: Enriquecimiento"
B["Diccionario inicial<br/>{'question': '...'}"] --> C
C["+ retrieved_docs<br/>[10 documentos fragmentados]"] --> D
end
subgraph "Paso 2: Refinamiento"
D["+ reranked_docs<br/>[5 documentos ordenados]"] --> E
E["+ filtered_docs<br/>[3 documentos filtrados]"] --> F
end
subgraph "Paso 3: Completitud"
F["+ full_docs<br/>[3 documentos COMPLETOS]"] --> G
G["+ context<br/>Texto formateado con fuentes]"] --> H
end
subgraph "Paso 4: Generación"
H["Diccionario final<br/>{'context': '...', 'question': '...'}"] --> I
I["Prompt procesado<br/>Instrucciones + datos"] --> J
J["Respuesta LLM<br/>Texto con citas"] --> K
end
K["Respuesta final<br/>Texto limpio para usuario"]
end
style A fill:#E1F5FE
style B fill:#FFF3E0
style C fill:#F1F8E9
style D fill:#FFF8E1
style E fill:#FCE4EC
style F fill:#E8F5E8
style G fill:#E3F2FD
style H fill:#FFFDE7
style I fill:#F3E5F5
style J fill:#FFEBEE
style K fill:#C8E6C9Cada paso agrega valor:
- Enriquecimiento: De pregunta sola a pregunta + documentos
- Refinamiento: De muchos documentos a los mejores
- Completitud: De fragmentos a contexto completo
- Generación: De datos a conocimiento aplicado
🎭 Los Personajes y Sus Roles (Revisited)
Ahora con la integración completa, entendemos mejor cada «actor»:
| Personaje | Rol en la Integración | Interactúa Con |
|---|---|---|
| 👤 Usuario | Inicia el proceso, recibe resultado | Tool Decorator |
| 🛠️ Tool Decorator | Orquestador principal | Todos los componentes |
| 🗄️ Retriever Cache | Optimizador de recursos | PGVector, ByteStore |
| 🔍 PGVector | Buscador semántico | OpenAI Embeddings |
| 📊 Cohere Reranker | Mejorador de relevancia | Circuit Breaker |
| ⚡ Circuit Breaker | Protector del sistema | Cohere, Logs |
| 🎯 Filtro Score | Control de calidad | Configuración ENV |
| 📦 ByteStore | Proveedor de contexto | PostgreSQL DB |
| 📋 Formateador | Preparador de datos | Documentos, LLM |
| 👁️ Inspector | Ojo de la transparencia | Logs, Debugging |
| 🧠 LLM Deepseek | Sintetizador de conocimiento | Prompt, Contexto |
| ✂️ Output Parser | Pulidor final | Respuesta, Usuario |
La magia: Ninguno conoce el sistema completo. Cada uno solo conoce su rol y a sus vecinos inmediatos. Pero juntos, crean algo mayor que la suma de sus partes.
🚀 Escenarios de Integración Avanzada
Escenario A: Sistema de Soporte Multi-Dominio
python
# Un agente que usa MÚLTIPLES herramientas RAG
agente_inteligente = initialize_agent(
tools=[
rag_tool_decorator("soporte_tecnico", ...),
rag_tool_decorator("facturacion", ...),
rag_tool_decorator("recursos_humanos", ...),
CalculatorTool(), # No-RAG
WebSearchTool(), # No-RAG
],
llm=llm_model
)
# Pregunta compleja: "¿Puedo tomar vacaciones si tengo factura pendiente?"
# El agente:
# 1. Usa RAG RRHH para políticas vacaciones
# 2. Usa RAG facturación para estado de cuenta
# 3. Combina ambas respuestas
# 4. Da respuesta integrada
Escenario B: Sistema de Auditoría Automática
python
# Cada respuesta RAG genera metadatos auditables
respuesta_con_metadata = {
"answer": "No puede tomar vacaciones...",
"sources": [
{"document": "politica.pdf", "page": 15, "score": 0.95},
{"document": "manual.pdf", "page": 8, "score": 0.85}
],
"processing_steps": [
{"step": "retrieval", "documents_found": 10},
{"step": "reranking", "cohere_used": True},
{"step": "filtering", "documents_kept": 3},
{"step": "generation", "model": "Deepseek", "tokens": 450}
],
"timestamps": {
"start": "2024-01-15T16:32:17",
"end": "2024-01-15T16:32:37",
"total_seconds": 20
}
}
# Auditor puede verificar CADA decisión
Escenario C: Sistema de Aprendizaje Continuo
python
# Feedback loop para mejorar el sistema
def procesar_feedback(respuesta_id, fue_util, correccion=None):
if not fue_util:
# Analizar por qué falló
logs = obtener_logs(respuesta_id)
if logs["scores_bajos"]:
# ¿Mejorar embeddings?
sugerir_retraining_embeddings()
if logs["cohere_no_mejoró"]:
# ¿Ajustar threshold?
ajustar_threshold_automático()
if correccion:
# Usar corrección para mejorar
agregar_a_dataset_finetuning(correccion)
🎯 Conclusión de la Integración: El Sistema como un Organismo
Este diagrama final nos muestra que nuestro sistema RAG no es una simple tubería de datos, sino un organismo complejo y adaptativo:
Principios de Integración Demostrados:
- ✅ Resiliencia distribuida: Cada componente tiene su propia estrategia de fallback
- ✅ Optimización colaborativa: El caché beneficia a todos los usuarios
- ✅ Transparencia integrada: Debugging no es añadido, es fundamental
- ✅ Escalabilidad natural: Más usuarios = mejor ratio de cache hit
- ✅ Mantenibilidad: Cada componente actualizable independientemente
La Lección Más Importante:
La calidad de un sistema no está en sus componentes individuales, sino en cómo se integran.
Podrías tener:
- El mejor algoritmo de búsqueda del mundo
- El modelo de re-ranking más preciso
- El LLM más inteligente
Pero si no se comunican bien, si no degradan elegantemente, si no comparten estado inteligentemente — el sistema fallará en producción.
Nuestro sistema triunfa porque:
- Coordina sin acoplar
- Optimiza sin complicar
- Informa sin abrumar
- Resiste sin rigidez
- Aprende sin romper
🔮 El Futuro de Esta Integración
Este sistema está diseñado para evolucionar:
graph LR
A[Sistema Actual] --> B[➕ Nuevos componentes]
A --> C[🔄 Componentes mejorados]
A --> D[🔗 Nuevas integraciones]
B --> E[🎭 Multi-modalidad<br/>(imágenes, audio)]
C --> F[⚡ Embeddings más rápidos]
D --> G[🌐 APIs externas]
E --> H[Sistema V2]
F --> H
G --> H
style A fill:#E1F5FE
style H fill:#C8E6C9Posibles evoluciones:
- Cache distribuido (Redis) para múltiples instancias
- Load balancer inteligente entre diferentes LLMs
- Sistema de feedback automático que ajusta thresholds
- Integración con bases de conocimiento dinámicas
- Soporte para queries complejas (multi-hop reasoning)
Pero la arquitectura central permanece: Porque está diseñada no para una tecnología específica, sino para un principio: transformar preguntas en respuestas verificables, de manera eficiente, transparente y resiliente.
Esta es la verdadera integración: no solo componentes que funcionan juntos, sino componentes que se hacen mejores al trabajar juntos.
Parte 8: Conclusión y Aprendizajes Clave – El Arte de Construir Sistemas RAG que Sobreviven al Mundo Real
🎯 La Gran Lección: No Es Solo Código, Es Filosofía de Diseño
Después de este viaje detallado a través de nuestro sistema RAG, hemos descubierto algo crucial: construir un sistema que funcione en un laboratorio es fácil; construir uno que sobreviva en producción es un arte.
Este código no es solo una implementación técnica de RAG. Es una declaración de principios sobre cómo construir sistemas de IA que sean útiles, confiables y mantenibles.
🔑 Los 10 Aprendizajes Clave que Este Código Demuestra
1. La Resiliencia Es una Característica de Diseño, No un Parche
python
# NO es un afterthought:
def safe_rerank(docs, query, reranker):
if time.now() < COHERE_DISABLED_UNTIL: # Resiliencia INTEGRADA
return docs # Fallback elegante
# VS el enfoque naive:
def rerank_naive(docs, query, reranker):
try:
return reranker.compress_documents(docs, query)
except:
raise Exception("¡Todo roto!") # ❌ Usuario decepcionado
Aprendizaje: Diseña pensando «¿qué pasa cuando esto falla?» desde el día 1.
2. La Transparencia Genera Confianza
La función inspect() no es solo para debugging. Es un contrato de confianza con los usuarios:
text
Cuando el sistema dice: "Según política página 15..." El usuario puede: IR a la política página 15 y VERIFCAR
Transparencia ≠ complejidad. Es mostrar justo lo suficiente para que los usuarios confíen, sin abrumarlos.
3. El Caché No Es Solo Optimización, Es Experiencia de Usuario
python
# Impacto REAL del caché: # Usuario 1: "Oye, tarda 1.5 segundos" # Usuario 100: "¡Wow, es instantáneo!" # Usuario 1000: "¿Cómo puede ser tan rápido?"
Aprendizaje: Lo que parece una optimización técnica (caché) se traduce directamente en percepción de calidad por parte del usuario.
4. Los Umbrales Son Juicios de Valor, No Números Mágicos
python
# 0.15 no es un número mágico RAG_FILTER_THRESHOLD = 0.15 # Significa: "Esto es lo suficientemente bueno" # Traducido a negocio: # "Aceptamos documentos con 15% de relevancia mínima" # "Equilibrio entre precisión y cobertura"
Aprendizaje: Cada parámetro configurable representa un trade-off de negocio. 0.15 significa «preferimos algo de ruido antes que perder señal importante».
5. La Modularidad Es Libertad
python
# Hoy: retriever = PGVector(...) reranker = CohereRerank(...) llm = Deepseek(...) # Mañana podría ser: retriever = Pinecone(...) # ♻️ Cambia solo esta línea reranker = SinReranking() # ♻️ Cambia solo esta línea llm = GPT4(...) # ♻️ Cambia solo esta línea # El pipeline sigue funcionando
Aprendizaje: Diseñar con interfaces claras te da opciones futuras sin reescribir todo.
6. La Degradación Elegante Es Mejor que la Perfección Frágil
graph TD
A[Calidad 100%] -->|Cohere falla| B[Calidad 85%]
B -->|ByteStore falla| C[Calidad 70%]
C -->|Filtro muy estricto| D[Calidad 60%]
style A fill:#4CAF50
style B fill:#8BC34A
style C fill:#FFC107
style D fill:#FF9800Aprendizaje: Un sistema que siempre responde (aunque no perfecto) es mejor que uno que a veces responde perfectamente y a veces no responde.
7. La Configuración Externa Es Autonomía Operacional
bash
# .env - El "panel de control" del sysadmin RAG_FILTER_THRESHOLD=0.15 # "Hoy estamos estrictos" RAG_FILTER_MAX_DOCS=6 # "Hoy queremos respuestas rápidas" COHERE_RERANK_COOLDOWN_SEC=180 # "Cohere está sensible hoy"
Aprendizaje: Separar configuración de código empodera al equipo de operaciones para ajustar el sistema sin depender de desarrolladores.
8. Los Logs Son Tu Memoria Institucional
python
# Cada interacción enseña algo: [16:32:17] Pregunta: "¿Vacaciones con 6 meses?" [16:32:21] Documentos encontrados: 10 [16:32:25] Cohere mejoró orden: política.pdf subió de #3 a #1 [16:32:26] Filtro: 3/10 documentos aprobados (score >= 0.15) [16:32:37] Respuesta entregada: 20 segundos # Patrón descubierto: "Cohere consistentemente mejora políticas sobre leyes"
Aprendizaje: Un sistema bien instrumentado aprende de sí mismo a través del tiempo.
9. El Diseño para Debugging Es Diseño para Mantenibilidad
python
# Función inspect() no es un "extra" # Es la diferencia entre: # "Algo está mal" (sin inspect) # VS # "El documento 'ley.pdf' tiene score 0.12, no pasa el filtro 0.15" (con inspect)
Aprendizaje: El tiempo que ahorras en debugging paga con creces el tiempo invertido en instrumentación.
10. RAG Es un Medio, No un Fin
El objetivo final NO es «implementar RAG». Es:
- ✅ Dar respuestas precisas basadas en documentos confiables
- ✅ Ahorrar tiempo a las personas que buscan información
- ✅ Tomar mejores decisiones con información completa
- ✅ Democratizar el acceso al conocimiento organizacional
RAG es el «cómo», no el «qué».
🏗️ Los 5 Principios de Diseño que Hacen que Este Sistema Funcione
Principio 1: Fallar es Humano, Recuperarse es Diseño
python
# Cada componente asume que otros PUEDEN fallar # Y está preparado para continuar de todos modos
Principio 2: Lo Simple Escala, lo Complejo Colapsa
python
# Caché con diccionarios Python > Sistema distribuido complejo # (Para este caso de uso)
Principio 3: La Transparencia Es la Nueva Confiabilidad
python
# En la era de la IA, "confía en mí" ya no basta # "Mira cómo llegué a esto" es el nuevo estándar
Principio 4: Optimiza para el Caso Común, Diseña para el Raro
python
# 99% del tiempo: Caché hit, Cohere funciona # 1% del tiempo: Fallbacks activados # Sistema optimizado para 99%, diseñado para 100%
Principio 5: El Usuario Final Es el Juez Final
python
# Toda decisión técnica se traduce a: # - ¿El usuario obtiene respuesta más rápido? # - ¿El usuario confía más en la respuesta? # - ¿El usuario puede verificar si quiere?
🚀 De la Teoría a la Práctica: Cómo Aplicar Estos Aprendizajes
Si Eres un Desarrollador Implementando RAG:
- Comienza con fallbacks: Antes de que todo funcione perfectamente, asegúrate de que algo funcione siempre
- Instrumenta desde el día 1: No añadas logging después, intégralo desde el diseño
- Separa configuración: Los parámetros que pueden cambiar van en .env, no en código
- Diseña interfaces claras: Mañana querrás cambiar un componente
- Piensa en caché temprano: La primera optimización que necesitarás
Si Eres un Líder de Producto/Proyecto:
- Define «éxito» en términos de usuario: No «95% de precisión», sino «empleados encuentran políticas en 30 segundos»
- Exige transparencia: Los usuarios necesitan saber de dónde viene la información
- Planifica para la degradación: ¿Qué pasa cuando la API externa falla?
- Mide lo que importa: Tiempo de respuesta, satisfacción del usuario, no solo métricas técnicas
- Invierte en mantenibilidad: Un sistema que puedes ajustar rápidamente vale más que uno «perfecto» pero rígido
Si Eres un Usuario Final (o Representante de Usuarios):
- Explica verificación: «¿Cómo verifico que esto es correcto?»
- Proporciona feedback: Cuando la respuesta es útil/mala, explica por qué
- Identifica casos de uso reales: No «búsqueda general», sino «encontrar cláusula específica en contrato»
- Valora velocidad y confiabilidad: 95% preciso en 20 segundos > 99% preciso en 2 minutos
- Participa en la evolución: Los mejores sistemas crecen con sus usuarios
📈 El Viaje Continuo: De Este Sistema a Sistemas Futuros
Este código representa un punto en el tiempo en la evolución de los sistemas RAG. Pero los principios que encarna son atemporales:
Lo que Probablemente Cambiará:
- Modelos más avanzados: GPT-5, Claude 3, etc.
- Vector stores más rápidos: Nuevas bases de datos especializadas
- Técnicas de re-ranking: Mejores algoritmos que Cohere
- Interfaces de usuario: Chat, voz, multimodal
Lo que Probablemente Permanecerá:
- Necesidad de transparencia: Los usuarios siempre querrán saber «¿por qué?»
- Importancia de la resiliencia: Las cosas siempre fallarán
- Valor de la velocidad: El tiempo siempre será escaso
- Poder de la modularidad: El cambio siempre será constante
🎓 El Examen Final: ¿Aprendiste la Lección?
Para verificar que entendiste los aprendizajes clave, responde estas preguntas:
Pregunta 1: Si Cohere API comienza a devolver errores 429 (rate limit), ¿qué hace nuestro sistema?
Respuesta correcta: Activa el circuit breaker, usa documentos sin re-ranking por 3 minutos, continúa dando respuestas (degradadas pero funcionales).
Pregunta 2: ¿Por qué tenemos DOS almacenes (PGVector y ByteStore)?
Respuesta correcta: PGVector para búsqueda rápida (fragmentos), ByteStore para contexto completo. Separación de preocupaciones: velocidad vs completitud.
Pregunta 3: Un usuario pregunta «¿de dónde sacaste eso?» ¿Cómo responde el sistema?
Respuesta correcta: Cita fuente específica (documento + página) gracias al formateo y transparencia integrada.
Pregunta 4: ¿Cómo ajustas el balance entre precisión y cobertura?
Respuesta correcta: Cambiando RAG_FILTER_THRESHOLD en el archivo .env (sin tocar código).
Pregunta 5: Usuario 1 tarda 1.5 segundos, Usuario 100 tarda 0.01 segundos. ¿Por qué?
Respuesta correcta: Caché de retrievers y bytestores. La primera creación es costosa, reutilizar es casi gratis.
🌟 El Legado de Este Diseño
Este sistema RAG nos deja un legado más valioso que el código en sí:
Un Marco Mental para Sistemas de IA:
- Humildad técnica: Asume que todo puede fallar
- Transparencia radical: Nada de cajas negras
- Optimización progresiva: Mejora con el uso, no solo con el desarrollo
- Enfoque en el usuario: Técnica al servicio de necesidades humanas
Un Conjunto de Patrones Transferibles:
- Circuit breaker para APIs externas
- Caché para recursos costosos
- Configuración externa para flexibilidad operacional
- Pipeline modular para mantenibilidad
- Instrumentación integrada para debugging
Una Filosofía de Ingeniería:
La mejor ingeniería no es la que hace que nada falle nunca, sino la que hace que cuando algo falle, nadie se dé cuenta.
🚪 La Puerta a lo que Viene
Este análisis detallado de un sistema RAG es solo el principio. Las mismas lecciones aplican a:
- Sistemas de recomendación (¿transparentes en por qué recomiendan?)
- Chatbots empresariales (¿pueden citar fuentes?)
- Motores de búsqueda internos (¿degradan elegantemente?)
- Asistentes de toma de decisiones (¿muestran su razonamiento?)
La pregunta final no es: «¿Implementamos RAG correctamente?»
La pregunta final es: «¿Construimos un sistema que realmente ayude a las personas, de manera confiable, transparente y mantenible?»
Por el diseño que hemos analizado, la respuesta es un rotundo SÍ.
📚 Recapitulación en Una Sola Frase
«Un sistema RAG bien diseñado transforma preguntas en respuestas verificables, de manera eficiente, transparente y resiliente, siempre priorizando la experiencia del usuario final sobre la elegancia técnica.»
Este código no es perfecto — ningún sistema lo es. Pero encarna principios que son más duraderos que cualquier tecnología específica. Y eso es lo que finalmente importa en la ingeniería de software: no solo resolver el problema de hoy, sino crear soluciones que puedan evolucionar para resolver los problemas de mañana.







