Módulo 8: RAG Evaluation + Proyecto Integrador
Fundamentos de RAG Evaluation
Descripción de la cápsula
Antes de elegir métricas o configurar herramientas, necesitas un marco mental claro de qué estás evaluando y por qué. Sin ese marco, vas a terminar midiendo cosas fáciles de medir en lugar de cosas que importan, y vas a celebrar mejoras que no se traducen en mejor experiencia de usuario.
Un sistema RAG tiene dos etapas que deben evaluarse por separado: retrieval (qué documentos se recuperan) y generation (qué respuesta se produce). Si solo mides la respuesta final, no sabes si el problema es que recuperaste documentos malos o si el LLM ignoró documentos buenos. Si solo mides retrieval, no sabes si la respuesta realmente contesta lo que el usuario preguntó.
Esta cápsula construye ese marco: qué medir en cada etapa, qué métricas usar, cuándo combinarlas, y cómo diseñar tu sistema de logging para que la evaluación sea posible mañana sin reconstruir nada.
Al terminar tendrás claridad sobre qué tipo de evaluación necesitas para tu caso (no todos necesitan lo mismo) y qué datos debes empezar a registrar hoy aunque todavía no tengas el pipeline de RAGAS implementado.
La regla fundamental: separa retrieval y generation
Imagina que un usuario pregunta "¿cuál es la política de vacaciones para empleados con menos de un año?" y recibe una respuesta incorrecta. Sin separar etapas, lo único que sabes es "el sistema falló". Con separación clara, puedes diagnosticar:
| Caso | Retrieval | Generation | Diagnóstico |
|---|---|---|---|
| A | Recuperó la política correcta | Respuesta correcta | ✅ Sistema OK |
| B | Recuperó la política correcta | Respuesta inventada | 🔥 Bug en prompt o LLM ignora contexto |
| C | Recuperó política equivocada | Respuesta basada en contexto malo | 🔥 Bug en chunking, embeddings o filtros |
| D | No recuperó nada relevante | LLM "completa" con conocimiento general | 🔥 Bug doble: retrieval falla, prompt no instruye a abstenerse |
Cada caso tiene una solución completamente distinta. No puedes diagnosticar sin separar.
Qué evaluar en retrieval
El retriever produce una lista ordenada de documentos. Las preguntas son:
- ¿Los documentos son relevantes a la query?
- ¿Cubren la evidencia necesaria para responder?
- ¿El ranking es estable a través de distintos tipos de query (técnicas, de búsqueda, conversacionales)?
Métricas estándar:
| Métrica | Qué mide | Cuándo usarla |
|---|---|---|
| Precision@k | De los k recuperados, cuántos son relevantes | Cuando el costo de procesar irrelevantes es alto (LLM consumirá los k) |
| Recall@k | De los relevantes existentes, cuántos recuperaste | Cuando perder evidencia es costoso (preguntas que requieren múltiples docs) |
| MRR (Mean Reciprocal Rank) | Posición del primer relevante | Cuando solo importa que aparezca pronto |
| NDCG@k | Ranking ponderado por relevancia gradada | Cuando hay relevancia parcial (no binaria) |
def precision_at_k(retrieved_ids: list[str], relevant_ids: set[str], k: int) -> float:
top_k = retrieved_ids[:k]
if not top_k:
return 0.0
return sum(1 for d in top_k if d in relevant_ids) / k
def recall_at_k(retrieved_ids: list[str], relevant_ids: set[str], k: int) -> float:
if not relevant_ids:
return 1.0
top_k = set(retrieved_ids[:k])
return len(top_k & relevant_ids) / len(relevant_ids)
def mrr(retrieved_ids: list[str], relevant_ids: set[str]) -> float:
for idx, doc_id in enumerate(retrieved_ids, start=1):
if doc_id in relevant_ids:
return 1.0 / idx
return 0.0
Regla práctica para tu sistema: mide recall@20 (lo que pasa al re-ranker) y precision@5 (lo que llega al LLM). Si recall@20 es bajo, mejora el retriever. Si recall@20 es alto pero precision@5 es bajo, mejora el re-ranker.
Qué evaluar en generation
El generador toma contexto + query y produce una respuesta. Las preguntas son:
- ¿La respuesta está soportada por el contexto (grounding)?
- ¿La respuesta contesta la pregunta original (relevancy)?
- ¿La respuesta es factualmente correcta (cuando hay ground truth)?
Métricas estándar (las trabajaremos a fondo en cápsula 03):
| Métrica | Qué mide | Cómo se calcula |
|---|---|---|
| Faithfulness | Respuesta soportada por contexto | LLM-as-judge: ¿cada claim de la respuesta está en el contexto? |
| Answer Relevancy | Respuesta contesta la pregunta | LLM-as-judge: ¿la respuesta aborda la query? |
| Correctness | Respuesta vs ground truth | Comparación semántica con respuesta de referencia |
| Context Precision | Documentos relevantes están al inicio | Posición de docs útiles en el contexto |
| Context Recall | Contexto contiene la info necesaria | ¿Ground truth se puede derivar del contexto? |
Punto crítico: faithfulness ≠ correctness. Una respuesta puede ser fiel al contexto (todo lo que dice está en los docs) pero incorrecta (los docs estaban equivocados). Y puede ser correcta pero no fiel (el LLM la sabía de su training, no del contexto). Necesitas ambas.
Evaluación manual vs automática
Hay un espectro entre evaluación 100% manual (humano lee y juzga) y 100% automática (LLM-as-judge sin supervisión). Cada extremo tiene problemas:
| Criterio | Manual | Automática (LLM-judge) |
|---|---|---|
| Profundidad cualitativa | Alta: humano captura sutileza | Media: LLM puede malinterpretar nuance |
| Escalabilidad | Baja: 100 queries = 4 horas humanas | Alta: 1000 queries = minutos |
| Costo por iteración | Alto: tiempo humano caro | Bajo-medio: solo costo API |
| Reproducibilidad | Baja: jueces humanos discrepan | Alta con temperature=0 |
| Detección de errores nuevos | Excelente | Pobre: solo detecta lo que sabe medir |
| Cuándo usarla | Smoke tests, validación de jueces | Regresión continua, CI/CD |
Regla práctica: combina ambas con frecuencias distintas:
- Cada PR: 10 queries con LLM-as-judge (smoke test, 30 segundos)
- Cada noche: 100 queries con LLM-as-judge (full set)
- Cada release: 20 queries con revisión humana (calibración del juez)
- Cada trimestre: 50 queries nuevas anotadas a mano (renovar golden dataset)
Sin la calibración humana periódica, tu LLM-judge va a desviar y dejar de detectar problemas reales.
Diseño de logging para evaluación posible
Para evaluar mañana, necesitas registrar hoy. Tu pipeline debe persistir cada query con suficiente detalle:
from pydantic import BaseModel
from datetime import datetime
class RAGTrace(BaseModel):
trace_id: str
timestamp: datetime
tenant_id: str
query: str
expanded_queries: list[str] | None = None # M03 query expansion
retrieved_docs: list[dict] # [{"id": ..., "score": ..., "content": ...}]
reranked_docs: list[dict] | None = None # M04 reranking
final_context: list[str] # lo que efectivamente fue al LLM
answer: str
latency_ms: dict # {"retrieval": 80, "rerank": 120, "generation": 850}
model: str
user_feedback: str | None = None # thumbs up/down si lo capturas
Por qué cada campo importa:
retrieved_docsyreranked_docsseparados: para diagnosticar dónde falla precision (caso C arriba)final_context: para reproducir exactamente lo que el LLM vio (no asumas, registra)latency_mspor etapa: para correlacionar calidad con performance (a veces respuestas malas son timeouts truncados)user_feedback: ground truth gratis. Cada thumbs down es una query candidata para tu golden dataset.
Conexión con el proyecto final
Tu Advanced RAG System debe separar explícitamente tres capas de métricas:
- Métricas de retrieval: precision@5, recall@20, MRR sobre golden dataset
- Métricas de generation: faithfulness, answer relevancy, correctness con RAGAS
- Métricas de sistema: p95 latencia, costo por query, tasa de errores
Cuando reportes mejoras en el README final, vas a poder decir cosas como:
"M3 query expansion mejoró recall@20 de 0.72 → 0.84 (+17%). M4 cross-encoder re-ranking mejoró precision@5 de 0.65 → 0.78 (+20%). El sistema completo tiene faithfulness 0.91 vs baseline simple RAG 0.74."
Sin separación de capas, lo único que puedes decir es "el sistema mejoró" — afirmación inútil.
Troubleshooting
Problema 1: "Una sola métrica resume todo"
Causa: simplificación excesiva, típicamente "answer correctness" como métrica única.
Solución: define al menos 4 métricas complementarias (2 retrieval + 2 generation). Una sola métrica esconde trade-offs y permite optimizar en una dimensión a costa de otras.
Problema 2: "No sé si falla retrieval o generación"
Causa: no separaste etapas en el logging ni en las métricas.
Solución: registra retrieved_docs, final_context y answer en cada trace. Calcula métricas de retrieval y de generación por separado. Cuando algo falla, mira primero retrieval (causa raíz más común).
Problema 3: "Evaluación lenta y cara"
Causa: correr 1000 queries con LLM-judge en cada PR cuesta dinero y tiempo.
Solución: estructura por capas: smoke (10 queries en cada PR), full (100 nightly), exhaustive (1000 weekly). Usa modelo barato (gpt-4o-mini) para el judge salvo en evaluaciones críticas pre-release.
Problema 4: "Métricas suben pero usuarios se quejan"
Causa: golden dataset no representa tráfico real; mide casos fáciles.
Solución: muestrea queries reales de logs (con consentimiento/anonimización) y conviértelas en golden dataset. Renueva 20% del dataset cada trimestre con queries que actualmente fallan.
Problema 5: "LLM-judge da scores inconsistentes"
Causa: temperature > 0 o prompts vagos.
Solución: temperature=0, prompts con criterios explícitos y ejemplos. Calibra mensualmente comparando el judge contra evaluación humana sobre 20 queries; si la correlación cae por debajo de 0.7, el judge necesita re-prompt.
Ejercicios
Ejercicio 1: Define el set mínimo de métricas para tu sistema
Para un sistema RAG de soporte técnico (queries específicas, ground truth disponible), define exactamente 5 métricas con justificación.
Ver solución
metrics_plan = {
"retrieval": {
"recall_at_20": "Garantiza que el reranker tenga material; perder relevantes aquí es irrecuperable",
"precision_at_5": "Lo que llega al LLM; ruido aquí daña respuestas",
},
"generation": {
"faithfulness": "El bug más común en soporte técnico es alucinar pasos no documentados",
"answer_relevancy": "Respuestas que divagan frustran a usuarios que buscan acción",
"correctness": "Tenemos ground truth para tickets resueltos; aprovéchalo",
},
}
Explicación: combinas dos retrieval (cobertura + precisión post-rerank) con tres generation (grounding + utilidad + exactitud). Cinco métricas son suficientes para diagnosticar 95% de problemas sin ahogarte en dashboards.
Ejercicio 2: Diseña la estructura de logging RAG
Define un schema Pydantic completo para registrar cada query del sistema, con campos suficientes para evaluación retrospectiva.
Ver solución
from pydantic import BaseModel
from datetime import datetime
class RetrievedDoc(BaseModel):
doc_id: str
score: float
content_snippet: str
metadata: dict
class RAGTrace(BaseModel):
trace_id: str
timestamp: datetime
tenant_id: str
query: str
expanded_queries: list[str] = []
retrieved_docs: list[RetrievedDoc]
reranked_docs: list[RetrievedDoc] = []
final_context: list[str]
answer: str
latency_ms: dict
model: str
cost_usd: float
user_feedback: int | None = None # 1, -1 o None
Explicación: este schema permite reproducir exactamente lo que pasó en cada query. cost_usd permite correlacionar calidad con costo (a veces respuestas malas son por modelos cheap escogidos automáticamente).
Ejercicio 3: Plan de evaluación por capas
Diseña un plan de evaluación con frecuencias distintas según trade-off velocidad/cobertura/costo.
Ver solución
evaluation_plan = {
"smoke": {
"queries": 10,
"frequency": "every_pr",
"metrics": ["faithfulness", "answer_relevancy"],
"judge_model": "gpt-4o-mini",
"max_runtime_min": 2,
"blocking": True, # PR no puede mergear si baja
},
"full": {
"queries": 100,
"frequency": "nightly",
"metrics": ["recall_at_20", "precision_at_5", "faithfulness", "relevancy", "correctness"],
"judge_model": "gpt-4o-mini",
"max_runtime_min": 15,
"blocking": False, # alerta pero no bloquea
},
"exhaustive": {
"queries": 500,
"frequency": "weekly",
"metrics": "all",
"judge_model": "gpt-4o", # judge más fuerte
"max_runtime_min": 60,
"blocking": False,
},
"human_calibration": {
"queries": 20,
"frequency": "monthly",
"method": "human_review",
"purpose": "valida que LLM-judge no haya desviado",
},
}
Explicación: smoke bloquea para feedback rápido; full corre nightly para detectar regresiones acumuladas; exhaustive valida pre-release; calibración humana mantiene el judge honesto.
Ejercicio 4: Diagnóstico por separación de capas
Una query devolvió respuesta incorrecta. Diseña un script de diagnóstico que diga si el problema fue retrieval o generation.
Ver solución
def diagnose_failure(trace: RAGTrace, ground_truth: str, relevant_doc_ids: set[str]) -> str:
retrieved_relevant = any(d.doc_id in relevant_doc_ids for d in trace.retrieved_docs)
final_context_has_answer = any(
ground_truth.lower()[:50] in ctx.lower() for ctx in trace.final_context
)
if not retrieved_relevant:
return "RETRIEVAL_FAIL: ningún documento relevante recuperado"
if not final_context_has_answer:
return "RERANK_FAIL: relevante recuperado pero filtrado del contexto final"
if final_context_has_answer:
return "GENERATION_FAIL: contexto contenía la respuesta, LLM no la usó"
return "UNKNOWN"
Explicación: este diagnóstico sigue el árbol de decisión retrieval → rerank → generation. Cada caso requiere acción distinta: retrieval fail apunta a embeddings o chunking; rerank fail apunta al re-ranker; generation fail apunta al prompt o modelo.
Resumen
- RAG evaluation requiere separar retrieval de generation; sin separación no puedes diagnosticar
- Retrieval mide relevancia y cobertura: precision@k, recall@k, MRR, NDCG
- Generation mide grounding y utilidad: faithfulness, answer relevancy, correctness
- Faithfulness ≠ correctness: necesitas ambas porque miden cosas distintas
- Combina evaluación automática (volumen) con manual (calibración mensual)
- Diseña tu logging hoy para que la evaluación sea posible mañana sin rehacer nada
- Estructura por capas: smoke (PR), full (nightly), exhaustive (weekly), human (monthly)
Recursos adicionales
- RAGAS Introduction - Fundamentos del framework.
- Evaluating RAG Systems - Pinecone - Guía práctica.
- OpenAI Evals Guide - Diseño de evaluaciones.
- Information Retrieval Evaluation - Wikipedia - Métricas IR clásicas.
- Eugene Yan on Evals - Síntesis exhaustiva.
- LangSmith Eval Docs - Patrones operacionales.
Creado: Marzo 13, 2026
Versión: 2.0