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:

CasoRetrievalGenerationDiagnóstico
ARecuperó la política correctaRespuesta correcta✅ Sistema OK
BRecuperó la política correctaRespuesta inventada🔥 Bug en prompt o LLM ignora contexto
CRecuperó política equivocadaRespuesta basada en contexto malo🔥 Bug en chunking, embeddings o filtros
DNo recuperó nada relevanteLLM "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étricaQué mideCuándo usarla
Precision@kDe los k recuperados, cuántos son relevantesCuando el costo de procesar irrelevantes es alto (LLM consumirá los k)
Recall@kDe los relevantes existentes, cuántos recuperasteCuando perder evidencia es costoso (preguntas que requieren múltiples docs)
MRR (Mean Reciprocal Rank)Posición del primer relevanteCuando solo importa que aparezca pronto
NDCG@kRanking ponderado por relevancia gradadaCuando 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étricaQué mideCómo se calcula
FaithfulnessRespuesta soportada por contextoLLM-as-judge: ¿cada claim de la respuesta está en el contexto?
Answer RelevancyRespuesta contesta la preguntaLLM-as-judge: ¿la respuesta aborda la query?
CorrectnessRespuesta vs ground truthComparación semántica con respuesta de referencia
Context PrecisionDocumentos relevantes están al inicioPosición de docs útiles en el contexto
Context RecallContexto 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:

CriterioManualAutomática (LLM-judge)
Profundidad cualitativaAlta: humano captura sutilezaMedia: LLM puede malinterpretar nuance
EscalabilidadBaja: 100 queries = 4 horas humanasAlta: 1000 queries = minutos
Costo por iteraciónAlto: tiempo humano caroBajo-medio: solo costo API
ReproducibilidadBaja: jueces humanos discrepanAlta con temperature=0
Detección de errores nuevosExcelentePobre: solo detecta lo que sabe medir
Cuándo usarlaSmoke tests, validación de juecesRegresió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_docs y reranked_docs separados: para diagnosticar dónde falla precision (caso C arriba)
  • final_context: para reproducir exactamente lo que el LLM vio (no asumas, registra)
  • latency_ms por 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:

  1. Métricas de retrieval: precision@5, recall@20, MRR sobre golden dataset
  2. Métricas de generation: faithfulness, answer relevancy, correctness con RAGAS
  3. 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

  1. RAGAS Introduction - Fundamentos del framework.
  2. Evaluating RAG Systems - Pinecone - Guía práctica.
  3. OpenAI Evals Guide - Diseño de evaluaciones.
  4. Information Retrieval Evaluation - Wikipedia - Métricas IR clásicas.
  5. Eugene Yan on Evals - Síntesis exhaustiva.
  6. LangSmith Eval Docs - Patrones operacionales.

Creado: Marzo 13, 2026
Versión: 2.0