Módulo 8: RAG Evaluation + Proyecto Integrador

Métricas Core de RAGAS

Descripción de la cápsula

RAGAS (Retrieval Augmented Generation Assessment) es el framework abierto de evaluación que se ha convertido en el estándar de facto para sistemas RAG. Su valor no es que tenga la mejor métrica única — es que ofrece un set coherente de métricas pensadas específicamente para la arquitectura RAG, calculadas con LLM-as-judge en un pipeline reproducible.

En esta cápsula vas a aprender las cuatro métricas core de RAGAS — faithfulness, answer relevancy, context precision y context recall — entender qué mide cada una a nivel mecánico (cómo el LLM-judge llega al score), interpretar los resultados con criterio (qué significa un 0.85 vs 0.90), y mapear cada métrica a una acción concreta cuando cae bajo threshold.

Al terminar tendrás el vocabulario y la intuición para hablar de calidad RAG en términos defendibles, no en métricas inventadas o intuiciones. Cuando un stakeholder pregunte "¿por qué dices que el sistema mejoró?", vas a poder señalar números específicos con interpretación clara.


El paradigma LLM-as-judge

Las cuatro métricas RAGAS comparten un mecanismo común: un LLM (el "judge") evalúa la respuesta del sistema contra criterios estructurados. Esto tiene tres implicaciones importantes:

  1. Las métricas no son determinísticas sin temperature=0. Aún con temperature=0, distintos modelos producen scores ligeramente distintos.
  2. El judge necesita ser tan capaz o más que el modelo evaluado. Evaluar gpt-4o con gpt-3.5 produce ruido.
  3. Los scores son comparativos, no absolutos. Un faithfulness de 0.87 no significa "87% correcto"; significa "más alto que 0.80 que tenías antes".
from openai import OpenAI

client = OpenAI()

def llm_judge(prompt: str, model: str = "gpt-4o-mini") -> str:
    response = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        temperature=0,  # crítico para reproducibilidad
        seed=42,        # mejora consistencia entre runs
    )
    return response.choices[0].message.content

1) Faithfulness: ¿la respuesta está soportada por el contexto?

Qué mide: qué fracción de los claims (afirmaciones) en la respuesta pueden ser inferidos del contexto recuperado.

Cómo se calcula (mecánica interna):

  1. El judge descompone la respuesta en claims atómicos individuales
  2. Para cada claim, el judge evalúa si el contexto lo soporta
  3. Score = (claims soportados) / (total de claims)
# Ejemplo conceptual de cómo RAGAS calcula faithfulness

def faithfulness_score(answer: str, context: list[str]) -> float:
    claims = extract_claims_with_llm(answer)
    if not claims:
        return 1.0
    supported = 0
    context_text = "\n".join(context)
    for claim in claims:
        if claim_is_supported_by(claim, context_text):
            supported += 1
    return supported / len(claims)

Interpretación:

ScoreSignificadoAcción
0.95-1.0Casi sin alucinacionesMantén el sistema
0.85-0.95Aceptable para producciónMonitorea, no urgente
0.70-0.85Riesgo de alucinacionesInvestigar prompt y contexto
<0.70Sistema alucina sistemáticamenteBloquear deploy, fix inmediato

Cuándo faithfulness baja sin causa obvia:

  • El contexto se truncó silenciosamente (token limit del LLM)
  • El prompt no instruye explícitamente "responde solo basándote en el contexto"
  • El modelo es demasiado pequeño y "completa" desde su training
  • Los chunks son demasiado cortos y pierden contexto relevante

2) Answer Relevancy: ¿la respuesta contesta la pregunta?

Qué mide: qué tan directa, completa y enfocada es la respuesta respecto a la query original.

Cómo se calcula:

  1. El judge genera N preguntas hipotéticas que la respuesta podría estar contestando
  2. Cada pregunta hipotética se compara semánticamente con la query original (cosine similarity de embeddings)
  3. Score = promedio de similitudes

La intuición: si la respuesta contesta bien la query, "preguntas reverse-engineered" desde la respuesta deben parecerse a la query original.

def answer_relevancy_score(query: str, answer: str, n: int = 5) -> float:
    hypothetical_questions = generate_questions_from_answer_with_llm(answer, n=n)
    query_embedding = embed(query)
    similarities = [
        cosine_sim(query_embedding, embed(hq))
        for hq in hypothetical_questions
    ]
    return sum(similarities) / len(similarities)

Interpretación:

ScoreSignificado
0.90+Respuesta directamente alineada con query
0.75-0.90Útil pero podría ser más enfocada
0.60-0.75Respuesta divaga o aborda solo parte
<0.60Respuesta off-topic o evasiva

Causas típicas de relevancy bajo:

  • Query ambigua que el LLM interpretó diferente
  • Prompt que pide "respuesta exhaustiva" → respuestas largas con info no pedida
  • Contexto que no contiene la respuesta → LLM divaga o se abstiene mal

3) Context Precision: ¿los documentos relevantes están al inicio?

Qué mide: qué tan al principio del contexto aparecen los documentos verdaderamente útiles para responder.

Por qué importa: los LLMs tienen sesgo posicional ("lost in the middle"). Documentos al final del contexto tienden a ser ignorados aunque sean relevantes. Si tu retriever encontró el doc correcto pero lo puso en posición 9 de 10, vas a tener faithfulness alta pero respuestas pobres.

Cómo se calcula:

def context_precision_score(query: str, context_docs: list[str], ground_truth: str) -> float:
    relevant_flags = [
        is_useful_for_answering(query, doc, ground_truth)
        for doc in context_docs
    ]
    if not any(relevant_flags):
        return 0.0
    # Precision@k pesado por posición
    weighted_sum = 0.0
    relevant_count = 0
    for k, is_relevant in enumerate(relevant_flags, start=1):
        if is_relevant:
            relevant_count += 1
            weighted_sum += relevant_count / k
    return weighted_sum / sum(relevant_flags)

Interpretación práctica: un context precision bajo es señal de que necesitas re-ranking más agresivo o mejor calibración del re-ranker existente.


4) Context Recall: ¿el contexto contiene lo necesario?

Qué mide: qué fracción del ground truth puede derivarse del contexto recuperado.

Cómo se calcula:

  1. El judge descompone el ground truth en statements atómicos
  2. Para cada statement, evalúa si está cubierto por algún documento del contexto
  3. Score = (statements cubiertos) / (total de statements)
def context_recall_score(context: list[str], ground_truth: str) -> float:
    gt_statements = extract_statements_with_llm(ground_truth)
    if not gt_statements:
        return 1.0
    context_text = "\n".join(context)
    covered = sum(
        1 for stmt in gt_statements
        if statement_supported_by(stmt, context_text)
    )
    return covered / len(gt_statements)

Interpretación:

  • Recall alto + Faithfulness baja: el contexto tiene la respuesta pero el LLM alucina. Bug de prompt o modelo.
  • Recall bajo + Faithfulness alta: el LLM está siendo honesto sobre contexto incompleto. Bug de retrieval.
  • Recall bajo + Faithfulness baja: doble problema. El LLM alucina sobre contexto incompleto. Crítico.
  • Recall alto + Faithfulness alta: sistema saludable.

Esta matriz 2x2 es la herramienta diagnóstica más útil de RAGAS.


Implementación con RAGAS

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)
from datasets import Dataset

def run_ragas_evaluation(samples: list[dict]) -> dict:
    """
    samples: lista de dicts con keys:
        - question: str
        - answer: str (respuesta del sistema)
        - contexts: list[str] (documentos recuperados)
        - ground_truth: str (respuesta correcta)
    """
    dataset = Dataset.from_list(samples)
    result = evaluate(
        dataset=dataset,
        metrics=[
            faithfulness,
            answer_relevancy,
            context_precision,
            context_recall,
        ],
    )
    return {
        "faithfulness": float(result["faithfulness"]),
        "answer_relevancy": float(result["answer_relevancy"]),
        "context_precision": float(result["context_precision"]),
        "context_recall": float(result["context_recall"]),
    }

Interpretación combinada y matriz de diagnóstico

FaithfulnessRecallDiagnósticoAcción
AltaAltaSistema saludableMantener
AltaBajaHonesto pero retrieval fallaMejorar retriever (chunking, embeddings, hybrid)
BajaAltaLLM alucina con info disponibleFix prompt, mejor modelo, más explícito sobre grounding
BajaBajaCrítico: alucina sobre contexto pobreFix retrieval primero, luego generation
RelevancyPrecisionDiagnósticoAcción
AltaAltaRespuesta enfocada con buen contextoMantener
AltaBajaBuena respuesta a pesar de contexto desordenadoMejorar re-ranker (defensivo)
BajaAltaLLM divaga aunque tenga buen contextoFix prompt: ser más directivo
BajaBajaSistema confusoInvestigar query understanding

Conexión con el proyecto final

Tu Advanced RAG System debe reportar estas cuatro métricas como tabla en el README:

| Métrica | Baseline simple RAG | Advanced RAG | Mejora |
|---------|---------------------|--------------|--------|
| Faithfulness | 0.74 | 0.91 | +23% |
| Answer Relevancy | 0.81 | 0.89 | +10% |
| Context Precision | 0.62 | 0.84 | +35% |
| Context Recall | 0.71 | 0.88 | +24% |

Esta tabla es la prueba cuantitativa de que las técnicas de la guía valen lo que cuesta implementarlas. Sin ella, todo lo que construiste es teoría.


Troubleshooting

Problema 1: "Faithfulness alta pero respuestas se sienten malas"

Causa: la respuesta es fiel al contexto, pero el contexto es trivial o incompleto.
Solución: revisa context recall (probablemente bajo). El problema es retrieval, no generation. Mejora chunking, hybrid search o expansion.

Problema 2: "Context recall alto pero faithfulness bajo"

Causa: el LLM tiene la respuesta disponible pero alucina de todos modos.
Solución: prompt explícito: "responde solo con información del contexto; si no está, di 'no encuentro esa información'". Considera modelo más capaz si persiste.

Problema 3: "Métricas inestables entre corridas"

Causa: temperature > 0, dataset cambiante, judge model varía.
Solución: fija temperature=0, seed=42, dataset versionado en git, judge model fijo (gpt-4o-mini en producción, gpt-4o para validación trimestral).

Problema 4: "Context precision bajo aunque hay documentos relevantes"

Causa: el re-ranker no está priorizando bien, o no hay re-ranker.
Solución: introduce o calibra el cross-encoder de M04. Si ya hay re-ranker, revisa que esté operando sobre top_k suficiente del retriever.

Problema 5: "Costo de evaluación es prohibitivo"

Causa: corres todas las métricas sobre 500 queries con gpt-4o.
Solución: usa gpt-4o-mini como judge (10× más barato, calidad similar para 90% de casos). Reserva gpt-4o para evaluación pre-release. Reduce a 100 queries si suficiente para detectar regresiones.


Ejercicios

Ejercicio 1: Configurar las 4 métricas core

Implementa una función que evalúe un dataset y retorne todas las métricas en formato JSON serializable.

Ver solución
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset
import json

def evaluate_rag(samples: list[dict]) -> dict:
    dataset = Dataset.from_list(samples)
    result = evaluate(
        dataset=dataset,
        metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
    )
    scores = {
        "faithfulness": float(result["faithfulness"]),
        "answer_relevancy": float(result["answer_relevancy"]),
        "context_precision": float(result["context_precision"]),
        "context_recall": float(result["context_recall"]),
    }
    return scores

scores = evaluate_rag(my_samples)
print(json.dumps(scores, indent=2))

Explicación: retornar dict con float() explícito asegura que sea JSON serializable y comparable entre corridas.

Ejercicio 2: Definir thresholds calibrados

Define thresholds iniciales y una función que evalúe si un set de scores cumple criterios de release.

Ver solución
THRESHOLDS = {
    "faithfulness": 0.85,
    "answer_relevancy": 0.80,
    "context_precision": 0.75,
    "context_recall": 0.80,
}

def passes_release_gate(scores: dict, thresholds: dict = THRESHOLDS) -> tuple[bool, list[str]]:
    failures = []
    for metric, min_value in thresholds.items():
        if scores.get(metric, 0) < min_value:
            failures.append(f"{metric}: {scores[metric]:.3f} < {min_value}")
    return len(failures) == 0, failures

ok, failures = passes_release_gate(scores)
if not ok:
    print("Release blocked:")
    for f in failures:
        print(f"  - {f}")

Explicación: thresholds iniciales son punto de partida; calibra después de tener 1 mes de datos. Demasiado alto bloquea releases válidos; demasiado bajo no detecta regresiones.

Ejercicio 3: Diagnóstico automático con la matriz 2x2

Implementa la función diagnóstica que clasifica el problema según faithfulness × recall.

Ver solución
def diagnose(scores: dict) -> dict:
    f = scores["faithfulness"]
    r = scores["context_recall"]

    if f >= 0.85 and r >= 0.80:
        return {"status": "healthy", "action": "monitor"}
    if f >= 0.85 and r < 0.80:
        return {
            "status": "retrieval_gap",
            "action": "improve retriever (chunking/hybrid/filters)",
            "priority": "high",
        }
    if f < 0.85 and r >= 0.80:
        return {
            "status": "hallucination",
            "action": "fix prompt to enforce grounding; consider stronger model",
            "priority": "critical",
        }
    return {
        "status": "compound_failure",
        "action": "fix retrieval first, then generation",
        "priority": "critical",
    }

print(diagnose(scores))

Explicación: clasificar el problema en 4 cuadrantes acelera el debug. El caso (baja, alta) es típico de prompts permisivos y se fixea sin tocar retrieval.

Ejercicio 4: Reporte comparativo baseline vs advanced

Crea una función que compare dos sets de scores y genere un reporte tabular en markdown.

Ver solución
def compare_systems(baseline: dict, advanced: dict) -> str:
    metrics = ["faithfulness", "answer_relevancy", "context_precision", "context_recall"]
    lines = ["| Métrica | Baseline | Advanced | Mejora |", "|---------|----------|----------|--------|"]
    for m in metrics:
        b = baseline[m]
        a = advanced[m]
        improvement = ((a - b) / b) * 100 if b > 0 else 0
        lines.append(f"| {m.replace('_', ' ').title()} | {b:.3f} | {a:.3f} | +{improvement:.1f}% |")
    return "\n".join(lines)

print(compare_systems(baseline_scores, advanced_scores))

Explicación: este reporte es exactamente lo que pones en el README final. La columna "Mejora" cuenta la historia que defiende todo el trabajo de la guía.


Resumen

  • RAGAS core: faithfulness, answer relevancy, context precision, context recall
  • Cada métrica usa LLM-as-judge con temperature=0 y seed fijo para reproducibilidad
  • Faithfulness = grounding (¿la respuesta está en el contexto?)
  • Answer Relevancy = utilidad (¿contesta la query?)
  • Context Precision = orden (¿lo relevante está al inicio?)
  • Context Recall = cobertura (¿el contexto contiene la respuesta?)
  • La matriz 2×2 (faithfulness × recall) clasifica problemas en 4 cuadrantes con acciones distintas
  • Thresholds convierten métricas en quality gates de release

Recursos adicionales

  1. RAGAS Metrics Documentation - Definiciones formales y ejemplos.
  2. RAG Evaluation Patterns - Pinecone - Casos prácticos.
  3. Lost in the Middle - Stanford - Sesgo posicional en LLMs.
  4. Prompting Guide for Groundedness - Buenas prácticas.
  5. LangSmith Evaluation - Alternativa a RAGAS.

Creado: Marzo 13, 2026
Versión: 2.0