Blogs / Evaluando un Pipeline RAG: Recall@k, MRR, Faithfulness

Evaluando un Pipeline RAG: Recall@k, MRR, Faithfulness

Publicado
26 de septiembre de 2026
Autor
Faizan Nadeem
Etiquetas
RAG AI Engineering Backend Development LLM
Laptop screen displaying performance analytics graphs and data visualizations
Photo by Luke Chesser on Unsplash

La mayoría de los equipos “evalúan” su pipeline RAG haciéndole un puñado de preguntas, leyendo las respuestas y decidiendo si se siente bien. Eso funciona exactamente hasta que deja de funcionar — hasta que un cambio de chunking, un intercambio de modelo de embedding, o un ajuste de prompt empeoran silenciosamente el retrieval mientras las dos o tres preguntas que todos siempre prueban siguen regresando bien. Sin un eval set real, cada cambio al pipeline es una apuesta que en realidad no puedes medir, lo que significa que las regresiones se lanzan silenciosamente y las mejoras no pueden probarse, solo afirmarse.

El problema más profundo de mirar las respuestas a ojo es que confunde dos modos de fallo que necesitan correcciones completamente diferentes. Un pipeline RAG puede producir una respuesta incorrecta porque el retrieval falló (el chunk correcto nunca fue encontrado) o porque la generación falló (el chunk correcto fue encontrado, pero el modelo no lo usó correctamente, o lo usó y aún así alucinó). Leer la respuesta final sola no puede decirte cuál de los dos ocurrió — necesitas métricas que aíslen cada etapa. Este post cubre la construcción de un eval set de tu propio corpus y la medición de ambas mitades por separado: recall@k y MRR para retrieval, faithfulness y answer relevance para generación.

Aprenderás:

  • Por qué leer respuestas finales no puede decirte si un fallo RAG es un problema de retrieval o un problema de generación
  • Cómo construir un eval set real de tu propio corpus en lugar de depender de preguntas de benchmarks genéricos
  • Cómo calcular recall@k y MRR para la etapa de retrieval, con Python ejecutable
  • Cómo medir faithfulness — si la respuesta generada está realmente respaldada por el contexto recuperado
  • Dónde encajan RAGAS y frameworks similares, y qué automatizan versus qué todavía necesita juicio humano
  • Cómo convertir estas métricas en un gate de regresión que realmente ejecutas con cada cambio al pipeline

Tabla de Contenidos

  1. Por qué “Se ve bien” no es una estrategia de evaluación
  2. Separando fallos de retrieval de fallos de generación
  3. Construyendo un eval set de tu propio corpus
  4. Métricas de retrieval: Recall@k y MRR
  5. Context Precision: Penalizando el retrieval ruidoso
  6. Un ejemplo concreto: Cinco consultas a través del harness
  7. Métricas de generación: Faithfulness y Answer Relevance
  8. Dónde encaja RAGAS, y qué todavía necesita un humano
  9. Convirtiendo métricas en un gate de regresión
  10. Errores comunes
  11. Conclusión

Por qué “Se ve bien” no es una estrategia de evaluación

El problema central con el spot-checking manual no es que sea lento — es que no es representativo y no es repetible. Un puñado de preguntas que alguien escribe durante una demo tienden a ser los casos fáciles: bien formulados, cercanos a la propia redacción del documento, sobre el contenido más obvio del corpus. El tráfico de producción no se parece en nada a eso — está mal formulado, pregunta sobre contenido de casos edge, y hace cosas que nadie pensó en probar manualmente. Un pipeline que se ve genial en cinco preguntas de demo puede tener brechas reales de recall en las consultas que los usuarios reales envían.

El segundo problema es que la revisión manual no es composable entre cambios. Si alguien ajusta la estrategia de chunking este mes, el prompt de retrieval el siguiente, y cambia el modelo de embedding el mes después, no hay manera de saber qué cambio ayudó, cuál dañó, y cuál canceló a otro — a menos que el mismo eval set fijo se ejecutara antes y después de cada cambio, con números para comparar, no impresiones.

Separando fallos de retrieval de fallos de generación

La respuesta final de un pipeline RAG es el producto de dos etapas que fallan independientemente, y la corrección para cada una es completamente diferente:

  • Fallo de retrieval: el chunk que contiene la respuesta nunca fue mostrado al LLM en absoluto. Ningún prompt engineering o modelo mejor corrige esto — la respuesta literalmente no está en la ventana de contexto. Este es un problema de chunking, indexación o embedding, cubierto en profundidad en por qué RAG recupera los chunks incorrectos.
  • Fallo de generación: el chunk correcto fue recuperado y estaba presente en el contexto, pero el modelo aún produjo una respuesta incorrecta o sin respaldo — leyó mal el contexto, generalizó más allá de lo que el contexto realmente decía, o ignoró el contenido recuperado en favor de su propio conocimiento previo.

La única manera de distinguirlos sistemáticamente es verificar, para cada consulta de evaluación, si el contenido de la respuesta gold estaba realmente presente en lo que se recuperó antes de juzgar si la respuesta final es correcta. Una respuesta incorrecta con el chunk correcto recuperado es un bug de generación; una respuesta incorrecta con el chunk correcto ausente es un bug de retrieval, y nunca deben agruparse en un único número de “precisión del pipeline”, porque corregir un tipo de problema no hace nada por el otro.

Construyendo un eval set de tu propio corpus

Los datasets de benchmark genéricos (Natural Questions, HotpotQA y similares) miden algo real, pero no lo que necesitas saber: cómo se desempeña tu pipeline en tus documentos, en la formulación de tu dominio, contra los patrones de preguntas reales de tus usuarios. Un eval set construido de tu propio corpus es el único que responde la pregunta que realmente te importa.

Obteniendo preguntas. Las dos fuentes más confiables son consultas de usuarios reales tomadas de logs (si ya tienes tráfico de producción) y preguntas escritas a mano por alguien que conoce el corpus, cubriendo deliberadamente una variedad de estilos de formulación — algunas cercanas a la propia redacción del documento, algunas parafraseadas, algunas usando sinónimos que el documento nunca usa.

Registrando la respuesta gold. Para cada pregunta, registrar dos cosas: el pasaje específico o chunk ID que contiene la respuesta (para scoring de retrieval), y una respuesta de referencia o los hechos clave que debe contener (para scoring de generación).

eval_set = [
    {
        "query": "How long does a webhook retry before giving up?",
        "gold_chunk_ids": ["doc_112#chunk_4"],
        "gold_answer": "Webhooks retry with exponential backoff for 24 hours before being marked failed.",
    },
    # ...
]

Cubriendo deliberadamente categorías propensas a fallos, no solo casos fáciles. Incluir preguntas donde la respuesta abarca dos chunks, preguntas formuladas con sinónimos en lugar de los términos exactos del documento, y preguntas sobre contenido cerca de los límites de sección — estas son las categorías más propensas a exponer debilidades reales de chunking y retrieval, y exactamente las categorías que un spot-check manual rápido tiende a omitir.

Un eval set en el rango de 50-150 preguntas, construido una vez y extendido con el tiempo a medida que nuevas categorías de fallos aparecen en producción, es suficiente para detectar la mayoría de las regresiones — no necesita miles de ejemplos para ser útil, necesita ser representativo y ejecutarse consistentemente.

Métricas de retrieval: Recall@k y MRR

Recall@k responde: entre los top k chunks recuperados, ¿estaba el chunk gold presente en absoluto?

def recall_at_k(retrieved_ids: list[str], gold_ids: set[str], k: int = 5) -> int:
    return int(bool(set(retrieved_ids[:k]) & gold_ids))

def mean_recall_at_k(results: list[dict], k: int = 5) -> float:
    hits = [recall_at_k(r["retrieved_ids"], set(r["gold_chunk_ids"]), k) for r in results]
    return sum(hits) / len(hits)

Mean Reciprocal Rank (MRR) responde una pregunta más precisa: no solo si el chunk gold fue recuperado, sino qué tan alto quedó rankeado. Es el promedio, a través de todas las consultas, de 1 / rango del primer chunk correcto (0 si nunca aparece):

def reciprocal_rank(retrieved_ids: list[str], gold_ids: set[str]) -> float:
    for rank, doc_id in enumerate(retrieved_ids, start=1):
        if doc_id in gold_ids:
            return 1.0 / rank
    return 0.0

def mean_reciprocal_rank(results: list[dict]) -> float:
    scores = [reciprocal_rank(r["retrieved_ids"], set(r["gold_chunk_ids"])) for r in results]
    return sum(scores) / len(scores)

Las dos métricas detectan regresiones diferentes. Recall@k puede mantenerse estable mientras MRR baja silenciosamente — el chunk correcto todavía se está recuperando, solo deslizándose del rango 1 al rango 4, lo que no rompe nada si siempre pasas el top 5 al LLM, pero comenzará a importar en el momento en que ajustes k o agregues un reranker con un corte de top-n más pequeño. Rastrear ambas es cómo notarías un cambio de estrategia de chunking que mantiene el recall estable pero degrada la calidad del ranking — un cambio que un dashboard solo de recall reportaría como un no-evento.

Context Precision: Penalizando el retrieval ruidoso

Recall@k y MRR solo preguntan si el chunk gold apareció y qué tan alto quedó rankeado — ninguna penaliza un conjunto recuperado que es técnicamente correcto pero está relleno con chunks irrelevantes. Context precision cierra esa brecha: de los k chunks recuperados, ¿qué fracción es realmente relevante para la consulta?

def context_precision_at_k(retrieved_ids: list[str], relevant_ids: set[str], k: int = 5) -> float:
    top_k = retrieved_ids[:k]
    hits = sum(1 for doc_id in top_k if doc_id in relevant_ids)
    return hits / k

Esto importa porque un conjunto de retrieval con baja precisión tiene un costo real aguas abajo incluso cuando el recall es perfecto: cada chunk irrelevante en la ventana de contexto es algo que el LLM debe ignorar correctamente, lo cual es exactamente el tipo de ruido que aumenta las probabilidades de un fallo de generación aunque la etapa de retrieval “técnicamente” encontró el chunk correcto. Un pipeline con recall@5 de 100% pero context precision de 0.2 está recuperando el chunk correcto enterrado bajo cuatro irrelevantes — vale la pena señalarlo por separado de un pipeline con el mismo recall pero precision de 0.8, aunque un dashboard de solo recall los mostraría como idénticos.

Un ejemplo concreto: Cinco consultas a través del harness

Para hacer concretos los números, aquí hay un pequeño trace ilustrativo — cinco consultas de un toy eval set ejecutadas a través de las funciones de recall@5, MRR y context precision anteriores, sobre un pipeline de retrieval hipotético:

ConsultaRango del chunk goldRecall@5Rango recíprocoContext precision@5
“How long until a webhook retry gives up?”111.000.8
“What’s the rate limit for the search endpoint?”310.330.6
“Can I rotate an API key without downtime?”no está en top 500.000.4
“Default timeout for a webhook delivery”210.500.8
“How are refunds handled for annual plans?”111.001.0

Promediando: recall@5 = 0.80, MRR = 0.57, mean context precision@5 = 0.72. La brecha entre recall@5 (0.80) y MRR (0.57) es la señal que vale la pena leer con atención aquí — tres de las cuatro recuperaciones exitosas no rankearon el chunk gold primero, lo que significa que un k más pequeño (digamos, top-3 pasado a generación en lugar de top-5) comenzaría a perder consultas que actualmente tienen éxito. Esa es una razón concreta y medida para mantener k en 5 en lugar de ajustarlo solo para ahorrar tokens de ventana de contexto, y es exactamente el tipo de tradeoff que es invisible si solo miras el recall.

Métricas de generación: Faithfulness y Answer Relevance

Las métricas de retrieval te dicen si el material correcto llegó al modelo. Faithfulness te dice si el modelo realmente lo usó correctamente — si cada afirmación en la respuesta generada está realmente respaldada por el contexto recuperado, en lugar de que el modelo llene vacíos con conocimiento exterior o directamente aluciné.

Una manera práctica de calcular faithfulness sin un framework especializado es LLM-como-juez: descomponer la respuesta generada en afirmaciones individuales, luego preguntarle a un modelo juez si cada afirmación está respaldada por el contexto recuperado solo.

FAITHFULNESS_PROMPT = """Context:
{context}

Claim: {claim}

Is this claim directly supported by the context above? Answer only "yes" or "no".
"""

def faithfulness_score(claims: list[str], context: str, judge_fn) -> float:
    supported = [judge_fn(FAITHFULNESS_PROMPT.format(context=context, claim=c)) for c in claims]
    return sum(1 for s in supported if s.strip().lower() == "yes") / len(claims)

Answer relevance es una verificación separada en la otra dirección: ¿la respuesta generada realmente aborda lo que se preguntó, independientemente de si es fiel al contexto? Una respuesta puede ser perfectamente fiel al contexto recuperado y aún así fallar en responder realmente la pregunta — por ejemplo, si fielmente resume un pasaje tangencialmente relacionado en lugar del que responde la pregunta específica. Una versión simple: pedirle a un modelo juez que genere una pregunta plausible a la que la respuesta estaría respondiendo, luego medir su similitud de embedding con la consulta original — baja similitud sugiere que la respuesta se desvió de lo que realmente se preguntó.

Faithfulness y relevance detectan diferentes bugs de generación: baja faithfulness con alta relevance significa que el modelo está respondiendo la pregunta correcta pero rellenando con información que el contexto en realidad no respaldaba; alta faithfulness con baja relevance significa que el modelo está reportando con precisión algo en el contexto que en realidad no era lo que se preguntó.

Dónde encaja RAGAS, y qué todavía necesita un humano

RAGAS (y frameworks de evaluación similares) automatizan el patrón LLM-como-juez anterior en métricas listas para usar — faithfulness, answer relevance, context precision, context recall — sin necesidad de escribir los prompts del juez uno mismo. Eso es genuinamente útil para poner en marcha rápidamente un pipeline de métricas, y es un punto de partida razonable en lugar de algo para construir desde cero el día uno.

Lo que no elimina es la necesidad de un eval set real construido a mano específico para tu corpus — RAGAS puntúa cualquier consulta y datos gold que le des; no genera un eval set significativo de la nada, y un eval set genérico o sintético produce números que son precisos pero no significativos para tus modos de fallo reales. Tampoco elimina la necesidad de revisión humana periódica del propio juez — el scoring LLM-como-juez tiene sus propios modos de fallo (un modelo juez puede ser indulgente, o puede juzgar incorrectamente afirmaciones sutilmente no respaldadas), así que revisar a mano una muestra de los veredictos de faithfulness del juez contra el juicio humano real de forma recurrente es lo que mantiene los números automatizados confiables en lugar de simplemente verse precisos.

También vale la pena ser deliberado sobre qué modelo juez usar. Usar el mismo modelo tanto para generación como para juzgar arriesga un sutil sesgo de auto-preferencia, donde el juez es más indulgente con los outputs que coinciden con sus propios hábitos de formulación. Un modelo juez separado, elegido independientemente — o al menos, consciencia de este sesgo al interpretar scores en el límite — mantiene los números de faithfulness más cerca de una señal honesta que una auto-congratulatoria.

Convirtiendo métricas en un gate de regresión

El eval set solo rinde frutos si se ejecuta consistentemente, no una vez. Un setup mínimo que detecte regresiones antes de que se lancen:

def run_eval(pipeline, eval_set: list[dict]) -> dict:
    retrieval_results = []
    for item in eval_set:
        retrieved_ids = pipeline.retrieve(item["query"])
        retrieval_results.append({"retrieved_ids": retrieved_ids, "gold_chunk_ids": item["gold_chunk_ids"]})

    return {
        "recall_at_5": mean_recall_at_k(retrieval_results, k=5),
        "mrr": mean_reciprocal_rank(retrieval_results),
        # faithfulness/relevance computed similarly over generated answers
    }

baseline = run_eval(pipeline, eval_set)
# after a chunking, embedding, or prompt change:
candidate = run_eval(pipeline_v2, eval_set)

assert candidate["recall_at_5"] >= baseline["recall_at_5"] - 0.02, "recall regression"

Conectar esto a CI, o al menos ejecutarlo manualmente antes de mergear cualquier cambio que afecte el retrieval, convierte “¿este cambio ayuda?” de una suposición a un número. Una tolerancia de dos puntos en recall (en lugar de requerir no-regresión estricta) da cuenta del ruido del eval set en un conjunto de consultas de tamaño modesto — elige un umbral que coincida con el tamaño de tu eval set, ya que un conjunto de 50 consultas tiene más ruido de medición que uno de 500.

Errores comunes

Construir el eval set de documentos que el pipeline nunca verá en producción. Un eval set extraído de un export desactualizado o de un corpus de muestra que no coincide con lo que está realmente indexado reportará números que se ven bien y no significan nada — siempre construye datos gold contra el índice actual y en vivo.

Dejar que el eval set se vuelva obsoleto a medida que el corpus crece. Nuevas categorías de documentos y nuevos patrones de consultas aparecen en producción mucho después de que el eval set fue escrito por primera vez. Trátalo como un artefacto vivo — agrega un puñado de casos cada vez que un fallo real de producción exponga una categoría que el eval set no cubría.

Confiar en un solo score de faithfulness sin hacer spot-checking del juez. El scoring LLM-como-juez es un proxy, no la verdad. Periódicamente muestrea 10-20 casos juzgados y haz que un humano verifique si el veredicto del juez realmente coincide con lo que un lector cuidadoso concluiría — un juez que es silenciosamente demasiado indulgente enmascarará regresiones reales de generación.

Combinar recall y context precision en un solo “score de retrieval”. Un pipeline puede tener recall perfecto y precisión terrible, o al revés, y promediarlos en un solo número oculta cuál realmente necesita arreglarse. Repórtalos siempre por separado.

Solo medir después de que algo se rompe. El valor completo de un harness de evaluación es detectar una regresión antes de que se lance, no diagnosticar una después de que los usuarios se quejen. Conéctalo al mismo paso de revisión que el cambio de código que toca retrieval o generación, no como una herramienta post-mortem.

Conclusión

Un eval set es lo que convierte los cambios al pipeline RAG de apuestas en mediciones. Los números específicos — recall@5, MRR, faithfulness — importan menos que la disciplina de separar la calidad de retrieval de la calidad de generación y medir ambas sobre preguntas construidas de tu propio corpus, no un puñado de consultas de demo fáciles o el dataset de benchmark de otra persona. Una vez que ese harness existe, cada otro post de esta serie — chunking, búsqueda híbrida, reranking — se convierte en algo que puedes validar en lugar de algo en lo que tienes que confiar.

Faizan Nadeem
Autor
Faizan Nadeem

AI/GenAI engineer specialising in agentic RAG, LangGraph, and production LLM systems with Python and FastAPI. 2 years building AI agents, RAG pipelines, and the backends behind them.

Más artículos