Métricas de evaluación de RAG: retrieval, reranking y generación

Traducción automática Este artículo se tradujo automáticamente a partir de la versión original en inglés.

Actualización del artículo

Publicado originalmente el 10 de mayo de 2026. Revisado y actualizado el 6 de septiembre de 2026. La actualización añade benchmarks de retrieval más recientes y nuevos candidatos a reranker, revisa las recomendaciones sobre herramientas de evaluación y corrige los parámetros de API del ejemplo del judge.

Un sistema RAG con filtros de relevancia defectuosos puede funcionar durante meses sin activar ninguna alerta operativa. Sigue devolviendo respuestas y cumple su objetivo de latencia, pero las respuestas se basan en evidencia incompleta. Recall@k frente al conjunto gold original de elementos elegibles pone de manifiesto la pérdida. Los dashboards de latencia y disponibilidad no.

Para los ingenieros que operan o evalúan sistemas RAG multietapa, esta referencia relaciona los fallos de parsing de documentos, filtrado, retrieval, reranking y generación con la métrica que identifica cada uno. También muestra qué comprobaciones se ejecutan antes del release y cuáles monitorizan el tráfico real.

¿Quieres saltar directamente a ejecutar código?

El repositorio ejecutable slavadubrov/rag-evals-demo aplica métricas seleccionadas a SciFact. make eval ejecuta la suite y make benchmark compara configuraciones de chunking, embedding y LLM. Los notebooks 00–09 cubren retrieval, filtrado, generación y ejemplos de sistema; no implementan todas las comprobaciones de esta referencia. La demo usa Qdrant embebido, por lo que no requiere Docker.

El repositorio complementario es un harness educativo. La revisión analizada el 6 de septiembre de 2026 todavía necesita correcciones en la contabilización de queries ausentes, el parsing de resultados del judge y los datos gold de autorización. Su judge pairwise tampoco recibe el contexto proporcionado necesario para evaluar el soporte. Los contratos corregidos y las comprobaciones inline siguientes no parchean ese repositorio; usa sus notebooks para inspeccionar el workflow y verifica estos casos antes de adoptar sus scores como criterios de release.

TL;DR

  • Una stack de evaluación útil cubre ingestion, retrieval, grounding de la generación, conformidad con la ontología y señales del sistema. RAGAS, TruLens, DeepEval, Arize Phoenix y el TREC 2024 RAG Track ofrecen librerías o protocolos públicos de evaluación. No eligen tus métricas por ti.
  • En RAG basado en metadatos y ontologías, una etiqueta incorrecta o un predicado hard frágil pueden reducir el recall a cero. El Recall@k estándar detecta la pérdida cuando conserva el conjunto gold original de elementos elegibles. Una métrica de false-exclusion del filtro identifica la causa. Faithfulness aún puede puntuar las claims frente a un contexto incompleto, pero no puede diagnosticar la causa del filtro o del retrieval. Una refusal vacía puede producir cero afirmaciones y NaN, según la implementación.

Tabla de decisión para evaluar RAG

Usa esta tabla como punto de partida antes de elegir un framework. La métrica adecuada depende del modo de fallo que quieras detectar, no del nombre de la herramienta.

PreguntaFamilia de métricasÚsala cuandoPrecauciones
¿El parsing conservó la fuente?Completitud de extracción, cobertura de tablas/figurasEntran en el corpus PDFs, diapositivas, scans y HTMLUn texto limpio puede haber perdido captions, notas al pie o estructura tabular
¿El retrieval encontró la evidencia correcta?Recall@k, nDCG@k, MRR, precision/recall de contextoPuedes etiquetar chunks o documentos relevantesUn filtro de metadatos hard puede eliminar el documento correcto antes del ranking
¿El reranking mejoró la shortlist?Uplift del reranker, Precision@1, delta de nDCGHay cross-encoders o LLM rankers después del retrievalMide la latencia y el coste junto con la mejora de calidad
¿La respuesta usó la evidencia?Faithfulness, groundedness, soporte de citasLa respuesta cita documentos o extrae hechos del contextoFaithfulness no puede diagnosticar un parsing o retrieval incorrectos
¿El sistema es estable en producción?Drift, regeneración, fallback, latencia p95, coste por respuestaEl tráfico cambia después del lanzamientoLa telemetría de producción necesita revisión humana muestreada para mantenerse calibrada

Para una comparación más breve de herramientas, consulta Mejores herramientas de evaluación de RAG: Ragas, DeepEval y TruLens.

Parte 1: Define el éxito antes de diseñar la arquitectura

Prepara el eval set antes del diagrama de arquitectura. Así cada decisión posterior sobre componentes tendrá un objetivo medible.

No puedes elegir entre BM25 y dense retrieval, chunking recursivo y semántico, o Cohere Rerank y BGE hasta saber qué estás optimizando. «Mejores respuestas» no es una métrica. Un requisito de release ilustrativo sería: «faithfulness ≥ 0,85 en un golden set de 200 queries que cubra nuestras tres intenciones principales, con latencia p95 < 1,5 s y una tasa de false-exclusion del filtro < 2 %». Sus cifras son orientativas; lo importante es que calidad, cobertura, latencia y filtrado tengan umbrales explícitos.

Define el harness antes de escribir el código de retrieval. El primer harness estará equivocado y tendrás que revisarlo. Revisar una métrica es mucho más barato que revisar un sistema que ya has puesto en producción.

Tres capas del pipeline y dos modos de ejecución

La evaluación en producción tiene tres capas de pipeline. La evaluación de ingestion pregunta si el corpus y el índice conservan la fuente. La evaluación en tiempo de query pregunta si el rewriting, el filtrado, el retrieval, el reranking y el ensamblado del contexto encontraron la evidencia correcta. La evaluación de respuestas y producción pregunta si la respuesta usó esa evidencia y si la calidad se mantiene con tráfico real. Si colapsas las capas en un único score, un bug de normalización puede desaparecer dentro de un score de respuesta aceptable.

Los tres puntos en los que un sistema RAG puede perder evidencia: el corpus y el índice, la ruta de retrieval y la respuesta y el tráfico realLos tres puntos en los que un sistema RAG puede perder evidencia: el corpus y el índice, la ruta de retrieval y la respuesta y el tráfico real

Estas capas describen dónde ocurre un fallo. Offline y online describen cuándo se ejecuta la comprobación y contra qué datos. La evaluación offline usa un dataset fijo con ground truth conocido; es reproducible y debe formar parte de la selección de componentes, las comparaciones A/B y las comprobaciones de CI que pueden bloquear un cambio. La evaluación online puntúa tráfico real muestreado y captura regeneraciones, dwell time, feedback explícito y drift real de queries. Es más ruidosa y más difícil de instrumentar.

Usa ambos modos cuando aporten valor: los corpus y conjuntos de queries fijos hacen reproducibles las regresiones; las trazas reales muestreadas revelan problemas de frescura y drift.

Evaluación por componente frente a end-to-end

Hay dos errores habituales. La evaluación exclusivamente end-to-end te dice que el sistema está roto, pero no dónde. La evaluación exclusivamente por componentes puede mostrar que todas las partes pasan mientras el sistema completo sigue fallando. La solución consiste en usar unas pocas métricas end-to-end principales para las decisiones go/no-go y métricas por componente para el diagnóstico. Las métricas de retrieval detectan regresiones del retriever. Las métricas de generación detectan regresiones del generator. La corrección end-to-end de la respuesta detecta fallos de integración.

Frameworks de referencia (recorrido con opinión)

FrameworkLo que hace mejorDónde falla
RAGASUn vocabulario común para faithfulness, answer relevancy y precision/recall de contexto (métricas)Coste del LLM judge; componentes del score opacos al depurar; cambios de versión
ARESUn classifier judge específico de tarea, si el entrenamiento y la anotación justifican el coste (paper); su precision publicada depende del benchmarkConfiguración más pesada; tienes que entrenar los modelos
TruLensFeedback functions vinculadas a trazas e integración con OpenTelemetry (proyecto)Menos métricas RAG específicas listas para usar que RAGAS
DeepEvalIntegración con test runners y métricas personalizadas (proyecto)El uso intensivo de LLM judges provoca picos de coste
Arize PhoenixTracing, experimentos con datasets y evaluadores RAG/agent predefinidos o personalizados (documentación de evaluación)Las rúbricas de dominio y los umbrales del judge aún necesitan calibración local
TREC 2024 RAG TrackBenchmark público para evaluación de nuggets (AutoNuggetizer), evaluación de soporte y fluidez sobre MS MARCO Segment v2.1No es una herramienta de runtime; es un benchmark contra el que calibrarse

Mi stack por defecto es RAGAS para el vocabulario de métricas, DeepEval para las comprobaciones de CI, Phoenix para el tracing en producción y código personalizado para las métricas específicas de la ontología. Elige el framework que facilite la creación de métricas personalizadas.

Para seleccionar benchmarks, ajusta primero la tarea y consulta después la leaderboard. BEIR, MTEB y MIRACL siguen siendo baselines útiles de retrieval. Añade pruebas para las capacidades que no establecen:

  • RAG end-to-end actual: el TREC 2026 RAG Track usa queries narrativas y ClimbMix-400b, en sustitución de MS MARCO v2.1, y enlaza el toolkit de evaluación RAGDoll. El 6 de septiembre, la página del track aún no había anunciado una fecha para devolver resultados y judgments. Sus topics publicados están disponibles para experimentos; no constituyen una leaderboard de 2026 completa y evaluada. Mantén los protocolos de 2024 y 2025 siguientes vinculados a sus propios corpus y judgments.
  • Preguntas técnicas sobre código en evolución: FreshStack combina preguntas de Stack Overflow formuladas por personas, corpus de repositorios y judgments de nuggets. Su snapshot publicado y su maquinaria para construir nuevos corpus son cosas distintas; fija la revisión del repositorio y la fecha de la pregunta.
  • Imágenes que contienen parte de la pregunta o la evidencia: MM-BRIGHT separa retrieval texto-a-texto, multimodal-a-texto, multimodal-a-imagen y multimodal-a-multimodal. Puntúa cada tarea relevante por separado. El texto de OCR por sí solo puede omitir lo que aporta un gráfico o una captura de pantalla.

Estos benchmarks amplían la cobertura; no sustituyen el conjunto de queries elegibles, versionado y propio de tu aplicación.


Parte 2: Mapea los puntos de evaluación

El pipeline RAG agrupado en rutas de ingestion, tiempo de query y respuesta, con métricas de diagnóstico junto a cada etapaEl pipeline RAG agrupado en rutas de ingestion, tiempo de query y respuesta, con métricas de diagnóstico junto a cada etapa

Usa el diagrama para dirigir cada síntoma hacia su primera métrica de diagnóstico. Las pérdidas aguas arriba limitan la calidad aguas abajo: un parsing incorrecto limita el retrieval, y un retrieval incorrecto limita el reranking y la generación. Faithfulness mide la respuesta, nunca la causa aguas arriba.


Parte 3: Evaluación de ingestion

Muchos fallos de RAG en producción empiezan en ingestion. El sistema funciona con documentos de prueba limpios y después falla con PDFs reales, scans, tablas y páginas de corpus desordenadas.

Adquisición y parsing de documentos

Qué medir:

  • Comprobación de plausibilidad de la longitud extraída: extracted_chars / expected_chars por clase de documento detecta cambios de longitud sospechosos, pero un texto duplicado o incorrecto puede seguir obteniendo 1,0. Compara el texto alineado con una referencia limpiada manualmente para detectar omisiones y sustituciones; después comprueba por separado las notas al pie, captions, contenido de tablas y orden de lectura.

  • Precisión del OCR: CER (Character Error Rate) y WER (Word Error Rate), las métricas estándar de speech/OCR:

    CER=S+D+IN,WER=Sw+Dw+IwNw\text{CER} = \frac{S + D + I}{N}, \qquad \text{WER} = \frac{S_w + D_w + I_w}{N_w}

    donde SS, DD y II son sustituciones, eliminaciones e inserciones a nivel de carácter, y NN es el número de caracteres de referencia (con el subíndice ww para la versión por palabras). No apliques el mismo umbral de CER a todo el corpus. Calíbralo por clase de documento y pérdida de respuestas downstream. El texto impreso, la escritura manuscrita y el material multilingüe tienen perfiles de error distintos. Calcula la métrica con jiwer (jiwer.cer(refs, hyps), jiwer.wer(refs, hyps)) o con evaluate de Hugging Face. Para corpus de evaluación, FUNSD y SROIE son benchmarks públicos.

    from jiwer import cer, wer
    
    refs = ["Mars has two moons, Phobos and Deimos."]
    hyps = ["Mars has two m00ns, Phobos and Deirnos."]
    
    print(f"CER = {cer(refs, hyps):.3f}")  # CER = 0.105
    print(f"WER = {wer(refs, hyps):.3f}")  # WER = 0.286
  • Fidelidad de extracción de tablas: TEDS (Tree-Edit-Distance-based Similarity) mide cuánto se aproxima el árbol de una tabla HTML predicha al de referencia, normalizado por el tamaño del árbol mayor. De Zhong et al., 2020 (PubTabNet):

    TEDS(Ta,Tb)=1EditDist(Ta,Tb)max(Ta,Tb)\text{TEDS}(T_a, T_b) = 1 - \frac{\text{EditDist}(T_a, T_b)}{\max(|T_a|, |T_b|)}

    TEDS usa tanto la estructura (filas, columnas y spans) como el contenido de las celdas. TEDS-S elimina el contenido y puntúa únicamente la estructura. Implementación de referencia: teds.py de [PubTabNet] (usa apted internamente). Para corpus de evaluación, consulta PubTabNet, FinTabNet y SciTSR. Los parsers ingenuos suelen fallar con tablas. Haz un benchmark antes de confiar en ellos.

  • Conservación del layout y la estructura: orden de headings, integridad de listas y orden de lectura en PDFs con varias columnas. Usa DocLayNet como benchmark etiquetado. Una comparación lista para usar puede incluir un parser de elementos como unstructured, una librería PDF como pymupdf y un pipeline de Docling seleccionado. Docling ofrece rutas estándar y basadas en VLM; registra cuál evalúas.

Compara familias de parsers distintas, por ejemplo un baseline con Tesseract, un modelo de OCR basado en VLM y el candidato de tu proveedor. Usa una muestra estratificada de clases de documentos reales con un DPI fijo, incluidos scans limpios, fotos, tablas, texto multilingüe, matemáticas y escritura manuscrita. Informa de CER o WER para cada clase y de TEDS para las páginas con tablas.

Limpieza y normalización

  • Precisión de la eliminación de boilerplate: precision/recall frente a spans de boilerplate etiquetados por personas. Una eliminación agresiva descarta contenido relevante; una eliminación laxa contamina los embeddings. Herramientas que comparar: trafilatura, jusText y Resiliparse. Barbaresi (2021) compara Trafilatura con baselines que incluyen jusText; Resiliparse es otro candidato, no un sistema evaluado en ese paper.

  • Normalización Unicode: el porcentaje de documentos que producen salidas NFC y NFKC idénticas (calculado con unicodedata.normalize de la stdlib) es una señal útil de drift de compatibilidad. No detecta code points invisibles o ignorables por defecto ni homógrafos entre scripts: analiza los primeros explícitamente y aplica una política o detector de confusables Unicode cuando los segundos entren en alcance.

  • Precisión de la detección de idioma: F1 en una muestra multilingüe etiquetada. Es crítica para índices multilingües. Usa fasttext-langdetect (el lid.176 de Facebook), lingua-py o cld3. FLORES-200 proporciona textos de evaluación en 200 idiomas, pero la mezcla de idiomas de producción debe determinar el slice de prueba.

  • Eficacia de la deduplicación (MinHash / LSH): precision/recall de tu detector de near-duplicates frente a un conjunto etiquetado manualmente. La idea subyacente es estimar la similitud de Jaccard J(A,B)=ABABJ(A, B) = \frac{|A \cap B|}{|A \cup B|} entre conjuntos de shingles de documentos mediante kk hashes de permutación aleatoria (Broder, 1997) y agrupar near-duplicates con banding de LSH (Indyk y Motwani, 1998). Haz sweep del número de hashes y del umbral de Jaccard en tu corpus. Sigue por separado la tasa de false-merge (que corrompe las respuestas) y la tasa de missed-merge (que desperdicia espacio del índice). datasketch proporciona la implementación usada más abajo; sus parámetros son ilustrativos:

    from datasketch import MinHash, MinHashLSH
    
    def shingles(text: str, k: int = 5) -> set[str]:
        text = text.lower()
        return {text[i:i + k] for i in range(len(text) - k + 1)}
    
    def to_minhash(text: str, num_perm: int = 128) -> MinHash:
        m = MinHash(num_perm=num_perm)
        for s in shingles(text):
            m.update(s.encode("utf-8"))
        return m
    
    docs = {
        "d1": "Mars has two moons, Phobos and Deimos.",
        "d2": "Mars has two moons, Phobos and Deimos!",   # near-dup
        "d3": "Curiosity rover landed on Mars in 2012.",
    }
    
    lsh = MinHashLSH(threshold=0.8, num_perm=128)
    for did, text in docs.items():
        lsh.insert(did, to_minhash(text))
    
    print(sorted(lsh.query(to_minhash(docs["d1"]))))  # ['d1', 'd2']
  • Supresión de PII: precision y recall, calculadas por separado para cada tipo de entidad (emails, SSN, nombres y direcciones). Los errores de recall crean riesgo de compliance; los errores de precision perjudican la calidad de las respuestas. Define el operating point con el equipo legal. Entre las herramientas candidatas están Microsoft Presidio, scrubadub o un modelo NER fine-tuned sobre un conjunto etiquetado.

El chunking controla la calidad del retrieval

El chunking determina qué evidencia llega a la respuesta. En el benchmark de proveedores de NVIDIA de 2025, el chunking a nivel de página obtuvo la mayor precisión media de respuesta end-to-end en la configuración probada, que conservaba tablas y gráficos como unidades completas. Eso mide precisión de respuesta, no recall de retrieval. Considera ese resultado como evidencia para el corpus probado, no como un ganador universal.

El chunking semántico agrupa frases adyacentes según la similitud de sus embeddings y corta en los límites disímiles. SemanticChunker de LangChain y SemanticSplitterNodeParser de LlamaIndex implementan esta estrategia. Puede mejorar el recall frente a ventanas fijas cuando importan los límites temáticos.

RecursiveCharacterTextSplitter de LangChain prueba por defecto dobles saltos de línea, saltos de línea simples, espacios y después caracteres individuales: ["\n\n", "\n", " ", ""]. No detecta límites de frase salvo que configures separadores adecuados o uses un splitter consciente de las frases. Elige valores de ventana y overlap que se ajusten a la estructura de tus documentos y compáralos en el golden set.

Métricas que seguir:

  • Coherencia del chunk: coherence=cos(si,sj)withincos(si,sj)across boundary\text{coherence} = \overline{\cos(s_i, s_j)}_{\text{within}} - \overline{\cos(s_i, s_j)}_{\text{across boundary}}, donde sis_i son embeddings de frases. Los chunks saludables son internamente similares y diferentes en los límites. Calcula la métrica con sentence-transformers y el cosine_similarity de scikit-learn.
  • Calidad de los límites: etiqueta humana de «¿es un corte razonable?» en una muestra, además de una comprobación estructural que verifique que los chunks no dividen tablas, listas o secciones numeradas.
  • Tamaño óptimo del chunk: haz sweep de tamaños en tokens (128, 256, 512, 1024) y representa Recall@k frente al tamaño en tu golden set. Elige el punto de inflexión. No elijas lo que diga el tutorial.
  • Eficacia del overlap: elimina varios porcentajes de overlap y mide Recall@k. Deja de aumentarlo cuando la curva de recall local se aplane o el coste de duplicación supere la ganancia.
  • Fidelidad de atribución del chunk: porcentaje de chunks que conservan un puntero verificable a la fuente (número de página, anchor de sección, doc ID). Esto es necesario para la auditabilidad.
  • Chunking late frente a early: el late chunking (Günther et al., 2024) genera el embedding del documento completo y después lo segmenta, preservando el contexto global (implementación de referencia en jina-embeddings-v3). Contextual Retrieval (Anthropic, 2024) antepone a cada chunk contexto generado por un LLM. Ambos añaden coste. Haz un benchmark en tu corpus antes de adoptar cualquiera.

En mi opinión, el chunking estructural (dividir por headings, tablas y secciones —implementado por parsers como unstructured.io o recorriendo el AST que ya produce tu parser) está infrautilizado. Si tus documentos tienen estructura, úsala antes de añadir heurísticas de similitud. La división recursiva por caracteres es el baseline; el chunking semántico merece el overhead principalmente en prosa no estructurada.

Extracción y enriquecimiento de metadatos

  • Precision/recall/F1 de NER: por tipo de entidad, sobre un subconjunto etiquetado. Estilo CoNLL/MUC estándar. Calcula la métrica con seqeval (from seqeval.metrics import f1_score) para la versión consciente de tags BIO/IOB, o con scikit-learn para comparar conjuntos de spans. CoNLL-2003 y OntoNotes 5.0 son los corpus de referencia canónicos.
  • F1 de extracción de relaciones: aún más importante en sistemas basados en ontologías. Etiqueta manualmente un conjunto estratificado por tipo de relación y clase de documento. TACRED y DocRED son benchmarks públicos; entre las implementaciones candidatas están opennre y los pipelines de relaciones de spaCy.
  • Precisión de extracción de títulos y headings: exact-match más similitud de Levenshtein normalizada (1edit_dist(a,b)max(a,b)1 - \frac{\text{edit\_dist}(a, b)}{\max(|a|, |b|)}) frente al ground truth; python-Levenshtein o rapidfuzz proporcionan ambas en una sola llamada.
  • Conservación de metadatos jerárquicos: porcentaje de chunks que conservan correctamente su sección padre, documento padre y ruta de ascendencia. Esta es la métrica que determina si tu RAG puede responder a preguntas del tipo «¿qué dice el hijo de la política X?».

Generación de embeddings

  • Benchmarks de selección de modelos: usa los resultados de tareas de retrieval de MTEB, BEIR para generalización zero-shot y MIRACL para retrieval multilingüe como puntos de comparación. MTEB suele informar de nDCG@10 en retrieval; otras familias de tareas usan métricas diferentes. El paquete Python de MTEB ejecuta los benchmarks localmente. Considera la transferencia de MTEB en inglés a un idioma con menos recursos como una hipótesis que debes probar en el conjunto etiquetado de ese idioma.
  • Evaluación específica del dominio: no trates el puesto de un benchmark general como un resultado de dominio. Dimensiona un golden set de dominio a partir de su matriz de cobertura y de la incertidumbre que tu decisión pueda tolerar. Después vuelve a ordenar los modelos candidatos con ranx o pytrec_eval. Un conjunto de dominio puede invertir el orden de una leaderboard, así que publica el slice del dataset, el protocolo de retrieval y el intervalo de confianza junto con el resultado.
  • Detección de drift de embeddings: compara una ventana de referencia fija con embeddings deslizantes mediante MMD o un classifier referencia-versus-actual validado. KL necesita un estimador explícito de distribución de probabilidad y no puede aplicarse directamente a coordenadas brutas de embeddings. Mide también la estabilidad de los nearest neighbors para un conjunto fijo de probes. evidently y alibi-detect implementan detectores basados en modelos y detectores estadísticos. El estudio comparativo de Evidently es una evaluación de proveedor; compara los métodos con shifts conocidos en tus propios embeddings.
  • Multi-vector frente a single-vector: la late interaction conserva representaciones a nivel de token en lugar de colapsar cada documento en un único vector; ColBERT es el diseño canónico, con implementaciones de referencia en RAGatouille y PyLate. Esta representación más rica aumenta el coste del índice y del retrieval. Compara calidad, almacenamiento y latencia con un baseline single-vector sobre el mismo conjunto de dominio antes de adoptarla.

Construcción del índice

  • Recall@k con aproximación: compara el índice approximate-nearest-neighbour (ANN) con un baseline exacto de fuerza bruta usando el mismo k; en FAISS, es IndexHNSWFlat (o IndexIVFFlat) frente a IndexFlatIP/IndexFlatL2. Define la pérdida de recall aceptable a partir de tu presupuesto de calidad downstream. El proyecto ann-benchmarks sigue las curvas de Pareto recall–QPS entre librerías.
  • Ajuste de HNSW: HNSW (Hierarchical Navigable Small World) es un grafo de proximidad por capas; consulta Malkov y Yashunin, 2018. Está implementado en hnswlib, en IndexHNSWFlat de FAISS y en la mayoría de las vector DBs. HNSW expone tres parámetros: M (fan-out del grafo), efConstruction (anchura de candidatos durante la construcción) y efSearch (anchura de candidatos durante la query). Parte de los valores por defecto documentados por la librería y haz sweep de los parámetros hasta que la curva recall–latencia cumpla los requisitos de tu eval set.
  • Ajuste de IVF: IVF (Inverted File index: particiona los vectores con k-means en nlist celdas y, durante la query, explora las nprobe celdas más cercanas; consulta IndexIVFFlat y IndexIVFPQ de FAISS). Haz sweep de nlist y nprobe frente al recall y la latencia de la búsqueda exacta. Evalúa las queries filtradas por separado, porque las familias de índices y las vector DBs implementan el recorrido de filtros de forma distinta.
  • Lag de frescura de las actualizaciones: tiempo desde el commit del documento hasta que está disponible para retrieval. Sigue p50 y p99. En sistemas con requisitos regulatorios, sigue también el porcentaje de queries servidas contra índices obsoletos.

Parte 4: Evaluación en tiempo de query

La ruta de tiempo de query contiene las métricas que diagnostican un camino de retrieval. Recall@k por sí solo no puede mostrar si el fallo lo causaron el rewriting, el filtrado, el reranking o el ensamblado del contexto.

Comprensión y rewriting de queries

  • Calidad de la expansión de queries: uplift de Recall@k en tu golden set, comparando la query expandida con la original. Define antes de probar la ganancia mínima útil y su incertidumbre. Si la expansión no cumple ese requisito, no justifica su latencia y coste. Los baselines clásicos de PRF (pseudo-relevance feedback), como RM3 y Bo1, siguen siendo comprobaciones de plausibilidad útiles; la expansión basada en LLM debe superarlos.
  • Evaluación de HyDE: HyDE (Gao et al., 2022) genera una respuesta hipotética con el LLM, obtiene su embedding y hace retrieval contra ella. Añade latencia de generación y una nueva superficie de fallo. Mide Recall@10 por separado en slices in-domain, out-of-domain y de baja confianza; después decide si pertenece a la ruta por defecto, a un fallback o a ninguna.
  • Generación de multi-query: unión del Recall@k de N rewrites frente a una sola query. Haz sweep de N y elige un punto del frente recall–latencia. Implementaciones: MultiQueryRetriever de LangChain y QueryFusionRetriever de LlamaIndex.
  • Precisión de la clasificación de intención: precision/recall/F1 estándar por intención (calcula con sklearn.metrics.classification_report), pero la métrica operativa es la corrección del routing: ¿se invoca el pipeline downstream correcto?
  • Routing adaptativo: Adaptive-RAG (Jeong et al., NAACL 2024) defiende que no todas las queries merecen la misma estrategia de retrieval. Sigue la precisión del router como problema de clasificación frente a un conjunto etiquetado de «no necesita retrieval / one-shot / iterativa».

La búsqueda iterativa necesita una prueba end-to-end con presupuesto

Un agent puede buscar, inspeccionar un resultado y volver a buscar en lugar de recuperar una lista top-k fija. El dominio de conocimiento actual de tau3 expone retrieval RAG configurable y búsqueda agentic basada en shell, lo que convierte esta posibilidad en una ruta de evaluación concreta y no solo en un esquema de arquitectura. Para una comparación local, proporciona al retrieval one-shot y al iterativo el mismo corpus elegible y límites explícitos de tiempo, tokens del modelo y tool calls. Registra cada query y la evidencia observada en cada turno; puntúa la cobertura final de evidencia, la corrección de la respuesta, el soporte de citas y los fallos por agotamiento del presupuesto. Más llamadas de búsqueda solo son útiles cuando la evidencia añadida mejora la respuesta dentro de esos límites.

Métricas de retrieval

Estas son las métricas baseline. Si no las sigues, no puedes saber si el retrieval está mejorando.

MétricaQué mideCuándo usarla
Recall@kfracción de documentos relevantes de una query devueltos en el top kcuando importa no perder ninguna parte del conjunto relevante
Precision@kporcentaje del top-k que es relevanteútil cuando el cuello de botella es la context window
MRRmedia de 1/rank del primer documento relevantecuando los usuarios solo miran el top-1 o top-3
nDCG@kganancia descontada por posición y ponderada por grados de relevanciamétrica estándar de retrieval con relevancia gradual
MAPmedia entre queries de la average precisioncuando importa toda la lista ordenada
Hit Rate@ksi aparece al menos un documento relevante en el top kpromedia el resultado binario entre queries como comprobación rápida
Coverageporcentaje de documentos gold recuperados alguna vez en todas las queriesdetecta carencias sistemáticas del índice

Las fórmulas, como referencia (relevancia binaria con conjunto relevante RqR_q para la query qq, y reli=1\text{rel}_i = 1 si el documento recuperado en la posición ii-ésima está en RqR_q):

Recall@k=Rq{d1,,dk}Rq,Precision@k=Rq{d1,,dk}k\text{Recall@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{|R_q|}, \quad \text{Precision@k} = \frac{|R_q \cap \{d_1, \dots, d_k\}|}{k} RRq=1rank of first relevant doc,MRR=1QqQRRq\text{RR}_q = \frac{1}{\text{rank of first relevant doc}}, \quad \text{MRR} = \frac{1}{|Q|} \sum_{q \in Q} \text{RR}_q DCG@k=i=1k2reli1log2(i+1),nDCG@k=DCG@kIDCG@k\text{DCG@k} = \sum_{i=1}^{k} \frac{2^{\text{rel}_i} - 1}{\log_2(i + 1)}, \quad \text{nDCG@k} = \frac{\text{DCG@k}}{\text{IDCG@k}}

Para relevancia gradual, reli{0,1,2,}\text{rel}_i \in \{0, 1, 2, \dots\}; el nDCG binario es el caso particular usado en el código siguiente. MAP es la media entre queries de APq=1Rqi:reli=1Precision@i\text{AP}_q = \frac{1}{|R_q|}\sum_{i: \text{rel}_i = 1} \text{Precision@}i. Consulta Manning, Raghavan y Schütze, Introduction to Information Retrieval, capítulo 8, para las derivaciones.

Para código de producción, usa ranx, pytrec_eval o ir_measures; implementan toda la familia de métricas TREC y gestionan correctamente la relevancia gradual. Define los objetivos de release con un golden set realista, la calidad downstream de las respuestas y el coste de un miss. No heredes umbrales de un tutorial.

Aquí, k cuenta IDs de documento únicos. Rechaza los duplicados en lugar de conceder ganancia adicional a un documento repetido. Para experimentos de chunking, declara si k cuenta chunks o documentos padre deduplicados, y compara también la evidencia proporcionada con un presupuesto fijo de tokens. El mismo recall de documentos puede ocultar calidades de contexto muy distintas.

El harness de estas métricas es corto. Puedes ejecutarlo desde un notebook antes incluso de elegir una vector database.

from math import log2
from statistics import mean

# synthetic gold set: query_id -> set of relevant doc ids
gold = {
    "q1": {"d3"},
    "q2": {"d7", "d2"},
    "q3": {"d11"},
    "q4": {"d5"},
}

# ranked retrieval results: query_id -> ranked list of doc ids (top-10)
runs = {
    "q1": ["d8", "d3", "d1", "d4", "d2", "d9", "d6", "d10", "d12", "d13"],
    "q2": ["d2", "d6", "d4", "d7", "d1", "d3", "d8", "d11", "d5", "d9"],
    "q3": ["d11", "d2", "d3", "d4", "d1", "d6", "d7", "d8", "d10", "d12"],
    "q4": ["d1", "d2", "d3", "d6", "d8", "d9", "d10", "d12", "d13", "d14"],
}

def recall_at_k(ranked, gold_set, k):
    if k <= 0 or len(ranked) != len(set(ranked)):
        raise ValueError("require positive k and unique document IDs")
    if not gold_set:
        return 0.0
    hit = sum(1 for d in ranked[:k] if d in gold_set)
    return hit / len(gold_set)

def reciprocal_rank(ranked, gold_set):
    if len(ranked) != len(set(ranked)):
        raise ValueError("require unique document IDs")
    # MRR contribution per query: 1/rank of the first relevant doc.
    for rank, d in enumerate(ranked, start=1):
        if d in gold_set:
            return 1.0 / rank
    return 0.0

def ndcg_at_k(ranked, gold_set, k):
    if k <= 0 or len(ranked) != len(set(ranked)):
        raise ValueError("require positive k and unique document IDs")
    # binary relevance: rel ∈ {0, 1}
    gains = [1.0 if d in gold_set else 0.0 for d in ranked[:k]]
    dcg = sum(g / log2(i + 2) for i, g in enumerate(gains))
    # ideal DCG: all gold docs ranked first, capped by k
    n_gold_in_topk = min(k, len(gold_set))
    idcg = sum(1.0 / log2(i + 2) for i in range(n_gold_in_topk))
    return dcg / idcg if idcg else 0.0

K = 5
print(f"Recall@{K}: {mean(recall_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
print(f"MRR:       {mean(reciprocal_rank(runs.get(q, []), gold[q]) for q in gold):.3f}")
print(f"nDCG@{K}:  {mean(ndcg_at_k(runs.get(q, []), gold[q], K) for q in gold):.3f}")
# Recall@5: 0.750
# MRR:       0.625
# nDCG@5:    0.627
assert recall_at_k([], {"d1"}, 5) == 0.0
for score in (lambda ids: recall_at_k(ids, {"d1"}, 2),
              lambda ids: reciprocal_rank(ids, {"d1"}),
              lambda ids: ndcg_at_k(ids, {"d1"}, 2)):
    try:
        score(["d1", "d1"])
    except ValueError:
        pass
    else:
        raise AssertionError("duplicate IDs must fail")

El aggregate itera sobre todas las queries esperadas y trata una ejecución ausente como vacía. Este ejemplo asigna cero a las queries con gold vacío; en una suite real, márcalas como un slice de answerability separado, con su propio denominador, en lugar de tratar el cero como recall medido. Informa de los timeouts y de las ejecuciones ausentes, además de los scores.

Ejecuta un subconjunto rápido guiado por cobertura en cada PR y el golden set completo antes del release. Bloquea el merge cuando una métrica preregistrada supere su presupuesto de regresión.

El repositorio complementario fija los números exactos anteriores (Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) como test unitario en tests/test_retrieval_metrics.py; el notebook 01 hace sweep de Recall@k / MRR / nDCG sobre un índice SciFact real, y el harness con forma de producción está en evaluation/retrieval.py.

Retrieval híbrido y reciprocal rank fusion

BM25 es un scorer léxico sparse que combina coincidencia exacta de términos, ponderación de términos y normalización por longitud. Está disponible en rank_bm25, Elasticsearch, OpenSearch y la mayoría de motores de búsqueda.

Reciprocal Rank Fusion (Cormack, Clarke y Buettcher, SIGIR 2009) combina rankings de BM25 y dense según su posición. El valor original de k=60 es un baseline útil. RRF no depende de los scores, lo que evita la normalización entre rutas necesaria para la interpolación lineal. Con un conjunto etiquetado suficientemente grande para estimar un delta estable, prueba también una combinación convexa y ajusta α.

Mi hipótesis es que el retrieval híbrido más un reranker cross-encoder puede ayudar con corpus técnicos, de logs y de código. La ganancia puede ser pequeña en corpus muy semánticos. Mide frente a las rutas dense-only y sparse-only, porque una configuración de fusión deficiente puede rendir peor que cualquiera de sus entradas. El notebook de SciFact complementario es una prueba acotada, no un resultado general.

La implementación cabe en unas pocas líneas.

from collections import defaultdict

# two retrieval lanes: dense embeddings and BM25.
dense  = ["d3", "d7", "d1", "d4", "d2", "d9", "d10"]
sparse = ["d2", "d3", "d8", "d1", "d11", "d4", "d6"]

def rrf(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
    """Reciprocal Rank Fusion (Cormack et al., SIGIR 2009).

    score(d) = sum over rankings of 1 / (k + rank(d))
    Score-agnostic: only rank position matters. k=60 is the canonical default.
    """
    scores: dict[str, float] = defaultdict(float)
    if k <= 0:
        raise ValueError("k must be positive")
    for ranking in rankings:
        if len(ranking) != len(set(ranking)):
            raise ValueError("each ranking must contain unique document IDs")
        for rank, doc in enumerate(ranking, start=1):
            scores[doc] += 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda kv: kv[1], reverse=True)

fused = rrf([dense, sparse], k=60)
for doc, score in fused[:5]:
    print(f"{doc}  score={score:.5f}")
# d3  score=0.03252   <- rank 1 dense, rank 2 sparse
# d2  score=0.03178   <- rank 5 dense, rank 1 sparse
# d1  score=0.03150
assert [doc for doc, _ in fused[:3]] == ["d3", "d2", "d1"]
try:
    rrf([["d1", "d1"]])
except ValueError:
    pass
else:
    raise AssertionError("one lane must not vote twice for a document")

Observa lo que RRF no hace: nunca consulta los scores de similitud brutos. Un retriever dense que devuelve cosine 0,98 y una ruta BM25 que devuelve un score de 17,4 no son directamente comparables. La normalización z-score y min-max elimina diferencias de escala afines, pero ninguna calibra un score como relevancia. Los outliers, la forma de la distribución y el conjunto de candidatos siguen afectando a los valores normalizados y, por tanto, al resultado de la fusión. Valida cualquier combinación basada en scores con queries etiquetadas.

RRF solo usa el rank. Si un retriever coloca un documento en la posición 2, ese voto vale 1 / (60 + 2), independientemente del score bruto que lo haya producido.

Hybrid + RRF en SciFact: el notebook 02 compara dense frente a BM25 y RRF con deltas por query. El fuser con forma de producción está en retrieval/hybrid_rrf.py; tests/test_rrf.py fija el orden canónico de d3 / d2 / d1 en k=60.

Reranking

  • ΔnDCG / ΔMRR: uplift frente a no aplicar reranking, sobre tu golden set y a la profundidad que usa realmente tu aplicación. Calcula las métricas de retrieval con y sin reranker sobre conjuntos de candidatos idénticos.
  • Cross-encoder frente a bi-encoder: un bi-encoder obtiene por separado el embedding de la query y del documento (un vector por lado) y puntúa mediante producto escalar; un cross-encoder concatena query y documento y ejecuta un único forward pass que atiende conjuntamente a ambos. Los cross-encoders intercambian un forward pass por candidato por una interacción query–documento más rica. Implementación de referencia: sentence-transformers CrossEncoder. Haz el benchmark de relevancia y latencia indicando hardware, batch size y profundidad de candidatos; no transfieras el resultado de un modelo o servicio gestionado a otro entorno.
  • Listwise frente a pointwise: pointwise puntúa cada par (query, documento) de forma independiente; listwise puntúa conjuntamente toda la lista de candidatos para que el modelo pueda compararlos. Evalúa ambos sobre los mismos conjuntos de candidatos. Calibra cualquier umbral de score por modelo y corpus, en lugar de tratar un ejemplo publicado como transferible.
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")  # Reproducible baseline, not a latest-model ranking

query = "How do I rotate database credentials in production?"
candidates = [
    "Production database credentials are rotated via Vault every 30 days.",
    "The new logo was unveiled at the all-hands meeting.",
    "To rotate prod DB creds, run the `rotate-secrets` GitHub Action.",
]

scores = reranker.predict([(query, c) for c in candidates])
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
for doc, score in ranked:
    print(f"{score:+.3f}  {doc}")

El código anterior de BGE es un baseline pequeño. Para una comparación actual, incluye Cohere Rerank 4.0 fast/pro para ranking de texto multilingüe gestionado, o Qwen3-VL-Reranker 2B/8B cuando las queries o los documentos incluyen imágenes, capturas de pantalla o vídeo. Mantén explícitos las modalidades de la tarea, la profundidad de candidatos, los límites de entrada, las instrucciones y el hardware. Un checkpoint general de generación Qwen3.8 no es el mismo modelo que el reranker especializado Qwen3-VL.

Un reranker suele ayudar a un pipeline RAG básico, pero no garantiza una mejora. Mide su ΔPrecision@1 y ΔnDCG en tu golden set y consérvalo solo si la ganancia supera su presupuesto de latencia y coste. Compara esa ganancia medida con cambios de retrieval más pequeños antes de elegir la siguiente optimización.

ΔnDCG y ΔPrecision@1 de un cross-encoder sobre SciFact: notebook 03; módulo: retrieval/reranker.py.

Construcción del contexto y lost-in-the-middle

Muchos fallos de «buen retrieval, mala respuesta» empiezan al construir el contexto.

  • Relevancia del contexto: ContextRelevance de Ragas evalúa la lista de contexto proporcionada usando dos prompts y ratings normalizados; no es un score por chunk. Para diagnosticar chunks, puntúa explícitamente cada par query–chunk, por ejemplo con un cross-encoder, e informa de la distribución usando un umbral calibrado localmente.
  • Cobertura de citas del contexto proporcionado: chunks proporcionados distintos que reciben citas divididos por chunks distintos proporcionados. Informa por separado de los casos con contexto vacío. Esta proxy observable indica qué chunks recibieron citas, no cuáles usó internamente el modelo ni si las citas respaldan sus claims. Comprueba el soporte de las citas por separado y compara la cobertura con la calidad de la respuesta y el coste en tokens.
  • Detección de lost-in-the-middle: eval sintético en el que colocas el chunk gold en las posiciones {first, middle, last} de un contexto largo y mides la corrección de la respuesta. El estudio citado de Liu et al. (TACL 2024) informa de una degradación en forma de U bajo sus condiciones de contexto largo. Trata el mismo patrón en un modelo actual como una hipótesis que hay que probar. Mitigaciones: haz rerank y después reordena el top-k para que el chunk con mayor score quede primero o último (LongContextReorder de LangChain hace exactamente esto), o comprime agresivamente los chunks centrales. Mide con un eval estratificado por posición, no solo con un score agregado. Hay un eval ejecutable y explicado, estratificado por posición, en el notebook 06 (módulo: evaluation/lost_in_middle.py).
  • Compresión del contexto: informa de la ratio de compresión (tokens de entrada / tokens de salida) junto con la corrección de la respuesta. Entre las herramientas están ContextualCompressionRetriever de LangChain y LongLLMLingua. Define previamente la mayor pérdida de corrección aceptable según el riesgo de la aplicación y el presupuesto de tokens; después rechaza las configuraciones que la superen.

Parte 5: Tasa de false-exclusion del filtro

Esta métrica merece una sección propia porque los scores agregados de retrieval no pueden atribuir un miss a un filtro de relevancia. Evalúa los filtros de elegibilidad por separado frente a los permisos del caller; un documento fuera de ese conjunto debe seguir excluido.

Un predicado de relevancia hard como product = Y AND locale = en-US puede reducir el recall efectivo a cero entre los documentos que el caller puede consultar. Un Recall@k correctamente implementado detecta la pérdida porque su denominador sigue siendo el conjunto original de documentos relevantes elegibles. No indica si el miss lo causó el filtro, el retriever o el ranker. Faithfulness evalúa las claims frente al contexto recuperado. Puede seguir puntuando claims respaldadas por ese contexto incompleto, pero no puede diagnosticar la causa del filtro o del retrieval. Una refusal vacía puede producir cero afirmaciones y NaN, según la implementación; no la trates como evidencia de que faithfulness ha aprobado la refusal.

La rama resaltada es el fallo habitual: el documento correcto existe, pero el filtro lo elimina antes del retrieval. Recall@k registra la caída; solo la tasa de exclusión la atribuye al predicado.

Fallos silenciosos de RAG trazados desde el corpus de origen, pasando por el filtrado, el ranking y la generación, hasta la métrica que identifica cada causaFallos silenciosos de RAG trazados desde el corpus de origen, pasando por el filtrado, el ranking y la generación, hasta la métrica que identifica cada causa

La métrica

filter_false_exclusion_rate =
    (# queries where all gold docs were excluded by metadata filter) /
    (# queries with at least one entitlement-eligible gold doc)

Esta definición a nivel de query cuenta las exclusiones catastróficas: ningún documento relevante elegible sobrevive. Intersecta el conjunto gold de cada query con el conjunto de permisos del caller antes de puntuar; un documento no elegible no es una falsa exclusión. Para queries con varios gold, el Recall@k estándar sigue mostrando la pérdida parcial; añade una tasa de exclusión por documento si ese límite es importante. Para calcular cualquiera de las dos tasas necesitas (a) IDs de documento ground truth para cada query de evaluación y (b) instrumentación que registre los predicados de filtro aplicados, no solo los resultados finales. Define el objetivo a partir del coste de excluir una respuesta válida y del intervalo de confianza de tu muestra de producción.

Aquí tienes una implementación funcional. Compara el recall estándar correcto con un evaluador inválido que redefine la relevancia después del filtrado.

# A small worked example where relevance predicates remove eligible documents.
docs = [
    {"id": "d1", "tenant": "acme",   "locale": "en-US"},
    {"id": "d2", "tenant": "acme",   "locale": "en-GB"},
    {"id": "d3", "tenant": "globex", "locale": "en-US"},
    {"id": "d4", "tenant": "acme",   "locale": "en-US"},
    {"id": "d5", "tenant": "acme",   "locale": "de-DE"},
    {"id": "d6", "tenant": "acme",   "locale": "fr-FR"},
]

# Tenant isolation is an eligibility invariant, not a relevance experiment.
# Verify it independently before evaluating relevance preferences.
eligible_docs = [d for d in docs if d["tenant"] == "acme"]
assert {d["id"] for d in eligible_docs} == {"d1", "d2", "d4", "d5", "d6"}

queries = [
    # the gold doc lives in en-GB but the dynamic filter forced en-US
    {"qid": "q1", "gold": {"d2"}, "filter": lambda d: d["locale"] == "en-US"},
    # the gold doc is correctly within the tenant filter
    {"qid": "q2", "gold": {"d4"}, "filter": lambda d: d["tenant"] == "acme"},
    # the gold doc lives in fr-FR but the dynamic filter forced en-US
    {"qid": "q3", "gold": {"d6"}, "filter": lambda d: d["locale"] == "en-US"},
    # the gold doc passes the filter (de-DE locale match)
    {"qid": "q4", "gold": {"d5"}, "filter": lambda d: d["locale"] == "de-DE"},
]

def filter_false_exclusion_rate(queries, eligible_docs):
    n_with_gold, n_excluded = 0, 0
    eligible_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        eligible_gold = q["gold"] & eligible_ids
        if not eligible_gold:
            continue
        n_with_gold += 1
        survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
        if not (eligible_gold & survivors):
            n_excluded += 1
    if not n_with_gold:
        raise ValueError("false-exclusion rate needs queries with eligible gold")
    return n_excluded / n_with_gold, n_with_gold

rate, eligible_query_count = filter_false_exclusion_rate(queries, eligible_docs)
print(f"filter_false_exclusion_rate = {rate:.2%}")
# filter_false_exclusion_rate = 50.00%
print(f"eligible queries = {eligible_query_count} / {len(queries)}")
# eligible queries = 4 / 4

# Correct Recall@k keeps the original eligible gold set as its denominator.
def standard_recall_at_k(queries, eligible_docs, k=10):
    recalls = []
    eligible_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        eligible_gold = q["gold"] & eligible_ids
        if not eligible_gold:
            continue
        # demo only: the survivor set stands in for a ranked run.
        # A real harness ranks the survivors first, then slices to k.
        survivors = [d for d in eligible_docs if q["filter"](d)][:k]
        survivor_ids = {d["id"] for d in survivors}
        recalls.append(len(eligible_gold & survivor_ids) / len(eligible_gold))
    return sum(recalls) / len(recalls) if recalls else 0.0

print(f"standard recall@10 = {standard_recall_at_k(queries, eligible_docs):.2%}")
# standard recall@10 = 50.00%

# INVALID: rebuilding the gold set after filtering changes the question.
# It drops queries whose relevant documents did not survive, then scores 100%.
def invalid_recall_over_filtered_gold(queries, eligible_docs, k=10):
    recalls = []
    all_doc_ids = {d["id"] for d in eligible_docs}
    for q in queries:
        all_survivors = {d["id"] for d in eligible_docs if q["filter"](d)}
        filtered_gold = q["gold"] & all_doc_ids & all_survivors
        if not filtered_gold:
            continue
        top_k_ids = set(list(all_survivors)[:k])
        recalls.append(len(filtered_gold & top_k_ids) / len(filtered_gold))
    return sum(recalls) / len(recalls) if recalls else 0.0

invalid = invalid_recall_over_filtered_gold(queries, eligible_docs)
print(f"INVALID recall (filtered gold) = {invalid:.2%}")
# INVALID recall (filtered gold) = 100.00%

assert rate == 0.5
assert standard_recall_at_k(queries, eligible_docs) == 0.5
assert invalid == 1.0

# An unauthorized-only gold document belongs in an entitlement test, not this metric.
unauthorized_only = [
    {"qid": "q5", "gold": {"d3"}, "filter": lambda d: d["locale"] == "en-US"},
]
assert eligible_query_count == 4
assert filter_false_exclusion_rate(queries + unauthorized_only, eligible_docs) == (rate, 4)
for unscorable in ([], unauthorized_only):
    try:
        filter_false_exclusion_rate(unscorable, eligible_docs)
    except ValueError:
        pass
    else:
        raise AssertionError("an unmeasured exclusion rate must not pass as zero")

La función devuelve la tasa y el número de queries con gold elegible. Si ese denominador está vacío, lanza una excepción en lugar de informar de un cero tranquilizador; una comprobación de release obligatoria debe tratarlo como evidencia ausente. Las queries cuyo gold es exclusivamente no autorizado permanecen en pruebas de permisos separadas.

La mitad de las queries pierde su documento gold por culpa del filtro, por lo que el Recall@10 correcto cae al 50 %. Ese score detecta el síntoma, pero no puede atribuirlo. La tasa de false-exclusion muestra que el predicado eliminó dos respuestas antes de que se ejecutara el retriever. El evaluador deliberadamente inválido informa del 100 % solo porque descarta esos fallos de su conjunto gold. Ningún modelo puede recuperar un documento que ha sido filtrado.

La tasa del 50 % anterior se reproduce como test unitario en el repositorio complementario: tests/test_filter_exclusion.py::test_50_percent_exclusion_rate. El notebook 04 lo ejecuta sobre SciFact con metadatos sintéticos para que puedas observar cómo un filtro real pone el recall a cero; la métrica de runtime (con su complemento de precision/recall del predicado) está en evaluation/filter_exclusion.py.

Métrica complementaria: precision y recall del predicado

Cuando el filtrado es dinámico —por ejemplo, cuando un LLM extrae predicados de filtro de la query— trata el extractor de predicados como un modelo de clasificación y evalúalo como tal. Mide la precision y el recall de los predicados frente a un conjunto etiquetado de pares (query, correct predicate). La tasa de error de los predicados no se traduce directamente en la misma pérdida puntual de recall de retrieval; mide con qué frecuencia esos errores excluyen un documento gold. Una vez que un filtro hard elimina el documento gold, ningún reranking puede ayudar.

Filtros de elegibilidad frente a preferencias de relevancia

La autorización, el aislamiento de tenants, la jurisdicción legal y el estado de publicación determinan si un documento puede entrar en el conjunto de candidatos. Mantén estos criterios como filtros hard y valídalos de forma independiente; Recall@k, la tasa de false-exclusion y la precision de relevancia no autorizan a relajarlos.

Para una preferencia de relevancia como el locale, la recencia o la versión, compara un predicado hard con un soft boost sobre las mismas queries retenidas y elegibles:

For each relevance preference F:
  hard_recall_F  = retrieval_recall@k with F as a hard filter
  soft_recall_F  = retrieval_recall@k with F as a +0.X rerank boost
  hard_precision = relevant_in_top_k / k under hard filter
  soft_precision = relevant_in_top_k / k under soft boost
  precision_gain = hard_precision - soft_precision
  precision_gain_CI = paired-query uncertainty interval for precision_gain
  exclusion_rate = % of queries where the gold doc was filtered out (hard)

Pre-register Δ_min, ε, and recall_loss_max for this workflow.
Use a hard predicate only if:
  lower_bound(precision_gain_CI) >= Δ_min
  AND exclusion_rate <= ε
  AND hard_recall - soft_recall >= -recall_loss_max.
Otherwise prefer a soft boost.

Elige la ganancia mínima útil de precision, el intervalo de incertidumbre, ε y el límite de pérdida de recall a partir del perjuicio de excluir una respuesta que, por lo demás, sería elegible; el beneficio de una precision mayor y el tamaño de la muestra retenida. Son criterios de release locales, no umbrales universales. Está prevista una publicación específica sobre esta decisión; consulta los follow-ups del final.


Parte 6: Evaluación de la generación

Las métricas de retrieval indican que el sistema podría responder correctamente. No indican que lo haya hecho. Las métricas de generación cubren esa diferencia.

Faithfulness y groundedness

Faithfulness de RAGAS descompone la respuesta en claims atómicas (afirmaciones fácticas breves y autocontenidas) y después verifica cada una frente al contexto recuperado mediante un LLM judge:

faithfulness=claims supported by contexttotal claims\text{faithfulness} = \frac{|\text{claims supported by context}|}{|\text{total claims}|}

Faithfulness comprueba el soporte en la evidencia proporcionada; no demuestra que la evidencia sea correcta o suficiente para responder a la pregunta. Registra por separado las respuestas sin claims e informa de su número en lugar de asignar faithfulness perfecta a una respuesta vacía.

La documentación actual de Ragas recomienda la API de collections siguiente. En un proyecto uv, instala ragas y openai con uv add ragas openai, configura OPENAI_API_KEY y guarda esto como script para ejecutarlo con uv run. Hace llamadas al proveedor y genera sus cargos; el score es el resultado de un judge, no una constante esperada determinista. Fija las dependencias resueltas en el lockfile.

import asyncio

from openai import AsyncOpenAI, omit
from ragas.llms import llm_factory
from ragas.metrics.collections import Faithfulness

async def main():
    async with AsyncOpenAI() as client:
        llm = llm_factory(
            "gpt-5.6-terra", client=client, reasoning_effort="none",
            max_tokens=omit, max_completion_tokens=1024,
            temperature=omit, top_p=omit,
        )
        scorer = Faithfulness(llm=llm)
        result = await scorer.ascore(
            user_input="How many moons does Mars have?",
            response="Mars has two moons, Phobos and Deimos.",
            retrieved_contexts=["Mars has two moons named Phobos and Deimos."],
        )
        print(result.value)

if __name__ == "__main__":
    asyncio.run(main())

Este ejemplo usa GPT-5.6 Terra como candidato actual con structured outputs; la factory de Ragas reenvía los argumentos del modelo. Ragas 0.4.3 no reconoce nombres de generación GPT con puntos en su mapper de límites de tokens. El ejemplo omite explícitamente su max_tokens heredado y los valores por defecto de sampling, y proporciona max_completion_tokens; verifica la request saliente al actualizar el adapter. Desactivar el reasoning hace explícita la configuración, pero no la valida. Compara los false passes, false failures y el coste del judge con etiquetas humanas antes de sustituir un judge calibrado más barato.

A continuación se muestra el mismo loop desplegado con un judge determinista de sustitución para que puedas ver la forma end-to-end.

def extract_claims(answer: str) -> list[str]:
    # Production: an LLM call that decomposes the answer.
    # Demo: split on sentence-final punctuation.
    return [c.strip() for c in answer.replace("?", ".").replace("!", ".").split(".") if c.strip()]

def verify_claim(claim: str, context: str) -> bool:
    # Production: an NLI (natural-language inference) model or LLM judge.
    # Demo: a deterministic stand-in so the example runs offline.
    entailed_pairs = {
        "Mars has two moons": True,
        "Phobos and Deimos orbit Mars": True,
        "Mars has a thick atmosphere": False,  # unsupported by context
        "Curiosity landed in 2012": True,
    }
    for k, v in entailed_pairs.items():
        if k.lower() in claim.lower() or claim.lower() in k.lower():
            return v
    words = [w.lower() for w in claim.split() if len(w) > 3]
    return all(w in context.lower() for w in words) if words else False

context = (
    "Mars has two moons, Phobos and Deimos. NASA's Curiosity rover "
    "landed on Mars in 2012."
)
answer = (
    "Mars has two moons. Phobos and Deimos orbit Mars. "
    "Mars has a thick atmosphere. Curiosity landed in 2012."
)

claims = extract_claims(answer)
verdicts = [(c, verify_claim(c, context)) for c in claims]
faithfulness = sum(1 for _, ok in verdicts if ok) / len(verdicts) if verdicts else None
for c, ok in verdicts:
    print(f"  [{'✓' if ok else '✗'}] {c}")
print(f"faithfulness = {faithfulness:.2f}" if faithfulness is not None else "no_claims")
# faithfulness = 0.75   (one unsupported claim about the atmosphere)
assert faithfulness == 0.75
assert extract_claims("") == []

La estructura importa. En producción, verify_claim se convierte en un modelo NLI o en una llamada a un LLM. Conserva la estructura extract–verify–aggregate, pero valida la extracción y el entailment por separado y registra los casos sin claims o con judgments fallidos. El stand-in offline anterior está hard-codeado para estos ejemplos; no es un detector de factualidad.

Extracción y verificación end-to-end de claims sobre respuestas SciFact generadas: notebook 05; módulo: evaluation/faithfulness.py. El repositorio ejecuta el mismo loop mediante dos familias de judges —el propio modelo del generator y un judge de otra familia (RAG_EVALS_JUDGE_MODEL)— además de un baseline léxico determinista, para que puedas ver dónde discrepan las familias.

Una alternativa específica a LLM-as-judge es HHEM-2.1-Open (Hughes Hallucination Evaluation Model, Vectara), un classifier fine-tuned para detectar alucinaciones. Su model card documenta el checkpoint, el score bruto de 0 a 1 que emite y los resultados de balanced accuracy en AggreFact y RAGTruth. No publica un umbral de decisión por defecto, así que te corresponde elegirlo. Trata esos resultados como evidencia de la model card, no como una garantía para tu corpus: calibra el umbral con etiquetas locales y compáralo con el judge elegido antes del despliegue.

Evaluación de hechos atómicos

FActScore (Min et al., EMNLP 2023) descompone las generaciones largas en hechos atómicos, recupera evidencia para cada hecho, etiqueta cada uno como supported / not-supported e informa de la fracción respaldada:

FActScore=supported atomic factstotal atomic facts\text{FActScore} = \frac{|\text{supported atomic facts}|}{|\text{total atomic facts}|}

Implementación de referencia: shmsw25/FActScore. Funciona bien para biografías, resúmenes y otras salidas largas. Precaución: los hechos triviales repetitivos pueden inflar el score, y afirmaciones verdaderas individualmente pueden formar una respuesta engañosa. MontageLie (EMNLP 2025) prueba esta debilidad mediante relaciones y ordenaciones engañosas entre afirmaciones verdaderas. VeriScore gestiona claims con modificadores necesarios; el filtro Core ayuda a evitar el fact-padding.

Precisión de las citas

Sigue la precision de citas (los spans citados respaldan realmente la claim) y el recall de citas (las claims que deberían citarse se citan):

cite_precision=cited spans that support a claimcited spans,cite_recall=claims with at least one supporting cited spanclaims that should be cited\text{cite\_precision} = \frac{|\text{cited spans that support a claim}|}{|\text{cited spans}|}, \quad \text{cite\_recall} = \frac{|\text{claims with at least one supporting cited span}|}{|\text{claims that should be cited}|}

El TREC 2024 RAG Track define un protocolo reproducible de support evaluation. Thakur et al. (SIGIR 2025) informan de una coincidencia aproximada del 56 % con judgments humanos hechos desde cero y del 72 % bajo otra condición en la que las personas editaban posteriormente las predicciones del LLM. Esta última es anotación asistida, no evidencia independiente de una mejora en la precisión del judge. Mantén la condición de anotación asociada a la cifra. Como aproximación automatizada, ALCE (Gao et al., EMNLP 2023) implementa precision/recall de citas con verificación basada en NLI.

Corrección, completitud y refusal de la respuesta

  • Corrección de la respuesta frente a una referencia: exact match o token-F1 pueden servir para respuestas cortas. Para respuestas largas, comprueba relaciones fácticas, entidades, cantidades, negaciones y la información requerida frente a referencias revisadas. BERTScore y la cosine de embeddings miden similitud; un número o una negación incorrectos pueden conservar un score alto. AnswerCorrectness de Ragas combina comparación factual con similitud, en lugar de equipararlas.
  • Completitud mediante nuggets: un nugget es una unidad de información relevante, con nuggets vitales diferenciados de otros útiles pero opcionales. Una pregunta sobre una fecha de fundación puede exigir el año; el nombre del fundador no es automáticamente obligatorio. AutoNuggetizer construye y refina nuggets a partir de pools de documentos evaluados y después comprueba su presencia en las respuestas generadas. Su informe inicial del TREC 2024 cubría 21 topics y 45 runs. El resumen del TREC 2025, publicado en marzo de 2026, amplía el protocolo a queries narrativas y evalúa la relevancia del retrieval, la completitud de la respuesta y la atribución. Son protocolos públicos de evaluación, no evidencia de que todos los sistemas RAG en producción necesiten la misma rúbrica de nuggets.
  • Comportamiento de refusal: etiqueta si la evidencia proporcionada permite responder y mide las refusals correctas entre todas las refusals y las refusals en casos que deberían abstenerse. NoMIRACL (Findings of EMNLP 2024) prueba la robustez frente a pasajes proporcionados relevantes y no relevantes; no demuestra que el corpus completo carezca de respuesta. Separa los misses de retrieval de las queries genuinamente fuera de alcance en tu propia suite.

Verificación posterior a la generación

Las ganancias de fiabilidad más baratas suelen proceder de post-checks deterministas, no de modelos más grandes.

  • Flag de entidades no observadas: registra las cadenas de entidades con nombre de la respuesta que no aparecen en una cadena de contexto normalizada (por ejemplo, spaCy’s ents más matching exacto). Es un flag barato y específico del dominio para cadenas de entidades no observadas, no una comprobación de grounding: no puede demostrar identidad, relación, negación, tiempo o procedencia. Mide su precision y recall con etiquetas locales antes de usarlo para aprobar un release y conserva el entailment a nivel de claim o la revisión humana para la verificación.

    def unseen_entity_flag(answer: str, context: str, entities: list[str]) -> bool:
        return any(entity.lower() not in context.lower() for entity in entities)
    
    context = "Paris was not founded in 1994."
    answer = "Paris was founded in 1994."
    assert not unseen_entity_flag(answer, context, ["Paris"])
    claim_supported = False  # human label or an NLI verdict, not the lexical flag
    assert not claim_supported
  • Verificación de claims: extrae claims, ejecuta NLI contra el contexto y falla o marca las que estén por debajo del umbral. Modelos NLI como faithfulness: cross-encoder/nli-deberta-v3-large, MoritzLaurer/DeBERTa-v3-large-mnli-fever-anli-ling-wanli. Añade latencia. Merece la pena en dominios de alto riesgo.

  • Self-consistency (Wang et al., ICLR 2023): muestrea varias generaciones con temperature > 0; informa de la tasa de acuerdo (por ejemplo, la proporción de generaciones que coincide con la respuesta modal o el BERTScore por pares); elige el número de muestras a partir de la curva estabilidad–coste y marca para revisión humana las respuestas con bajo acuerdo.

  • Calibración de confianza: recopila la confianza verbalizada («¿Qué confianza tienes, de 0 a 1?») y compárala con la corrección real en el eval set. Representa una curva de calibración e informa del Expected Calibration Error: ECE=m=1MBmnacc(Bm)conf(Bm)\text{ECE} = \sum_{m=1}^{M} \frac{|B_m|}{n} |\text{acc}(B_m) - \text{conf}(B_m)|, donde BmB_m son los bins de confianza. Implementaciones: netcal, torchmetrics.CalibrationError. Un modelo que informa de una confianza de 0,9 debería acertar aproximadamente el 90 % de casos comparables; mide la diferencia en lugar de asumir que está calibrado.


Parte 7: Evaluación de RAG basado en ontologías

Las métricas estándar anteriores cubren el RAG de corpus abierto. Si tu RAG recupera frente a una ontología, taxonomía o knowledge graph estructurado, esas métricas son necesarias, pero no suficientes. Algunos ejemplos son productos de un catálogo, condiciones de SNOMED, componentes de una BOM y técnicas de MITRE ATT&CK. También debes medir la capa de ontología.

Precisión del entity linking

La primera tarea es mapear una mención de una query a una entidad de la ontología («Aspirina» → wikidata:Q18216, «el 737» → aircraft:Boeing_737).

  • Precision/recall/F1 a nivel de mención: estándar, frente a spans de mención gold (calcula con seqeval o un comparador de conjuntos de spans).
  • Precisión de la desambiguación: entre las menciones detectadas correctamente, ¿qué fracción se asigna al ID de entidad correcto? Las referencias públicas incluyen ReFinED, REL y GENRE; benchmarks como AIDA-CoNLL y BELB muestran que los resultados varían según el sistema y el dominio.
  • Gestión de NIL: precision/recall en «entidad no presente en la ontología». Mide por separado el over-linking a entidades cercanas pero incorrectas y la abstención correcta.

Evaluación consciente de la jerarquía

La accuracy plana trata «predecir Sedan cuando la verdad es Hatchback» igual que «predecir Sedan cuando la verdad es Submarine». Esos errores no son equivalentes.

  • Precision/recall/F1 jerárquicas (Kosmopoulos et al., 2015): da crédito a los ancestros compartidos en el DAG de la ontología. Con P^q\hat{P}_q como el nodo predicho más todos sus ancestros y TqT_q como el nodo verdadero más todos sus ancestros:

    hP=qP^qTqqP^q,hR=qP^qTqqTq,hF1=2hPhRhP+hRhP = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |\hat{P}_q|}, \quad hR = \frac{\sum_q |\hat{P}_q \cap T_q|}{\sum_q |T_q|}, \quad hF1 = \frac{2 \cdot hP \cdot hR}{hP + hR}

    Implementa la métrica con networkx sobre el grafo de la ontología: amplía cada predicción y cada etiqueta con sus ancestros y calcula las intersecciones de conjuntos anteriores.

  • Similitud de Wu-Palmer entre la entidad predicha y la gold en la taxonomía (Wu y Palmer, 1994):

    WuP(c1,c2)=2depth(LCA(c1,c2))depth(c1)+depth(c2)\text{WuP}(c_1, c_2) = \frac{2 \cdot \text{depth}(\text{LCA}(c_1, c_2))}{\text{depth}(c_1) + \text{depth}(c_2)}

    donde LCA es el ancestro común más bajo de la taxonomía. Está disponible out of the box en NLTK para WordNet (from nltk.corpus import wordnet as wn; wn.synset("car.n.01").wup_similarity(wn.synset("truck.n.01"))); para taxonomías personalizadas, calcula el LCA con networkx.

  • Tasa de confusión con hermanos/padres: sigue por separado las confusiones con hermanos, padres e hijos: count_sibling / total_errors, count_parent / total_errors, count_descendant / total_errors. Usa ejemplos revisados para comprobar si los errores con hermanos proceden de menciones ambiguas o si los errores con padres proceden de una generalización excesiva.

Tasa de false-exclusion del filtro (de nuevo, ahora crítica)

En sistemas basados en ontologías, los filtros hard suelen proceder de la propia ontología («recupera solo documentos etiquetados con la categoría X»). La métrica de tasa de exclusión (definida en la Parte 5) se convierte en una señal primaria de corrección. Una predicción de categoría incorrecta puede poner el recall a cero; la tasa de exclusión atribuye esa pérdida al filtro.

Conformidad de la generación restringida

Cuando la salida debe ajustarse a una ontología (cada nombre de entidad de la respuesta debe ser un miembro válido de la ontología; cada predicado debe proceder de un vocabulario cerrado), mide:

  • Tasa de validez del schema: porcentaje de salidas que se parsean y validan frente al schema de la ontología. Valida con jsonschema o pydantic. JSONSchemaBench es el benchmark público para structured output general; para schemas específicos de ontología, crea tu propio validator.
  • Conformidad del vocabulario: porcentaje de entidades con nombre de la salida que son IDs válidos de la ontología; es una comprobación de pertenencia a un conjunto frente al vocabulario cerrado.
  • Conformidad semántica: una salida sintácticamente válida aún puede elegir una entidad válida pero incorrecta. Combina la conformidad con la corrección downstream de la respuesta.

Los frameworks de constrained decoding (Outlines, XGrammar, Guidance, OpenAI Structured Outputs) están diseñados para imponer la validez del schema. JSONSchemaBench compara eficiencia, cobertura y calidad entre implementaciones. Vuelve a ejecutar los casos que coincidan con tus schemas y backend de serving, porque la cobertura y la latencia dependen de ambos.

Auditabilidad

Para sistemas basados en ontologías cuyas respuestas están sujetas a revisión:

  • Completitud de citas: porcentaje de claims fácticas con al menos una cita verificable.
  • Profundidad de procedencia: porcentaje de citas que resuelven hasta un documento fuente con un ID estable, no solo hasta un hash de chunk.
  • Tasa de reproducibilidad: al repetir la misma query sobre un snapshot fijo se obtiene la misma respuesta. Fija la versión del modelo, el runtime, la configuración de decoding y la seed; después establece la tasa de repetición requerida según las necesidades de auditabilidad del workflow. Temperature cero por sí sola no garantiza determinismo. Un miss puede proceder de la generación, del runtime de serving o de cualquier etapa anterior.

Parte 8: Evaluación a nivel de sistema

Calidad holística de la respuesta

  • LLM-as-judge (Zheng et al., NeurIPS 2023): enfoque escalable de evaluación basada en modelos. G-Eval (Liu et al., EMNLP 2023) genera pasos de evaluación a partir de la tarea y los criterios, y después pondera los niveles de rating según sus probabilidades de token: score=ip(si)si\text{score} = \sum_i p(s_i)\,s_i. Son probabilidades, no log probabilities. El acuerdo depende del judge, la tarea, el prompt y el conjunto de calibración.
  • Preferencia pairwise: presenta al judge la respuesta A frente a la respuesta B y registra la preferencia. Sustituye el rating absoluto por una decisión comparativa, pero sigue necesitando calibración frente a preferencias humanas. MT-Bench informó de un acuerdo del judge GPT-4 superior al 80 % con las preferencias humanas y con el acuerdo entre personas bajo sus condiciones de benchmark; no traslades esa tasa a otro dominio sin calibración.

LLM-as-judge tiene sesgos reales:

  • Position bias: mide la sensibilidad al orden sobre casos etiquetados por personas para el judge y la tarea seleccionados. La aleatorización o la agregación con el orden intercambiado pueden ayudar a algunos pares modelo/tarea, pero consérvalas solo si mejoran el acuerdo humano local; el estudio controlado de 2026 encontró que intercambiar posiciones perjudicaba en sus casos adversariales.
  • Verbosity bias: los judges pueden confundir longitud con calidad. El estudio controlado de 2026, versión 2 citado encontró un comportamiento heterogéneo en pares de expansión: tres judges preferían respuestas más largas, Claude prefería respuestas concisas y GPT-4o era aproximadamente neutral. Los cinco funcionaron bien en controles de truncation. Estos resultados dependen del benchmark; indica al judge cómo debe tratar la completitud y el relleno, e informa del rendimiento controlado por longitud con tu propia rúbrica.
  • Riesgo de self-preference: Zheng et al. observaron una tasa de victorias propias de GPT-4 un 10 % superior y una tasa de victorias propias de Claude-v1 un 25 % superior en sus datos, pero concluyeron que los datos limitados y las diferencias pequeñas no permitían establecer un sesgo de auto-refuerzo. Compara judges de la misma familia y de familias distintas frente a etiquetas humanas locales; elige el judge mejor calibrado en lugar de asumir que cualquiera de las dos configuraciones es segura.

Receta práctica: selecciona un judge sobre datos de calibración etiquetados por personas, oculta las identidades de los modelos, mide la sensibilidad al orden y especifica la política de longitud en la rúbrica. Repite casos solo cuando las muestras adicionales reduzcan materialmente la incertidumbre. En evaluaciones de alto riesgo, compara judges de la misma familia y de familias distintas, y analiza sus discrepancias frente a etiquetas humanas.

Schema-Guided Reasoning para judges

La salida libre es una fuente de variación en las ejecuciones del judge. Dos ejecuciones sobre la misma respuesta pueden organizar la rúbrica de forma diferente y producir scores distintos. Schema-Guided Reasoning (SGR) hace explícita esa rúbrica: define las etapas de evaluación como un schema de Pydantic y usa constrained output mediante Outlines, XGrammar, structured outputs de vLLM u OpenAI response_format para imponer las restricciones de schema admitidas. El orden de los campos puede ayudar a una persona revisora a inspeccionar el registro, pero no demuestra que el modelo haya razonado siguiendo esas etapas en ese orden.

Para evaluar RAG, el schema descompone el judgment en campos explícitos y auditables, en lugar de permitir que el modelo salte directamente a una cifra:

from pydantic import BaseModel, Field, ValidationError, model_validator
from typing import Literal

class FaithfulnessJudgment(BaseModel):
    extracted_claims: list[str] = Field(
        description="Atomic factual claims in the answer, one per item."
    )
    supported_claims: list[str] = Field(
        description="Subset of extracted_claims that are entailed by the context."
    )
    unsupported_claims: list[str] = Field(
        description="Subset that is NOT entailed by the context."
    )
    outcome: Literal["scored", "no_claims"]
    failure_mode: Literal[
        "none", "fabrication", "overgeneralization", "wrong_entity", "stale_fact"
    ]
    rationale: str

    @model_validator(mode="after")
    def require_claim_partition(self):
        for claims in (self.extracted_claims, self.supported_claims, self.unsupported_claims):
            if any(not claim.strip() for claim in claims):
                raise ValueError("claims must contain non-whitespace text")
        extracted = set(self.extracted_claims)
        supported = set(self.supported_claims)
        unsupported = set(self.unsupported_claims)
        if not extracted:
            if supported or unsupported or self.outcome != "no_claims":
                raise ValueError("an empty extraction must be a no_claims outcome")
            return self
        if (
            len(extracted) != len(self.extracted_claims)
            or len(supported) != len(self.supported_claims)
            or len(unsupported) != len(self.unsupported_claims)
            or supported & unsupported
            or supported | unsupported != extracted
            or self.outcome != "scored"
        ):
            raise ValueError("verdict lists must be a complete, disjoint claim partition")
        return self

    @property
    def score(self) -> float | None:
        if not self.extracted_claims:
            return None
        return len(self.supported_claims) / len(self.extracted_claims)

valid = FaithfulnessJudgment(
    extracted_claims=["Mars has two moons", "Mars has a thick atmosphere"],
    supported_claims=["Mars has two moons"],
    unsupported_claims=["Mars has a thick atmosphere"],
    outcome="scored",
    failure_mode="fabrication",
    rationale="The atmosphere claim lacks support.",
)
assert valid.score == 0.5
empty = FaithfulnessJudgment(
    extracted_claims=[],
    supported_claims=[],
    unsupported_claims=[],
    outcome="no_claims",
    failure_mode="none",
    rationale="Score refusal correctness separately.",
)
assert empty.score is None
try:
    FaithfulnessJudgment(
        extracted_claims=["Mars has two moons"],
        supported_claims=["Mars has two moons"],
        unsupported_claims=["Mars has two moons"],
        outcome="scored",
        failure_mode="none",
        rationale="Contradictory verdict.",
    )
except ValidationError:
    pass
else:
    raise AssertionError("overlapping verdicts must fail validation")

for blank in ("", " ", "\t\n"):
    for verdict_field in ("supported_claims", "unsupported_claims"):
        fields = {"extracted_claims": [blank], "supported_claims": [], "unsupported_claims": []}
        fields[verdict_field] = [blank]
        try:
            FaithfulnessJudgment(
                **fields, outcome="scored", failure_mode="none", rationale="Blank claim."
            )
        except ValidationError:
            pass
        else:
            raise AssertionError("blank claims must fail validation")

Este contrato ilustrativo calcula el score solo después de que las listas de veredictos formen una partición completa y disjunta de las claims extraídas. Un resultado sin claims no tiene score de faithfulness y debe quedar fuera del aggregate de faithfulness; informa de su número y puntúa por separado la corrección de la refusal para que las abstenciones no mejoren silenciosamente la media. Conserva estas comprobaciones semánticas en producción; el constrained output garantiza la forma, no un veredicto imparcial. El modelo de Pydantic también hace visible un cambio de rúbrica como diff de código, mientras que las pruebas de calibración humana evalúan el propio judgment.

Esto sirve para cualquier judge basado en rúbricas, no solo para faithfulness. La preferencia pairwise, el soporte de citas y la corrección de refusals también se benefician del mismo tratamiento.

En el notebook 07 encontrarás un judge simplificado basado en rúbricas, junto con ejemplos pairwise, de position bias y cross-family; módulo: evaluation/llm_judge.py. En la revisión analizada, la función llamada g_eval solicita un único rating entero; no genera pasos de evaluación ni calcula scores ponderados por probabilidad, por lo que no reproduce el protocolo G-Eval. El sweep del benchmark (make benchmark en el repositorio) conecta tres modelos (gpt-5-mini, claude-haiku-4-5, gemini-2.5-flash) en un A/B pairwise con judge rotatorio: cada judge evalúa respuestas de las otras dos familias de modelos. Esa topología permite obtener resultados pairwise cross-family; medir self-preference también requiere condiciones same-family y cross-family comparadas con etiquetas humanas locales.

Latencia y coste

  • p50, p95 y p99 en cada etapa del pipeline. Elige el percentil del SLO y el umbral de alerta a partir del user journey, el volumen de tráfico y el error budget.
  • Time-to-first-token frente al tiempo total de generación. Para una UX de streaming, a los usuarios les importa el TTFT.
  • Desglose por etapa: retrieval, reranking, generación y post-processing. Usa la traza para localizar la cola en lugar de asumir qué etapa la causó; registra el dispositivo del reranker y el batch size al comparar ejecuciones.
  • Total $/query = embedding + retrieval + rerank + generación + almacenamiento amortizado. Sigue p50 y p99; el long tail es donde se va el presupuesto.
  • Tasas de cache hit en los niveles de embedding cache, retrieval cache y KV-cache. Define objetivos separados a partir de la repetición observada, la política de invalidación y el coste evitado en cada capa.

El p50/p95/p99 por etapa con desglose está integrado en el notebook 08 y en el runner de evaluation/latency.py; el informe del benchmark combina latencia con faithfulness en una única matriz que puedes volver a ejecutar con make benchmark.

Pruebas A/B

  • Unidad de aleatorización: elige la unidad a partir del estimand, el carryover y la interferencia. Usa asignación por usuario o sesión cuando la exposición repetida pueda cambiar el comportamiento o crear una UX inconsistente. La asignación por query solo es defendible cuando esos efectos sean insignificantes y el análisis modele observaciones repetidas.
  • Métricas primarias, guardrails y exploratorias: preregístralas. Elige la métrica primaria a partir del resultado de producto; entre las proxies de satisfacción están los thumbs, las regeneraciones y el dwell. Trata la latencia y el coste como guardrails cuando limiten la experiencia.
  • Tamaño de muestra: haz un power analysis antes del lanzamiento a partir del efecto mínimo que merezca la pena detectar, la varianza baseline, la unidad de asignación y la regla de parada.

Parte 9: Construcción del conjunto de pruebas

Una métrica solo es tan buena como el conjunto de pruebas sobre el que se ejecuta. Si tu golden set cubre tres intenciones y el tráfico de producción abarca doce, Recall@10 solo mide esas tres. Peor aún, un conjunto de pruebas que se sobreajuste a preguntas fáciles («¿Cuál es la política de devoluciones de la empresa?») puede aprobar un sistema que falla en las difíciles («¿Qué requisitos de elegibilidad tiene una cancelación parcial en virtud de la Digital Services Act de la UE de 2023, facturada en EUR y originada en Irlanda?»). El score agregado sube mientras el sistema sigue fallando en una parte importante del tráfico de producción.

Las etiquetas de relevancia incompletas pueden distorsionar el recall en ambas direcciones. Si el conjunto relevante real es {a, b}, pero las etiquetas solo contienen {a}, recuperar {a} obtiene 1,0 en lugar de 0,5; recuperar {b} obtiene cero en lugar de 0,5. Versiona los judgments y revisa la evidencia nueva y no evaluada que se haya recuperado antes de interpretar un delta.

Construye primero el conjunto de pruebas alrededor de la distribución real y la dificultad de las queries. Después elige métricas que respondan a los modos de fallo objetivo y ajusta el sistema contra ellas.

Generación sintética de queries

Usa un LLM para generar preguntas a partir de tu corpus:

  • Por chunk: «Genera 3 preguntas que un usuario podría hacer y que este chunk responde».
  • Multi-hop: muestrea dos chunks y genera una pregunta que requiera ambos.
  • Adversarial: genera preguntas con entidades distractoras, phrasing casi duplicado y menciones ambiguas.

La generación de tests de Ragas usa escenarios basados en grafos con queries single-hop y multi-hop y necesidades de información específicas o abstractas. DataMorgana genera benchmarks sintéticos configurables entre categorías de usuario y de pregunta. Los datos sintéticos son útiles para cold starts y pruebas de cobertura. No pueden sustituir a las queries reales de usuarios.

Construcción del golden dataset

Los datos curados por personas anclan el golden set.

  1. Muestrea queries reales de usuarios (o simuladas si estás antes del lanzamiento), estratificadas por intención.
  2. Pide a SMEs que respondan cada pregunta e identifiquen qué documento o documentos contienen la respuesta.
  3. Dimensiona el conjunto a partir de la matriz de cobertura y del intervalo de confianza necesario para las decisiones de release; la cobertura importa más que un número de queries heredado.
  4. Vuelve a curarlo cuando el ritmo de releases, las señales de drift, el riesgo del dominio y la capacidad de anotación lo justifiquen.

Mantén separados las queries de desarrollo, la calibración del judge, la medición de release en un conjunto retenido y las muestras de monitorización. Agrupa los documentos fuente y las sesiones compartidos antes de dividir. Una vez que una query o etiqueta guía el tuning, pasa a ser datos de desarrollo. Reserva casos intactos para validar la configuración y el judge elegidos.

Registra las versiones del corpus, la query, las etiquetas de relevancia y las políticas; la unidad de k; el presupuesto del contexto proporcionado; las revisiones del scorer y del judge; y las reglas de agregación. Compara deltas por query emparejados sobre la misma población, con incertidumbre y recuentos por slice. Incluye todas las queries esperadas: un timeout, un resultado ausente o una respuesta del judge no puntuable deben seguir visibles en la contabilización de completitud y errores. No permitas que un score de release mejore descartando fallos silenciosamente. ARES ofrece un enfoque de investigación que usa judges automatizados, validación humana e inferencia con prediction-powered para estimar el sistema cuando la anotación es escasa; una suite local revisada puede empezar de forma más sencilla.

Conjuntos de pruebas adversariales

  • Contrafactuales: intercambia entidades clave de la query. ¿El sistema recupera los chunks correctos para la query modificada?
  • Distractores: queries en las que el corpus contiene una respuesta plausible pero incorrecta que no debería recuperarse. Esto es lo que pone a prueba RGB (Chen et al., AAAI 2024): robustez frente al ruido, rechazo de negativos, integración de información y robustez contrafactual.
  • Negación y cuantificadores: queries con «no», «excepto» y «solo». Los retrievers dense suelen tener dificultades con estos casos.
  • Fuera de alcance: queries sin respuesta en el corpus. El sistema debería decir «No lo sé», no alucinar. NoMIRACL proporciona pruebas de relevancia y answerability a nivel de pasaje; añade etiquetas separadas de fuera de alcance a nivel de corpus. Evalúa explícitamente la abstención sobre tus tipos de queries de producción.

Cobertura y evaluación continua

  • Construye una matriz de cobertura: intención de query × tipo de documento × rama de la ontología. Una query por celda es un inventario inicial de cobertura, no suficiente potencia estadística para una decisión de release. Las celdas vacías muestran carencias de cobertura; dimensiona los slices poblados para la incertidumbre que puedas tolerar.
  • Ejecuta un subconjunto de regresión acotado y rápido en cada PR, y la suite completa en una cadencia más lenta.
  • Programa la evaluación completa del golden set según la cadencia de releases y el coste de evaluación; ejecútala sobre candidatos de release.
  • Programa la evaluación de drift según el volumen de tráfico, el cambio esperado y el riesgo. Usa una muestra de producción móvil y estratifícala por feedback, en lugar de cambiar silenciosamente la distribución objetivo.

Parte 10: Monitorización en producción

La suite de evaluación que despliegas describe el sistema en el lanzamiento. El tráfico de producción cambia después.

Feedback implícito y explícito

  • Trata los eventos implícitos como señales candidatas, no como KPIs positivos o negativos de calidad, hasta que correlacionen con revisión ciega o feedback explícito en una muestra local.
  • Click-through / open rate de las fuentes citadas (si tu UI las expone).
  • Dwell time sobre la respuesta.
  • Tasa de regeneración: porcentaje de respuestas que el usuario vuelve a preguntar o solicita rehacer. Trátala como una señal de insatisfacción y calibra su relación con conversaciones revisadas.
  • Tasas de copia, compartición y exportación: señales implícitas candidatas que pueden representar utilidad, verificación, handoff o insatisfacción. Mide su asociación y su intervalo de confianza antes de asignarles una dirección.
  • Patrones de follow-up: usa «¿Seguro?» o «¿Y qué pasa con X?» como estratos de revisión y etiqueta su asociación con desconfianza o necesidad no resuelta.
  • Thumbs up/down con categorías de motivo opcionales (incorrecto, incompleto, fuera de tema, dañino, lento). Las ediciones inline pueden conservar más contexto diagnóstico; evalúa ese valor sobre muestras revisadas.

Detección de drift

  • Drift de queries: compara los embeddings de queries con una ventana de referencia usando MMD o un classifier referencia-versus-actual validado. KL requiere un estimador de probabilidad definido, como histogramas elegidos; las coordenadas brutas de embeddings no son probabilidades. Calibra las alarmas con shifts conocidos y después inspecciona los slices afectados.
  • Drift de embeddings: fija una representación y un conjunto de probes, y mide la estabilidad de los vecinos y la calidad del retrieval. Versiones distintas del modelo no tienen por qué compartir dimensiones ni una base de coordenadas, por lo que la cosine entre versiones puede carecer de sentido. Migra conjuntamente los encoders de queries y documentos, evalúa el índice nuevo y conserva snapshots versionados para rollback.
  • Drift de rendimiento: sigue métricas equivalentes a producción (tasa de regeneración por intención) a lo largo del tiempo. Los cambios bruscos y graduales sugieren hipótesis distintas, pero su forma no establece la causa; inspecciona los cambios en datos, tráfico, proveedor, políticas y despliegue.

Evaluación en shadow y human-in-the-loop

Ejecuta el sistema candidato en paralelo con producción, compara las salidas offline y no las sirvas a los usuarios. Esto puede revelar regresiones antes del lanzamiento. La inferencia en shadow sigue consumiendo capacidad y puede invocar herramientas: aísla recursos, suprime las escrituras y comprueba que la comparación no degrade la latencia de producción.

Para la revisión human-in-the-loop (HITL):

  • Envía las salidas de baja confianza a una cola de revisión.
  • Incluye una muestra aleatoria del tráfico de producción para revisión ciega; define la tasa según el volumen de tráfico, el riesgo y la capacidad de los revisores.
  • Sobremuestrea las salidas con thumbs-down para revisarlas junto con la muestra aleatoria.
  • Usa las salidas revisadas para ampliar el golden set.

El conjunto mínimo de guardrails

Elige las prioridades y los umbrales de alerta según el perjuicio para el usuario, los SLO y el rendimiento validado de los detectores. Entre las señales candidatas están:

  1. Score de Faithfulness/HHEM por debajo del umbral en una muestra móvil de producción.
  2. Latencia p95 por encima del SLO.
  3. Tasa de false-exclusion del filtro por encima del umbral (basada en muestras).
  4. Tasa de regeneración fuera de una banda de control calibrada localmente que tenga en cuenta el tamaño de la ventana, el volumen de tráfico, la estacionalidad y el presupuesto de falsas alarmas.
  5. Coste/query por encima del presupuesto.

Usa el momento del release para orientar el diagnóstico y después verifícalo con trazas y slices afectados. Un despliegue puede coincidir con drift del tráfico, y un cambio del proveedor o de los datos puede producirse sin un release de la aplicación. Las alertas son evidencia que investigar; mide su antelación respecto a los informes de los usuarios.


Precauciones

  • Los objetivos son locales, no universales. Cualquier cifra etiquetada como ilustrativa en esta guía es una configuración de ejemplo o un resultado trabajado, no un umbral de release. Calibra los umbrales según tu dominio, el nivel de riesgo, la incertidumbre del conjunto de evaluación y las expectativas de los usuarios.
  • El espacio de frameworks cambia rápido. Las versiones de HHEM, los nombres de métricas de RAGAS, las model cards y el orden de las leaderboards pueden cambiar después de la publicación. Vuelve a comprobar la fuente enlazada y repite el benchmark antes de comprometerte.
  • Las cifras de acuerdo de LLM-as-judge tienen asteriscos. La cifra del 80 % de GPT-4 frente a personas procede de las condiciones de MT-Bench / Chatbot Arena. No demuestra acuerdo en un dominio específico o en un slice adversarial. Usa los judges como multiplicadores de fuerza, no como sustitutos de las comprobaciones puntuales.
  • Los uplifts de benchmarks de proveedores a menudo no se pueden reproducir de forma independiente. Reproduce los resultados con tus propios datos antes de creer una cifra, especialmente con rerankers y sistemas de OCR recientes.
  • Ninguna métrica sustituye a revisar las salidas. Programa revisión ciega de una muestra aleatoria de producción según el tráfico, el riesgo y la capacidad de los revisores. Las métricas escalan ese hábito; no lo sustituyen.

Próximamente en esta serie

Este era el índice. Estos son los follow-ups que estoy planificando:

  • Soft Boosts frente a Hard Filters: análisis detallado de la tasa de false-exclusion del filtro, con código, ejemplos reales de producción y un framework de decisión.
  • El chunking es la variable oculta: experimento controlado con chunking recursivo, semántico, late y estructural sobre tres corpus.
  • Selección de reranker en 2026: BGE frente a Cohere, ZeRank y modelos cross-encoder actuales, comparados directamente en coste, latencia y uplift.
  • RAG basado en ontologías: guía end-to-end: construcción del harness de evaluación completo para un sistema de retrieval basado en entidades.
  • LLM-as-judge sin la trampa del self-preference: recetas prácticas para una evaluación automatizada sin sesgos.
  • Evaluación online en producción: patrones de instrumentación, políticas de alertas y dashboards que detectan regresiones reales.

Referencias

Frameworks y benchmarks

Retrieval y ranking

Generación, faithfulness y judges

Drift y producción

Código complementario

  • slavadubrov/rag-evals-demo — harness ejecutable para las métricas seleccionadas en este artículo sobre el corpus SciFact, además de un sweep de benchmark chunking × embedding × LLM. Incluye los notebooks 00–09, tests unitarios que fijan los ejemplos trabajados anteriores y un índice Qdrant embebido para ejecutarse sin Docker.