Métricas de evaluación de RAG: recuperación, 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.

Esta referencia está pensada para quienes operan o evalúan sistemas de generación aumentada por recuperación (RAG). Relaciona los fallos en el parsing de documentos, el filtrado, la recuperación, el reranking y la generación con la métrica que identifica cada uno. También indica qué comprobaciones se ejecutan antes de lanzar y cuáles vigilan el tráfico real.

Un sistema RAG con filtros de relevancia rotos puede funcionar durante meses sin disparar ninguna alerta operativa. Sigue devolviendo respuestas y cumple su objetivo de latencia, pero esas respuestas se apoyan en evidencia incompleta. El Recall@k calculado sobre el conjunto de referencia original de documentos elegibles revela la pérdida. Los paneles de latencia y disponibilidad, no.

¿Quieres saltar al código y ejecutarlo?

El repositorio ejecutable slavadubrov/rag-evals-demo separa la recuperación en SciFact, la generación sobre QA sintética etiquetada y las comprobaciones offline con datos de prueba fijos. make eval ejecuta recuperación, diagnóstico de filtros y reproducción de resultados de generación tras make index. make benchmark barre ajustes de chunking y embeddings sin llamadas a API; comparar dos modelos de OpenAI requiere una clave de API y la opción --live (véase la parte 8). Los notebooks 00–09 cubren recuperación, filtrado, generación y ejemplos del sistema. La demo usa Qdrant integrado y no requiere Docker.


Tabla de decisión para la evaluación de 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 cuandoTen cuidado con
¿El parsing conservó la fuente?Completitud de la extracción, cobertura de tablas y figurasAl corpus entran PDF, presentaciones, escaneos y páginas HTMLUn texto de aspecto limpio puede haber perdido pies de figura, notas al pie o tablas
¿La recuperación encontró la evidencia?Recall@k, nDCG@k, MRR, precisión y recall del contextoPuedes etiquetar fragmentos o documentos relevantesUn filtro duro de metadatos puede eliminar el documento correcto antes de empezar a ordenar
¿El reranking mejoró la lista corta?Mejora del reranker, Precision@1, diferencia de nDCGHay cross-encoders o rankers LLM después de la recuperaciónMide la latencia y el coste junto con la mejora de calidad
¿La respuesta usó la evidencia?Fidelidad (faithfulness), anclaje (groundedness), respaldo de las citasLa respuesta cita documentos o afirma hechos del contextoLa fidelidad no diagnostica un parsing o una recuperación deficientes
¿El sistema es estable en producción?Deriva, regeneración, fallback, latencia p95, coste por respuestaEl tráfico cambia después del lanzamientoLa telemetría de producción necesita revisión humana por muestreo para seguir calibrada

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

Parte 1: Define el éxito antes de la arquitectura

Redacta el conjunto de evaluación antes del diagrama de arquitectura. Así cada decisión posterior sobre un componente tiene un objetivo medible.

No puedes elegir entre BM25 y recuperación densa, entre chunking recursivo y semántico, o entre Cohere Rerank y BGE hasta que sepas qué optimizas. “Mejores respuestas” no es una métrica. Un requisito de lanzamiento ilustrativo es “fidelidad ≥ 0,85 en un conjunto de oro de 200 consultas que cubra nuestras tres intenciones principales, con latencia p95 < 1,5 s y tasa de falsa exclusión del filtro < 2 %”. Sus cifras son marcadores de posición; lo importante es que la calidad, la cobertura, la latencia y el filtrado tengan umbrales explícitos.

Define el harness antes de escribir el código de recuperación. El primer harness saldrá mal y lo revisarás. Revisar una métrica cuesta mucho menos 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 la ingesta pregunta si el corpus y el índice conservan la fuente. La evaluación en tiempo de consulta pregunta si la reescritura, el filtrado, la recuperación, el reranking y el ensamblado del contexto encontraron la evidencia correcta. La evaluación de la respuesta y de producción pregunta si la respuesta usó esa evidencia y si la calidad se mantiene con tráfico real. Si colapsas las capas en una sola puntuación, un fallo de normalización puede quedar oculto dentro de una puntuación de respuesta aceptable.

Una cuadrícula con tres columnas, ingesta, tiempo de consulta, y respuesta y tráfico, y dos filas, offline y online, que enumera las comprobaciones de cada celda, como TEDS en páginas con tablas offline y el retraso de actualización online para la ingesta, y la tasa de exclusión falsa en ambas filas para el tiempo de consultaUna cuadrícula con tres columnas, ingesta, tiempo de consulta, y respuesta y tráfico, y dos filas, offline y online, que enumera las comprobaciones de cada celda, como TEDS en páginas con tablas offline y el retraso de actualización online para la ingesta, y la tasa de exclusión falsa en ambas filas para el tiempo de consulta

Esas capas describen dónde ocurre un fallo. Offline y online describen cuándo se ejecuta la comprobación y con qué datos. La evaluación offline usa un dataset fijo con verdad de referencia conocida; es reproducible y encaja en 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 una muestra del tráfico real y registra regeneraciones, tiempo de permanencia, comentarios explícitos y la deriva real de las consultas. Es más ruidosa y más difícil de instrumentar.

Usa ambos modos donde ayuden: los corpus y los conjuntos de consultas fijos hacen reproducibles las regresiones; las trazas reales muestreadas revelan fallos de frescura y deriva.

Por componentes frente a end-to-end

Hay dos errores habituales. Evaluar solo end-to-end indica que el sistema falla, pero no dónde. Evaluar solo por componentes puede mostrar todas las piezas en verde mientras el sistema completo sigue fallando. La solución es combinar unas pocas métricas end-to-end principales para decidir si se lanza con métricas por componente para diagnosticar. Las métricas de recuperación detectan regresiones del recuperador. Las métricas de generación detectan regresiones del generador. La corrección de la respuesta end-to-end detecta fallos de integración.

Los frameworks de referencia (recorrido con opinión)

FrameworkLo que mejor haceDónde falla
RAGASUn vocabulario común para fidelidad, relevancia de la respuesta y precisión/recall del contexto (métricas)Coste del LLM como juez; componentes de la puntuación opacos al depurar; cambios de versión
ARESUn juez clasificador específico de la tarea, si compensa el coste de entrenamiento y anotación (artículo); su precisión publicada depende del benchmarkConfiguración más pesada; hay que entrenar modelos de verdad
TruLensFunciones de feedback enlazadas a las trazas e integración con OpenTelemetry (proyecto)Menos métricas específicas de RAG listas para usar que RAGAS
DeepEvalIntegración con ejecutores de pruebas y métricas personalizadas (proyecto)Uso intensivo de LLM como juez = picos de coste
Arize PhoenixTrazado, experimentos con datasets y evaluadores de RAG y de agentes, predefinidos o personalizados (documentación de evaluación)Las rúbricas del dominio y los umbrales del juez siguen necesitando calibración local
TREC 2024 RAG TrackBenchmark público para la evaluación por nuggets (AutoNuggetizer), la evaluación del respaldo y la fluidez sobre MS MARCO Segment v2.1No es una herramienta de ejecución; es un benchmark con el que calibrar

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

Para elegir un benchmark, empieza por la tarea antes de mirar la clasificación. BEIR, MTEB y MIRACL siguen siendo buenas bases de referencia para la recuperación. Añade pruebas para las capacidades que no establecen:

  • RAG end-to-end actual: el TREC 2026 RAG Track usa consultas narrativas y ClimbMix-400b, que sustituye a MS MARCO v2.1, y enlaza el kit de evaluación RAGDoll. Sus temas de 2026 ya están publicados; los juicios, todavía no. Usa los temas para experimentar, no como una clasificación con juicios. Mantén los protocolos de 2024 y 2025 que aparecen más abajo vinculados a sus propios corpus y juicios.
  • Preguntas técnicas sobre código en evolución: FreshStack combina preguntas de Stack Overflow formuladas por personas, corpus de repositorios y juicios por nuggets. La instantánea publicada y la maquinaria para construir corpus nuevos son cosas distintas; fija la revisión del repositorio y la fecha de las preguntas.
  • Imágenes que aportan parte de la pregunta o de la evidencia: MM-BRIGHT separa la recuperación de texto a texto, de multimodal a texto, de multimodal a imagen y de multimodal a multimodal. Puntúa cada tarea relevante por separado. El texto de un 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 consultas elegibles y versionado de tu aplicación.


Parte 2: Mapea los puntos de evaluación

Cuando una respuesta es incorrecta o no tiene respaldo, sigue el documento de referencia por las cuatro comprobaciones de la figura, de izquierda a derecha. La primera etapa en la que falta indica qué métrica revisar primero y qué parte de este artículo la cubre.

Cinco filas siguen un documento de referencia por cuatro comprobaciones: en el índice, tras los filtros, en el top k tras el reranking y usado por la respuesta; cada fila muestra dónde se pierde y la métrica que hay que revisar, y un documento que pasa las cuatro apunta a la generaciónCinco filas siguen un documento de referencia por cuatro comprobaciones: en el índice, tras los filtros, en el top k tras el reranking y usado por la respuesta; cada fila muestra dónde se pierde y la métrica que hay que revisar, y un documento que pasa las cuatro apunta a la generación

Las pérdidas aguas arriba limitan la calidad aguas abajo: un mal parsing limita la recuperación, y una mala recuperación limita el reranking y la generación. La fidelidad mide la respuesta, nunca la causa aguas arriba.


Parte 3: Evaluación de la ingesta

Muchos fallos de RAG en producción empiezan en la ingesta. El sistema funciona con documentos de prueba limpios y luego falla con PDF reales, escaneos, tablas y páginas de corpus desordenadas.

Adquisición y parsing de documentos

Qué medir:

  • Comprobación de coherencia de la longitud extraída: extracted_chars / expected_chars por clase de documento señala cambios de longitud sospechosos, pero un texto duplicado o erróneo aún puede puntuar 1,0. Compara el texto alineado con una referencia limpiada a mano para detectar omisiones y sustituciones, y revisa por separado las notas al pie, los pies de figura, el contenido de las tablas y el orden de lectura.

  • Precisión del OCR: CER (Character Error Rate, tasa de error de caracteres) y WER (Word Error Rate, tasa de error de palabras), las métricas estándar de voz y 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, II son las sustituciones, eliminaciones e inserciones a nivel de carácter y NN es el número de caracteres de la referencia (el subíndice ww indica la versión por palabras). No apliques un único umbral de CER a todos los corpus. Calíbralo por clase de documento y por pérdida de calidad en la respuesta final. El texto impreso, la escritura a mano y el material multilingüe tienen perfiles de error distintos. Calcúlalo con jiwer (jiwer.cer(refs, hyps), jiwer.wer(refs, hyps)) o con evaluate de Hugging Face. Como 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 la extracción de tablas: TEDS (Tree-Edit-Distance-based Similarity, similitud basada en la distancia de edición de árboles) mide cuánto se acerca el árbol de una tabla HTML predicha a la referencia, normalizado por el tamaño del árbol mayor. Procede de Zhong et al., 2020 (PubTabNet):

    TEDS(Ta,Tb)=1−EditDist(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, celdas combinadas) como el contenido de las celdas. TEDS-S elimina el contenido y puntúa solo la estructura. Implementación de referencia: src/metric.py de PubTabNet (usa apted por debajo). Como corpus de evaluación, consulta PubTabNet, FinTabNet y SciTSR. Los parsers ingenuos suelen fallar con las tablas. Haz un benchmark antes de fiarte de ellos.

  • Conservación del diseño y la estructura: orden de los encabezados, integridad de las listas, orden de lectura en PDF de varias columnas. Usa DocLayNet como benchmark etiquetado. Una comparación con herramientas listas para usar puede abarcar un parser de elementos como unstructured, una biblioteca de PDF como pymupdf y un pipeline de Docling concreto. Docling ofrece rutas estándar y rutas con VLM; anota cuál pruebas.

Compara familias de parsers distintas, por ejemplo una línea base con Tesseract, un modelo de OCR basado en VLM y el candidato de tu proveedor. Los mejores modelos de OCR compara las opciones actuales. Usa una muestra estratificada de clases reales de documentos con un DPI fijo, que incluya escaneos limpios, fotos, tablas, texto multilingüe, matemáticas y escritura a mano. Informa del CER o el WER de cada clase y del TEDS de las páginas con tablas.

Limpieza y normalización

  • Precisión de la eliminación de texto repetitivo (boilerplate): precisión y recall frente a tramos de boilerplate etiquetados por personas. Una eliminación agresiva descarta contenido relevante; una eliminación perezosa contamina los embeddings. Herramientas que comparar: trafilatura, jusText, Resiliparse. Barbaresi (2021) compara Trafilatura con líneas base que incluyen jusText; Resiliparse es un candidato aparte, no un sistema evaluado en ese artículo.

  • Normalización Unicode: el porcentaje de documentos cuyas salidas NFC y NFKC coinciden (calculado con unicodedata.normalize de la biblioteca estándar) es una señal útil de deriva de las formas de compatibilidad. No detecta los puntos de código invisibles o ignorables por defecto ni los homógrafos entre escrituras: busca los primeros de forma explícita y aplica una política o un detector de confusables de Unicode cuando los segundos entren en el alcance.

  • Precisión de la detección de idioma: F1 sobre una muestra multilingüe etiquetada. Es fundamental en los índices multilingües. Usa fasttext-langdetect (el lid.176 de Facebook), lingua-py o cld3. FLORES-200 aporta texto de evaluación en 200 idiomas, pero la mezcla de idiomas de tu producción debe determinar el subconjunto de prueba.

  • Eficacia de la deduplicación (MinHash / LSH): precisión y recall de tu detector de casi duplicados frente a un conjunto etiquetado a mano. La idea de fondo: estimar la similitud de Jaccard J(A,B)=∣A∩B∣∣A∪B∣J(A, B) = \frac{|A \cap B|}{|A \cup B|} entre los conjuntos de shingles de los documentos mediante kk hashes de permutaciones aleatorias (Broder, 1997) y agrupar los casi duplicados con bandas LSH (Indyk y Motwani, 1998). Recorre el número de hashes y el umbral de Jaccard sobre tu corpus. Mide por separado la tasa de fusiones erróneas (corrompe las respuestas) y la tasa de fusiones omitidas (desperdicia espacio del índice). datasketch aporta la implementación que se usa a continuación; 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']
  • Depuración de datos personales (PII): precisión y recall, calculados por separado para cada tipo de entidad (correos electrónicos, SSN, nombres, direcciones). Los errores de recall generan riesgo de cumplimiento normativo; los errores de precisión perjudican la calidad de las respuestas. Fija el punto de operación con el equipo jurídico. Entre las herramientas candidatas están Microsoft Presidio, scrubadub o un modelo de NER con fine-tuning sobre un conjunto etiquetado.

El chunking determina la calidad de la recuperación

El chunking cambia qué evidencia llega a la respuesta. En el benchmark de proveedor de NVIDIA de 2025, el chunking por páginas logró la mayor exactitud media de respuesta end-to-end en la configuración probada, porque conservaba las tablas y los gráficos como unidades completas. Eso mide la exactitud de la respuesta, no el recall de la recuperación. Trata ese resultado como evidencia para el corpus probado, no como un ganador universal.

El chunking semántico agrupa frases contiguas por similitud de embeddings y corta donde se vuelven distintas. El SemanticChunker de LangChain y el SemanticSplitterNodeParser de LlamaIndex implementan esta estrategia. Puede mejorar el recall frente a las ventanas fijas cuando importan los límites temáticos.

El RecursiveCharacterTextSplitter de LangChain prueba por defecto saltos de línea dobles, simples, espacios y caracteres individuales: ["\n\n", "\n", " ", ""]. No detecta límites de frases salvo que configures separadores adecuados o uses un divisor que los reconozca. Su valor por defecto length_function=len cuenta caracteres. Para probar 400–512 tokens por fragmento con un solapamiento del 10–20 %, usa una función que cuente tokens. Compara estos ajustes iniciales con otros valores en tu conjunto de referencia.

Métricas que seguir:

  • Coherencia de los fragmentos: coherence=cos⁡(si,sj)‾within−cos⁡(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 fragmentos sanos son similares por dentro y distintos en el límite. Calcúlala con sentence-transformers y la función cosine_similarity de scikit-learn.
  • Calidad de los límites: una muestra etiquetada por personas con la pregunta “¿es un corte razonable?”, más una comprobación estructural de que los fragmentos no parten tablas, listas ni secciones numeradas.
  • Tamaño óptimo del fragmento: recorre varios tamaños en tokens (128, 256, 512, 1024) y representa el Recall@k frente al tamaño sobre tu conjunto de oro. Elige el menor tamaño a partir del cual el Recall@k deja de mejorar.
  • Eficacia del solapamiento: recorre solapamientos de 0, 10, 20 y 30 % como cuadrícula inicial y mide el Recall@k. Deja de aumentar el solapamiento cuando la curva local de recall se aplane o el coste de la duplicación supere la ganancia.
  • Fidelidad de la atribución de fragmentos: porcentaje de fragmentos que conservan una referencia verificable a su origen (número de página, ancla de sección, ID de documento). La auditabilidad lo exige.
  • Chunking tardío frente a temprano: el chunking tardío (late chunking, Günther et al., 2024) genera los embeddings del documento completo y después lo segmenta, con lo que conserva el contexto global (implementación de referencia en jina-embeddings-v3). Contextual Retrieval (Anthropic, 2024) antepone a cada fragmento un contexto generado por un LLM. Ambos añaden coste. Haz un benchmark sobre tu corpus antes de adoptar cualquiera de los dos.

Mi opinión: el chunking estructural (dividir por encabezados, tablas y secciones, que implementan parsers como unstructured.io o que se logra recorriendo el AST que tu parser ya produjo) se usa menos de lo que merece. Si tus documentos tienen estructura, aprovéchala antes de añadir heurísticas de similitud. La división recursiva por caracteres es la línea base; el chunking semántico solo compensa su sobrecoste, sobre todo, con prosa no estructurada.

Extracción y enriquecimiento de metadatos

  • Precisión, recall y F1 de NER: por tipo de entidad, sobre un subconjunto etiquetado. Estilo estándar de CoNLL y MUC. Calcúlalos con seqeval (from seqeval.metrics import f1_score) para la versión que entiende las etiquetas BIO/IOB, o con scikit-learn para comparar conjuntos de tramos. 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 anclados en ontologías. Etiqueta a mano un conjunto estratificado por tipo de relación y por 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 la extracción de títulos y encabezados: coincidencia exacta más similitud de Levenshtein normalizada (1−edit_dist(a,b)max⁡(∣a∣,∣b∣)1 - \frac{\text{edit\_dist}(a, b)}{\max(|a|, |b|)}) frente a la verdad de referencia; python-Levenshtein o rapidfuzz te dan ambas en una sola llamada.
  • Conservación de los metadatos jerárquicos: porcentaje de fragmentos que conservan correctamente su sección padre, su documento padre y su ruta de ascendientes. Esta métrica decide si tu RAG puede responder preguntas del tipo “¿qué dice el hijo de la política X?”.

Generación de embeddings

  • Benchmarks para elegir el modelo: usa los resultados de MTEB en tareas de recuperación, BEIR para la generalización zero-shot y MIRACL para la recuperación multilingüe como puntos de comparación. MTEB suele informar del nDCG@10 en recuperación; otras familias de tareas usan otras métricas. El paquete de Python de MTEB ejecuta los benchmarks en local. Trata la transferencia de MTEB en inglés a un idioma con menos recursos como una hipótesis que probar con el conjunto etiquetado de ese idioma.
  • Evaluación específica del dominio: no tomes la posición en una clasificación general como un resultado de tu dominio. Empieza con unos cientos de consultas etiquetadas (200–500) que cubran tus tipos de consulta principales; la Parte 9 muestra cómo comprobar si eso basta. Después reordena los modelos candidatos sobre ese conjunto con ranx o pytrec_eval. Un conjunto del dominio puede invertir el orden de una clasificación, así que publica junto con el resultado el subconjunto del dataset, el protocolo de recuperación y el intervalo de confianza.
  • Detección de deriva de los embeddings: compara dos colecciones de embeddings: una muestra de referencia fija y una muestra actual móvil. La discrepancia máxima de medias (MMD) comprueba si sus distribuciones difieren. Otra opción es un clasificador validado, entrenado para distinguir las muestras de referencia de las actuales. La divergencia de Kullback–Leibler (KL) compara distribuciones de probabilidad, así que necesita un estimador explícito, como histogramas elegidos; las coordenadas crudas de un embedding no son probabilidades. Mide también la estabilidad de los vecinos más cercanos para un conjunto fijo de sondas. evidently y alibi-detect implementan detectores estadísticos y basados en modelos. El estudio comparativo de Evidently es una evaluación de un proveedor; compara los métodos con desplazamientos conocidos en tus propios embeddings.
  • Multivector frente a vector único: la interacción tardía conserva las representaciones a nivel de token en lugar de colapsar cada documento en un solo vector; ColBERT es el diseño canónico, con implementaciones de referencia en RAGatouille y PyLate. Esa representación más rica aumenta el coste del índice y de la recuperación. Compara calidad, almacenamiento y latencia con una línea base de vector único sobre el mismo conjunto del dominio antes de adoptarla.

Construcción del índice

  • Recall@k bajo aproximación: compara el índice de vecinos más cercanos aproximados (ANN) con una línea base exacta de fuerza bruta con el mismo k; en FAISS, eso es IndexHNSWFlat (o IndexIVFFlat) frente a IndexFlatIP/IndexFlatL2. Como primera comprobación, exige un Recall@10 de al menos 0,95 frente a la búsqueda exacta y luego endurécelo o relájalo según la calidad de la respuesta final. El proyecto ann-benchmarks registra curvas de Pareto de recall frente a QPS para distintas bibliotecas.
  • Ajuste de HNSW: HNSW (Hierarchical Navigable Small World) es un grafo de proximidad por capas; véase Malkov y Yashunin, 2018. Está implementado en hnswlib, en el IndexHNSWFlat de FAISS y en la mayoría de las bases de datos vectoriales. HNSW expone tres parámetros: M (ramificación del grafo), efConstruction (anchura de candidatos al construir) y efSearch (anchura de candidatos al consultar). Valores iniciales: M de 16–32 y efConstruction de 100–200; después sube efSearch hasta que el Recall@10 frente a la búsqueda exacta deje de mejorar. La guía de parámetros de hnswlib indica que un M de 12–48 sirve para la mayoría de los usos y propone una comprobación para efConstruction: si con ef igual a efConstruction el recall de los M vecinos más cercanos queda por debajo de 0,9, el grafo todavía puede mejorar.
  • Ajuste de IVF: IVF (Inverted File index) reparte los vectores en nlist celdas con k-means y, al consultar, recorre las nprobe celdas más cercanas; véanse IndexIVFFlat e IndexIVFPQ de FAISS. Las pautas de índices de FAISS sugieren de 4√N a 16√N celdas para N vectores como nlist inicial. Recorre nlist y nprobe frente al recall de la búsqueda exacta y la latencia. Evalúa las consultas con filtro por separado, porque cada familia de índices y cada base de datos vectorial implementa el recorrido con filtros de forma distinta.
  • Retraso de frescura de las actualizaciones: tiempo desde que se confirma un documento hasta que se puede recuperar. Mide p50 y p99. En los sistemas con requisitos regulatorios, mide también el porcentaje de consultas servidas con índices obsoletos.

Parte 4: Evaluación en tiempo de consulta

El carril de tiempo de consulta contiene las métricas que diagnostican una ruta de recuperación. El Recall@k por sí solo no puede mostrar si la causa del fallo fue la reescritura, el filtrado, el reranking o el ensamblado del contexto.

Comprensión y reescritura de consultas

  • Calidad de la expansión de consultas: mejora del Recall@k sobre tu conjunto de oro, consulta expandida frente a consulta original. Define de antemano la ganancia mínima útil y su incertidumbre, antes de probar. Si la expansión no cumple ese requisito, no justifica su latencia ni su coste. Las líneas base clásicas de PRF (pseudo-relevance feedback, retroalimentación por pseudorrelevancia), como RM3 y Bo1, siguen siendo comprobaciones básicas útiles; la expansión basada en LLM tiene que superarlas.
  • Evaluación de HyDE: HyDE (Gao et al., 2022) genera una respuesta hipotética con el LLM, calcula su embedding y recupera con él. Añade una llamada de generación, y esa llamada puede inventar contenido que desvíe la recuperación del tema. Mide el Recall@10 por separado en los subconjuntos dentro del dominio, fuera del dominio y de baja confianza, y decide después si pertenece a la ruta por defecto, a un fallback o a ninguna de las dos.
  • Generación de varias consultas: Recall@k de la unión de N reformulaciones frente a una sola consulta. Recorre varios valores de N y elige un punto de tu frontera de recall frente a latencia. Implementaciones: el MultiQueryRetriever de LangChain y el QueryFusionRetriever de LlamaIndex.
  • Precisión de la clasificación de intenciones: precisión, recall y F1 estándar por intención (se calculan con sklearn.metrics.classification_report), pero la métrica que importa es la corrección del enrutamiento: ¿se invoca el pipeline correcto aguas abajo?
  • Enrutamiento adaptativo: Adaptive-RAG (Jeong et al., NAACL 2024) defiende que no todas las consultas merecen la misma estrategia de recuperación. Mide la precisión del enrutador como un problema de clasificación sobre un conjunto etiquetado de “no necesita recuperación / una sola pasada / iterativa”.

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

Un agente puede buscar, inspeccionar un resultado y volver a buscar, en lugar de recuperar una única lista fija de los k primeros. El dominio banking_knowledge de tau2-bench cambia la recuperación con el indicador --retrieval-config: sin base de conocimiento, con toda la base de conocimiento en el contexto, con búsqueda BM25 o por embeddings, con una herramienta de shell en un sandbox, o con BM25, búsqueda densa y shell a la vez. Para una comparación local, da a la recuperación de una sola pasada y a la iterativa el mismo corpus elegible y límites explícitos de tiempo, tokens del modelo y tool calls. Registra cada consulta y la evidencia que se vio en cada turno; puntúa la cobertura final de la evidencia, la corrección de la respuesta, el respaldo de las citas y los fallos por presupuesto agotado. Más llamadas de búsqueda solo son útiles cuando la evidencia añadida mejora la respuesta dentro de esos límites.

El módulo retrieval/iterative.py del repositorio complementario ejecuta reformulaciones de consulta suministradas bajo presupuestos explícitos de llamadas, tiempo y tokens. Para los agentes que eligen sus propias búsquedas, Evaluación de agentes de AI: de las trazas a las suites de pruebas explica cómo puntuar la trayectoria además de la respuesta.

Métricas de recuperación

Estas son las métricas base. Si no las mides, no puedes saber si la recuperación está mejorando.

MétricaQué mideCuándo usarla
Recall@kfracción de los documentos relevantes de una consulta que aparecen en los k primeroscuando importa que no falte ninguna parte del conjunto relevante
Precision@kporcentaje de los k primeros que son relevantesútil cuando la ventana de contexto es el cuello de botella
MRRmedia de 1/posición del primer documento relevantecuando los usuarios solo miran el primero o los tres primeros
nDCG@kganancia con descuento por posición, ponderada por grados de relevanciamétrica estándar de recuperación para relevancia graduada
MAPmedia sobre las consultas de la precisión promediocuando importa toda la lista ordenada
Hit Rate@ksi al menos un documento relevante aparece entre los k primerospromedia el resultado binario entre consultas para una métrica rápida de comprobación
Coberturaporcentaje de documentos de oro recuperados alguna vez en todas las consultasdetecta lagunas sistemáticas del índice

Las fórmulas, como referencia (relevancia binaria con el conjunto relevante RqR_q de la consulta qq, y reli=1\text{rel}_i = 1 si el ii-ésimo documento recuperado pertenece a 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=1∣Q∣∑q∈QRRq\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=1k2reli−1log⁡2(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 graduada, reli∈{0,1,2,… }\text{rel}_i \in \{0, 1, 2, \dots\}; el nDCG binario es el caso particular que se usa en el código siguiente. MAP es la media sobre las consultas de APq=1∣Rq∣∑i:reli=1Precision@i\text{AP}_q = \frac{1}{|R_q|}\sum_{i: \text{rel}_i = 1} \text{Precision@}i. Para las derivaciones, véase el capítulo 8 de Manning, Raghavan y Schütze, Introduction to Information Retrieval.

Para el código de producción, usa ranx, pytrec_eval o ir_measures: implementan toda la familia de métricas de TREC y manejan correctamente la relevancia graduada. Como umbrales iniciales de lanzamiento, el repositorio complementario incluye Recall@10 ≥ 0,85 y MRR ≥ 0,6 (en .env.example, que se aplican con --quality-gates). Después ajústalos sobre tu conjunto de oro frente a la calidad de la respuesta final y al coste de un fallo.

Aquí, k cuenta ID de documento únicos, sin repetidos. Rechaza los duplicados en lugar de dar una ganancia adicional a un documento repetido. En los experimentos de chunking, declara si k cuenta fragmentos o documentos padre deduplicados, y compara también la evidencia suministrada dentro de un presupuesto fijo de tokens. Un mismo recall de documentos puede ocultar calidades de contexto muy distintas.

El harness de pruebas para esto es corto. Puedes ejecutarlo desde un notebook antes de haber elegido siquiera una base de datos vectorial.

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
# Duplicate and missing runs are tested in the companion's tests/test_audit_contracts.py.

El agregado recorre todas las consultas esperadas y trata una ejecución ausente como vacía. Este ejemplo asigna cero a las consultas sin evidencia de oro; en una suite real, márcalas como un subconjunto aparte de contestabilidad, con su propio denominador, en lugar de tratar ese cero como un recall medido. Informa de los tiempos de espera agotados y de las ejecuciones ausentes además de las puntuaciones.

Ejecuta un subconjunto rápido en cada PR y el conjunto de oro completo antes del lanzamiento (la Parte 9 da tamaños iniciales). Bloquea una fusión cuando una métrica elegida de antemano supere su presupuesto de regresión.

El repositorio complementario fija las cifras anteriores (Recall@5 = 0.750, MRR = 0.625, nDCG@5 = 0.627) mediante una prueba unitaria en tests/test_retrieval_metrics.py. Notebook 01 ejecuta este ejemplo con k=5 y después muestra Recall@10, MRR y nDCG@10 en un subconjunto de SciFact; no barre k. El harness con estructura de producción está en evaluation/retrieval.py.

Recuperación híbrida y fusión de rangos recíprocos

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

La fusión de rangos recíprocos (Reciprocal Rank Fusion, RRF; Cormack, Clarke y Buettcher, SIGIR 2009) combina los rankings de BM25 y de la búsqueda densa según la posición. El ajuste original k=60 es una línea base útil. RRF no depende de las puntuaciones, lo que evita la normalización entre carriles que exige la interpolación lineal. Si dispones de un conjunto etiquetado lo bastante grande para estimar una diferencia estable, prueba también una combinación convexa y ajusta α.

Mi hipótesis es que la recuperación híbrida junto con un reranker cross-encoder puede ayudar en corpus técnicos, de tipo log y de código. La ganancia puede ser pequeña en corpus muy semánticos. Mide frente a los carriles solo denso y solo disperso, porque una mala configuración de la fusión puede rendir peor que cualquiera de las dos entradas. El notebook de SciFact del repositorio complementario es una prueba acotada, no un resultado general.

El gráfico usa las dos listas ordenadas del código siguiente. Cada carril suma 1/(60 + posición) por cada documento que devuelve, así que los cuatro documentos presentes en ambas listas van en cabeza. d7 ocupa el segundo puesto en la lista densa pero no está en la lista BM25, así que cae al quinto.

Un gráfico de barras apiladas con las puntuaciones RRF fusionadas con k = 60 para diez documentos: d3, d2, d1 y d4 reciben un voto denso y un voto BM25 y puntúan entre 0,031 y 0,033; d7, d8, d11, d9, d10 y d6 reciben un voto y puntúan entre 0,015 y 0,016, con d7 resaltado por ser el segundo en la lista densa pero no aparecer en BM25Un gráfico de barras apiladas con las puntuaciones RRF fusionadas con k = 60 para diez documentos: d3, d2, d1 y d4 reciben un voto denso y un voto BM25 y puntúan entre 0,031 y 0,033; d7, d8, d11, d9, d10 y d6 reciben un voto y puntúan entre 0,015 y 0,016, con d7 resaltado por ser el segundo en la lista densa pero no aparecer en BM25

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"]

Fíjate en lo que RRF no hace: nunca mira las puntuaciones de similitud en bruto. Si un recuperador coloca un documento en la posición 2, ese voto vale 1 / (60 + 2), sea cual sea la puntuación bruta que lo produjo. Un recuperador denso que devuelve un coseno de 0,98 y un carril BM25 que devuelve una puntuación de 17,4 no son directamente comparables. La normalización z-score y la min-max eliminan las diferencias de escala afines, pero ninguna calibra una puntuación como relevancia. Los valores atípicos, 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 puntuaciones con consultas etiquetadas.

Recuperación híbrida + RRF en SciFact: notebook 02 compara recuperación densa, BM25 y RRF mediante Recall@10, MRR y nDCG@10 agregados. El módulo de fusión con estructura de producción está en retrieval/hybrid_rrf.py; tests/test_rrf.py fija el orden canónico d3 / d2 / d1 con k=60.

Reranking

  • ΔnDCG / ΔMRR: mejora sobre no hacer reranking, en tu conjunto de oro, a la profundidad que tu aplicación usa de verdad. Se calcula ejecutando tus métricas de recuperación con y sin el reranker sobre conjuntos de candidatos idénticos.
  • Cross-encoder frente a bi-encoder: un bi-encoder calcula el embedding de la consulta y del documento de forma independiente (un vector por lado) y puntúa con el producto escalar; un cross-encoder concatena consulta y documento y ejecuta una sola pasada que atiende a ambos de forma conjunta. Los cross-encoders cambian una pasada por candidato a cambio de una interacción más rica entre consulta y documento; la figura siguiente muestra la diferencia. Implementación de referencia: CrossEncoder de sentence-transformers. Mide la relevancia y la latencia con un hardware, un tamaño de lote y una profundidad de candidatos concretos; no traslades el resultado de un modelo o servicio gestionado a otro entorno.
  • Listwise frente a pointwise: el enfoque pointwise puntúa cada par (consulta, documento) de forma independiente; el listwise puntúa la lista completa de candidatos de forma conjunta para que el modelo pueda compararlos. Evalúa ambos sobre los mismos conjuntos de candidatos. Calibra cualquier umbral de puntuación por modelo y por corpus en lugar de tratar un ejemplo publicado como si fuera portable.

En la figura, compara las dos franjas de tiempo. El bi-encoder codifica todos los documentos antes de cualquier consulta y almacena los vectores; cuando llega una consulta, codifica solo la consulta. El cross-encoder no tiene nada que calcular antes de la consulta. Lee la consulta y el documento en una sola llamada al modelo, de modo que se ejecuta una vez por candidato y solo sobre la lista corta que devuelve la recuperación.

Dos columnas, bi-encoder y cross-encoder, divididas en antes de cualquier consulta y cuando llega una consulta: el bi-encoder almacena de antemano los vectores de los documentos y, al consultar, codifica solo la consulta y calcula un producto escalar para obtener el top k; el cross-encoder no calcula nada de antemano y hace una llamada por cada par de consulta y documento para puntuar el top kDos columnas, bi-encoder y cross-encoder, divididas en antes de cualquier consulta y cuando llega una consulta: el bi-encoder almacena de antemano los vectores de los documentos y, al consultar, codifica solo la consulta y calcula un producto escalar para obtener el top k; el cross-encoder no calcula nada de antemano y hace una llamada por cada par de consulta y documento para puntuar el top k

El código siguiente puntúa tres candidatos para una consulta con un cross-encoder BGE:

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 BGE anterior es una línea base pequeña. Para una comparación actual, incluye Cohere Rerank 4.0 fast/pro para el ranking gestionado de texto multilingüe, o Qwen3-VL-Reranker 2B/8B cuando las consultas o los documentos incluyan imágenes, capturas de pantalla o vídeo. Mantén explícitas las modalidades de la tarea, la profundidad de candidatos, los límites de entrada, las instrucciones y el hardware.

Un reranker suele ayudar a un pipeline RAG básico, pero no es una victoria garantizada. Mide su ΔPrecision@1 y su ΔnDCG sobre tu conjunto de oro y consérvalo solo si la mejora supera su presupuesto de latencia y de coste. Compara esa mejora medida con la de cambios de recuperación más pequeños antes de elegir la siguiente optimización. BM25, embeddings y rerankers ofrece una guía más breve para elegir cada capa de la pila de ranking.

Notebook 03 muestra P@10 antes y después del reranking con un cross-encoder en SciFact, además de ΔnDCG@10; módulo: retrieval/reranker.py. El módulo evaluation/reranking.py compara rerankers sobre grupos idénticos de candidatos.

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

Muchos fallos de “buena recuperación, mala respuesta” empiezan en la construcción del contexto.

  • Relevancia del contexto: ContextRelevance de Ragas juzga la lista de contexto suministrada con dos prompts y valoraciones normalizadas; no es una puntuación por fragmento. Para diagnosticar fragmentos, puntúa explícitamente cada par consulta–fragmento, por ejemplo con un cross-encoder, e informa de la distribución con un umbral calibrado localmente.
  • Cobertura de citas del contexto suministrado: fragmentos suministrados distintos que se citan, divididos por los fragmentos distintos suministrados. Informa por separado de los casos de contexto vacío. Este indicador observable dice qué fragmentos recibieron citas, no cuáles usó internamente el modelo ni si las citas respaldan sus afirmaciones. Comprueba el respaldo 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: una evaluación sintética en la que colocas el fragmento de oro en las posiciones {primera, media, última} 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 en sus condiciones de contexto largo. Trata el mismo patrón en un modelo actual como una hipótesis que probar. Mitigaciones: aplicar reranking y luego reordenar los k primeros para que el fragmento mejor puntuado quede el primero o el último (el LongContextReorder de LangChain hace exactamente esto), o comprimir con agresividad los fragmentos intermedios. Mide con una evaluación estratificada por posición, no solo con una puntuación agregada. En el notebook 06 hay una evaluación estratificada por posición, completa y ejecutable (módulo: evaluation/lost_in_middle.py).
  • Compresión del contexto: informa del ratio de compresión (tokens de entrada / tokens de salida) junto con la corrección de la respuesta. Entre las herramientas están el ContextualCompressionRetriever de LangChain y LongLLMLingua. Define de antemano la mayor pérdida de corrección aceptable según el riesgo de la aplicación y su presupuesto de tokens, y rechaza las configuraciones que la superen.

Parte 5: Tasa de falsa exclusión del filtro

Esta métrica tiene su propia sección porque las puntuaciones agregadas de recuperación no pueden atribuir un fallo a un filtro de relevancia. Evalúa los filtros de elegibilidad por separado frente a los permisos de quien consulta; un documento fuera de ese conjunto debe seguir excluido.

Un predicado de relevancia duro como product = Y AND locale = en-US puede reducir a cero el recall efectivo entre los documentos que quien consulta tiene permitido ver. Un Recall@k bien implementado detecta la pérdida porque su denominador sigue siendo el conjunto original de documentos relevantes elegibles. No indica si el fallo lo causó el filtro, el recuperador o el ranker. La fidelidad juzga las afirmaciones frente al contexto recuperado. Aun así puede puntuar las afirmaciones que ese contexto incompleto respalda, pero no puede diagnosticar la causa en el filtro o en la recuperación. Una negativa vacía puede no producir ninguna declaración y dar NaN, según la implementación; no la tomes como prueba de que la fidelidad aprobó la negativa.

El diagrama recorre el ejemplo práctico siguiente. La comprobación de permisos elimina d3, que pertenece a otro tenant; esa exclusión es correcta. Después, el filtro de relevancia elimina el documento de referencia de q1 y de q3. El Recall@10 baja al 50% y registra la caída; solo la tasa de exclusión falsa, también del 50%, la atribuye al predicado. Un evaluador que reconstruye el conjunto de referencia tras el filtrado informa del 100%.

Una cuadrícula de cuatro consultas frente a seis documentos: d3 pertenece a otro tenant y la comprobación de permisos lo elimina; el filtro locale = en-US elimina los documentos de referencia d2 de q1 y d6 de q3, mientras que q2 y q4 conservan los suyos; debajo, la tasa de exclusión falsa es del 50%, el Recall@10 es del 50% y un Recall@10 no válido que reconstruye el conjunto de referencia tras el filtrado es del 100%Una cuadrícula de cuatro consultas frente a seis documentos: d3 pertenece a otro tenant y la comprobación de permisos lo elimina; el filtro locale = en-US elimina los documentos de referencia d2 de q1 y d6 de q3, mientras que q2 y q4 conservan los suyos; debajo, la tasa de exclusión falsa es del 50%, el Recall@10 es del 50% y un Recall@10 no válido que reconstruye el conjunto de referencia tras el filtrado es del 100%

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 consulta cuenta las exclusiones catastróficas: no sobrevive ningún documento relevante elegible. Intersecta el conjunto de oro de cada consulta con el conjunto de permisos de quien consulta antes de puntuar; un documento no elegible no es una falsa exclusión. En las consultas con varios documentos de oro, el Recall@k estándar sigue mostrando la pérdida parcial; añade una tasa de exclusión por documento si ese límite te importa. Para calcular cualquiera de las dos tasas necesitas (a) los ID de documento de referencia de cada consulta de evaluación y (b) instrumentación que registre los predicados de filtro aplicados, no solo los resultados finales. Como umbral inicial, el repositorio complementario usa una tasa de falsa exclusión de como máximo el 2 %; ajústala según el coste de excluir una respuesta válida y el intervalo de confianza de tu muestra de producción.

Esta es una implementación que funciona. Compara el recall estándar correcto con un evaluador inválido que redefine la relevancia después de filtrar.

# 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
# Unauthorized gold and an empty denominator are tested in the companion's
# tests/test_audit_contracts.py.

La función devuelve la tasa y el número de consultas con evidencia de oro elegible. Si ese denominador está vacío, lanza una excepción en lugar de informar de un cero tranquilizador; una comprobación de lanzamiento obligatoria debe tratarlo como falta de evidencia. Las consultas con evidencia de oro solo no autorizada se quedan en pruebas de permisos aparte.

La mitad de las consultas pierden su documento de oro por culpa del filtro, así que el Recall@10 correcto cae al 50 %. Esa puntuación detecta el síntoma, pero no puede atribuirlo. La tasa de falsa exclusión muestra que el predicado eliminó dos respuestas antes de que el recuperador se ejecutara. El evaluador deliberadamente inválido informa del 100 % solo porque descarta esos fallos de su conjunto de oro. Ningún modelo puede recuperar un documento que se filtró.

tests/test_filter_exclusion.py::test_50_percent_exclusion_rate del repositorio complementario y el primer ejemplo de notebook 04 usan datos de prueba antiguos de cinco documentos que también dan un 50 %, pero su q3 queda excluida por el aislamiento entre tenants. Esos datos no reproducen el ejemplo anterior con documentos de referencia autorizados. El notebook 04 también ejecuta diagnósticos de filtros en SciFact con metadatos sintéticos. El evaluation/filter_exclusion.py actual acepta un predicado de autorización separado y un conjunto fijo de documentos de referencia autorizados e incluye precisión y exhaustividad del predicado.

Métrica complementaria: precisión y recall del predicado

Cuando el filtrado es dinámico (por ejemplo, un LLM extrae los predicados de filtro de la consulta), trata el extractor de predicados como un modelo de clasificación y evalúalo como tal. Mide la precisión y el recall del predicado frente a un conjunto etiquetado de pares (query, correct predicate). Una tasa de error de predicados no se traslada directamente a la misma pérdida de puntos en el recall de la recuperación; mide con qué frecuencia esos errores excluyen un documento de oro. Cuando un filtro duro elimina el documento de oro, ningún reranking ayuda.

Filtros de elegibilidad frente a preferencias de relevancia

La autorización, el aislamiento entre tenants, la jurisdicción legal y el estado de publicación determinan si un documento puede entrar en el conjunto de candidatos. Mantenlos como filtros duros y valídalos de forma independiente; ni el Recall@k, ni la tasa de falsa exclusión, ni la precisión de relevancia autorizan a relajarlos.

Para una preferencia de relevancia como el idioma regional, la actualidad o la versión, compara un predicado duro con un refuerzo suave (soft boost) sobre las mismas consultas elegibles reservadas para la evaluación. Dos reglas deciden el resultado:

  1. Las reglas de acceso (autorización, tenant, jurisdicción, estado de publicación) son siempre filtros duros.
  2. Cualquier otra preferencia es un refuerzo suave, salvo que supere la prueba medida siguiente. En ese caso puede pasar a ser un predicado duro.

El pseudocódigo da la prueba de la regla 2:

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 precisión, el intervalo de incertidumbre, ε y el límite de pérdida de recall según el daño de excluir una respuesta que de otro modo sería elegible, el beneficio de la precisión añadida y el tamaño de la muestra reservada. Empieza con una ε del 1–2 % y redúcela donde un fallo de respuesta sea costoso.


Parte 6: Evaluación de la generación

Las métricas de recuperación indican que el sistema podría responder bien. No indican que lo hizo. Las métricas de generación cubren esa laguna.

Fidelidad y anclaje

La fidelidad (faithfulness) de RAGAS descompone la respuesta en afirmaciones atómicas (declaraciones factuales breves y autocontenidas) y verifica cada una frente al contexto recuperado mediante un LLM como juez:

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

La fidelidad comprueba el respaldo en la evidencia suministrada; no establece que la evidencia sea correcta ni suficiente para responder a la pregunta. Registra por separado las respuestas sin afirmaciones e informa de cuántas hay, en lugar de asignar fidelidad perfecta a una respuesta vacía.

La documentación actual de Ragas recomienda la API de colecciones que se muestra a continuación. En un proyecto de uv, instala ragas y openai con uv add ragas openai, define OPENAI_API_KEY y guarda este código como un script que ejecutar con uv run. Hace llamadas al proveedor y genera sus cargos; la puntuación es el resultado de un juez, no una constante esperada determinista. Fija las dependencias resueltas en tu 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:
        # Ragas 0.4.3 sends max_tokens by default; GPT-5.6 models need max_completion_tokens.
        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 juez; la factoría de Ragas reenvía los argumentos del modelo. Compara los falsos aprobados, los falsos suspensos y el coste del juez con etiquetas humanas antes de sustituir un juez más barato que ya hayas contrastado con etiquetas.

La tabla de frameworks de la Parte 1 también menciona la relevancia de la respuesta y la precisión del contexto. La misma API de colecciones puntúa ambas con el mismo llm. AnswerRelevancy necesita además un modelo de embeddings, por ejemplo embedding_factory("openai", model="text-embedding-3-small", client=client) de ragas.embeddings.base. ContextPrecision necesita una respuesta reference en ascore. Cada puntuación responde a una pregunta distinta: la fidelidad pregunta si el contexto respalda la respuesta, la relevancia de la respuesta pregunta si la respuesta aborda la pregunta, y la precisión del contexto pregunta si el contexto relevante queda en los primeros puestos.

Para construir tu propio puntuador, conserva la estructura de extraer, verificar y agregar. La extracción es una llamada a un LLM que divide la respuesta en afirmaciones; la verificación la hace un modelo de NLI (inferencia del lenguaje natural) o un LLM como juez. Valida por separado la extracción y la implicación, y registra los juicios sin afirmaciones o fallidos. Por ejemplo, una respuesta con 4 afirmaciones, 3 de ellas respaldadas, obtiene 3/4 = 0,75. Una respuesta con 0 afirmaciones no se puntúa.

Notebook 05 ejecuta offline un ejemplo fijo sobre Marte con una aproximación léxica; esta no verifica la implicación semántica. Su módulo evaluation/faithfulness.py también implementa extracción y verificación de afirmaciones con SGR. La batería generation evalúa QA sintética etiquetada con --suite generation --live. El repositorio usa solo modelos de OpenAI. gpt-5.6-luna genera y evalúa por defecto; --judge-model gpt-5.6-terra cambia el juez. Ambos modelos son de un proveedor, por lo que el repositorio muestra el flujo del juez y no una comparación entre familias.

Una alternativa diseñada a propósito frente al LLM como juez es HHEM-2.1-Open (Hughes Hallucination Evaluation Model, de Vectara), un clasificador con fine-tuning para detectar alucinaciones. Su ficha del modelo documenta el checkpoint, la puntuación en bruto de 0 a 1 que emite y los resultados de precisión equilibrada en AggreFact y RAGTruth. No publica ningún límite de decisión por defecto, así que elegirlo te corresponde a ti. Trata esos datos como evidencia de la ficha del modelo, no como una garantía sobre tu corpus: calibra el umbral con etiquetas locales y compáralo con el juez que hayas elegido antes del despliegue.

Evaluación por 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 facts∣∣total atomic facts∣\text{FActScore} = \frac{|\text{supported atomic facts}|}{|\text{total atomic facts}|}

Implementación de referencia: shmsw25/FActScore. La evaluación original se centró en biografías con Wikipedia como evidencia. Adaptarla a resúmenes u otros textos largos exige verificar localmente la extracción de afirmaciones, la cobertura de evidencia y los juicios de respaldo. Los hechos triviales repetidos pueden inflar la puntuación, y afirmaciones individualmente verdaderas pueden formar una respuesta engañosa. MontageLie (EMNLP 2025) prueba esta debilidad mediante relaciones y órdenes engañosos entre afirmaciones verdaderas. VeriScore trata afirmaciones con los matices necesarios; el filtro Core ayuda a evitar añadir hechos para inflar la puntuación.

Precisión de las citas

Mide la precisión de las citas (los tramos citados respaldan de verdad la afirmación) y el recall de las citas (las afirmaciones que deberían citarse, se citan):

cite_precision=∣cited spans that support a claim∣∣cited spans∣,cite_recall=∣claims with at least one supporting cited span∣∣claims 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 evaluación del respaldo. Thakur et al. (SIGIR 2025) informan de un acuerdo de aproximadamente el 56 % con los juicios humanos emitidos desde cero y del 72 % en una condición distinta, en la que las personas poseditaban las predicciones del LLM. Esta última es una anotación asistida, no una evidencia independiente de que mejore la precisión del juez. Mantén la condición de anotación junto a la cifra. Como aproximación automática, ALCE (Gao et al., EMNLP 2023) implementa la precisión y el recall de las citas con verificación basada en NLI.

Corrección de la respuesta, completitud y negativa a responder

  • Corrección de la respuesta frente a una referencia: la coincidencia exacta o el F1 por tokens pueden servir para respuestas cortas; evaluate.load("squad") de Hugging Face calcula ambos, y el paquete bert-score calcula BERTScore. Para respuestas más largas, comprueba las relaciones factuales, las entidades, las cantidades, las negaciones y la información requerida frente a referencias revisadas. BERTScore y el coseno de embeddings miden similitud; un número erróneo o una negación pueden conservar una puntuación alta. AnswerCorrectness de Ragas combina la comparación factual con la similitud en lugar de equipararlas.
  • Completitud mediante nuggets: un nugget es una unidad de información relevante, y se distinguen los nuggets vitales de los útiles pero opcionales. Una pregunta sobre una fecha de fundación puede exigir el año; el nombre del fundador no es obligatorio automáticamente. AutoNuggetizer construye y refina nuggets a partir de conjuntos de documentos juzgados y comprueba después su presencia en las respuestas generadas. Su informe inicial de TREC 2024 cubrió 21 temas y 45 ejecuciones. El resumen de TREC 2025, publicado en marzo de 2026, extiende el protocolo a consultas narrativas y evalúa la relevancia de la recuperación, la completitud de la respuesta y la atribución. Son protocolos públicos de evaluación, no una prueba de que todo sistema RAG de producción necesite la misma rúbrica de nuggets.
  • Comportamiento de negativa: etiqueta si la evidencia suministrada permite responder y mide después las negativas correctas entre todas las negativas y las negativas entre los casos en los que se debería abstenerse. NoMIRACL (Findings of EMNLP 2024) prueba la robustez frente a pasajes suministrados relevantes y no relevantes; no demuestra que todo el corpus carezca de respuesta. En tu propia suite, separa los fallos de recuperación de las consultas realmente fuera de alcance.

Verificación posterior a la generación

Las mejoras de fiabilidad más baratas suelen venir de comprobaciones posteriores deterministas, no de modelos más grandes.

  • Marca de entidades no vistas: registra las cadenas de entidades con nombre de la respuesta que no aparecen en una cadena de contexto normalizada (por ejemplo, ents de spaCy más coincidencia exacta). Es una marca barata y específica del dominio para cadenas de entidades no vistas, no una comprobación de anclaje: no puede establecer identidad, relación, negación, tiempo ni procedencia. Mide su precisión y su recall con etiquetas locales antes de usarla para aprobar un lanzamiento, y conserva la implicación a nivel de afirmación o la revisión humana para la verificación.

    En este ejemplo la marca no encuentra ninguna entidad no vista, aunque la respuesta dice lo contrario que el contexto.

    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 afirmaciones: extrae las afirmaciones, ejecuta NLI frente al contexto y haz fallar o marca cualquiera que quede por debajo del umbral. Modelos de NLI como medida de fidelidad: 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.

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

  • Calibración de la confianza: recoge la confianza verbalizada (“¿Qué grado de confianza tienes, de 0 a 1?”) y compárala con la corrección real en el conjunto de evaluación. Representa una curva de calibración e informa del Expected Calibration Error: ECE=∑m=1M∣Bm∣n∣acc(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 grupos por nivel de confianza. Implementaciones: netcal, torchmetrics.CalibrationError. Un modelo que declara una confianza de 0,9 debería acertar en aproximadamente el 90 % de los casos comparables; mide la diferencia en lugar de suponer que está calibrado.


Parte 7: Evaluación de RAG anclado en una ontología

Las métricas estándar anteriores cubren el RAG de corpus abierto. Si tu RAG recupera frente a una ontología, una taxonomía o un grafo de conocimiento estructurados, esas métricas son necesarias, pero no suficientes. Algunos ejemplos son los productos de un catálogo, las enfermedades en SNOMED, los componentes de una lista de materiales (BOM) y las técnicas de seguridad de MITRE ATT&CK. También necesitas medir la capa de la ontología.

Precisión del enlazado de entidades

La primera tarea es asignar una mención de la consulta a una entidad de la ontología (“Aspirin” → wikidata:Q18216, “the 737” → aircraft:Boeing_737).

  • Precisión, recall y F1 a nivel de mención: estándar, frente a los tramos de mención de referencia (se calculan con seqeval o con un comparador de conjuntos de tramos).
  • Precisión de la desambiguación: de las menciones detectadas correctamente, ¿qué fracción se asigna al ID de entidad correcto? Entre las referencias públicas están 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: precisión y recall para “entidad que no está en la ontología”. Mide por separado el enlazado excesivo a entidades parecidas pero erróneas y la abstención correcta.

Evaluación consciente de la jerarquía

La exactitud simple trata “predijo Sedán cuando la verdad es Hatchback” igual que “predijo Submarino cuando la verdad es Hatchback”. Esos errores no son iguales. En el diagrama, Sedán comparte dos de sus tres nodos con la etiqueta verdadera, y Submarino solo comparte la raíz; la precisión jerárquica siguiente convierte ese solapamiento en una puntuación.

La misma taxonomía de vehículos dibujada dos veces para la etiqueta verdadera Hatchback: la predicción Sedán comparte Coche y Vehículo y puntúa hP = 2/3; la predicción Submarino comparte solo Vehículo y puntúa hP = 1/3; la exactitud simple es 0 en ambos casosLa misma taxonomía de vehículos dibujada dos veces para la etiqueta verdadera Hatchback: la predicción Sedán comparte Coche y Vehículo y puntúa hP = 2/3; la predicción Submarino comparte solo Vehículo y puntúa hP = 1/3; la exactitud simple es 0 en ambos casos

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

    hP=∑q∣P^q∩Tq∣∑q∣P^q∣,hR=∑q∣P^q∩Tq∣∑q∣Tq∣,hF1=2⋅hP⋅hRhP+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}

    Impleméntalo con networkx sobre el grafo de la ontología: amplía cada predicción y cada etiqueta con sus ancestros y calcula después los solapamientos de conjuntos anteriores.

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

    WuP(c1,c2)=2⋅depth(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 en la taxonomía. Está disponible de serie 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 propias, calcula el LCA con networkx.

  • Tasa de confusión entre hermanos y padres: mide 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 falsa exclusión del filtro (repaso, ahora crítica)

En los sistemas anclados en una ontología, los filtros duros suelen proceder de la propia ontología (“recupera solo documentos etiquetados con la categoría X”). La métrica de la tasa de exclusión (definida en la Parte 5) pasa a ser una señal principal de corrección. Una predicción de categoría errónea puede reducir el recall a cero; la tasa de exclusión atribuye esa pérdida al filtro.

Conformidad de la generación restringida

Cuando tu 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 esquema: porcentaje de salidas que se parsean y validan frente al esquema de la ontología. Valida con jsonschema o pydantic. JSONSchemaBench es el benchmark público de structured output general; para esquemas específicos de una ontología, construye tu propio validador.
  • Conformidad del vocabulario: porcentaje de entidades con nombre de la salida que son ID válidos de la ontología; una comprobación de pertenencia a un conjunto, de una línea, frente al vocabulario cerrado.
  • Conformidad semántica: una salida sintácticamente válida puede elegir igualmente una entidad válida pero equivocada. Combina la conformidad con la corrección de la respuesta final.

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

Auditabilidad

Para los sistemas anclados en una ontología cuyas respuestas se someten a revisión:

  • Completitud de las citas: porcentaje de afirmaciones factuales con al menos una cita verificable.
  • Profundidad de la procedencia: porcentaje de citas que se resuelven hasta un documento de origen con un ID estable, y no solo hasta el hash de un fragmento.
  • Tasa de reproducibilidad: al volver a ejecutar la misma consulta sobre una instantánea fija se obtiene la misma respuesta. Fija la versión del modelo, el runtime, la configuración de decodificación y la semilla, y establece después la tasa de repetición requerida según las necesidades de auditabilidad del flujo de trabajo. Una temperatura cero no garantiza por sí sola el determinismo. Un fallo puede proceder de la generación, del runtime de serving o de cualquier etapa anterior.

Parte 8: Evaluación a nivel de sistema

Calidad global de la respuesta

  • LLM como juez (Zheng et al., NeurIPS 2023): un enfoque de evaluación escalable basado en modelos. G-Eval (Liu et al., EMNLP 2023) genera pasos de evaluación a partir de la tarea y los criterios y pondera después los niveles de valoración por la probabilidad de sus tokens: score=∑ip(si) si\text{score} = \sum_i p(s_i)\,s_i. Son probabilidades, no log-probabilidades. El grado de acuerdo depende del juez, de la tarea, del prompt y del conjunto de calibración.
  • Preferencia por pares: presenta al juez la respuesta A frente a la respuesta B y registra su preferencia. Esto sustituye una valoración absoluta por una decisión comparativa, pero sigue necesitando calibración frente a las preferencias humanas. MT-Bench informó de un acuerdo del juez GPT-4 superior al 80 %, tanto con las preferencias humanas como entre humanos, en las condiciones de su benchmark; no traslades esa tasa a otro dominio sin calibrar.

El LLM como juez tiene sesgos reales:

  • Sesgo de posición: mide la sensibilidad al orden con casos etiquetados por personas para el juez y la tarea elegidos. La aleatorización o la agregación con el orden intercambiado pueden ayudar en algunos pares de modelo y tarea, pero conserva cualquiera de las dos solo si mejora el acuerdo local con las personas; el estudio controlado de 2026 encontró que intercambiar las posiciones era perjudicial en sus casos adversarios.
  • Sesgo de verbosidad: los jueces pueden confundir la longitud con la calidad. El estudio controlado de 2026, versión 2, citado antes, encontró un comportamiento heterogéneo en los pares de expansión: tres jueces prefirieron las respuestas más largas, Claude prefirió las concisas y GPT-4o fue aproximadamente neutral. Los cinco rindieron bien en los controles de truncamiento. Esos resultados dependen del benchmark, así que indica a tu juez cómo tratar la completitud y el relleno, y después informa del rendimiento con la longitud controlada sobre tu propia rúbrica.
  • Riesgo de autopreferencia: Zheng et al. observaron en sus datos una tasa de victorias propias un 10 % mayor en GPT-4 y un 25 % mayor en Claude-v1, pero concluyeron que los datos limitados y las diferencias pequeñas no permitían establecer un sesgo de autoensalzamiento. Compara jueces de la misma familia y de otras familias con etiquetas humanas locales; elige el juez mejor calibrado en lugar de suponer que cualquiera de las dos disposiciones es segura.

Receta práctica: elige un juez con datos de calibración etiquetados por personas, enmascara la identidad de los modelos, mide la sensibilidad al orden y declara la política de longitud en la rúbrica. Repite los casos solo cuando las muestras adicionales reduzcan de forma apreciable la incertidumbre. En las evaluaciones de alto riesgo, compara jueces de la misma familia y de otras familias, y analiza los desacuerdos frente a las etiquetas humanas.

Schema-Guided Reasoning para jueces

La salida de formato libre es una fuente de variación entre ejecuciones del juez. Dos ejecuciones sobre la misma respuesta pueden organizar la rúbrica de forma distinta y producir puntuaciones diferentes. Schema-Guided Reasoning (SGR) hace explícita esa rúbrica: define las etapas de evaluación como un esquema de Pydantic y usa después una salida restringida mediante Outlines, XGrammar, los structured outputs de vLLM o el response_format de OpenAI para imponer las restricciones de esquema admitidas. El orden de los campos puede ayudar a quien revisa a inspeccionar el registro, pero no demuestra que el modelo razonara por las etapas en ese orden.

En la evaluación de RAG, el esquema descompone el juicio en campos explícitos y auditables, en lugar de dejar que el modelo salte directamente a un número:

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")

Este contrato ilustrativo calcula la puntuación solo cuando las listas de veredictos forman una partición completa y disjunta de las afirmaciones extraídas. Un resultado sin afirmaciones no tiene puntuación de fidelidad y debe quedar fuera del agregado de fidelidad; informa de cuántos hay y puntúa por separado la corrección de las negativas, para que las abstenciones no mejoren el promedio en silencio. Conserva esas comprobaciones semánticas en producción; la salida restringida garantiza la forma, no un veredicto sin sesgos. El modelo de Pydantic también hace visible un cambio de rúbrica como un diff de código, mientras que la calibración humana pone a prueba el juicio en sí.

Esto sirve para cualquier juez basado en una rúbrica, no solo para la fidelidad. La preferencia por pares, el respaldo de las citas y la corrección de las negativas se benefician del mismo tratamiento.

Notebook 07 mide el sesgo de posición con llamadas por pares que invierten el orden. Su módulo evaluation/llm_judge.py también implementa puntuación individual con SGR. Las ejecuciones por pares presentan ambos órdenes de respuesta y muestran la consistencia al intercambiarlos. El juez individual devuelve un entero; no es el método G-Eval ponderado por probabilidades. Como ambos modelos son del mismo proveedor, medir la autopreferencia sigue requiriendo un juez de otra familia comparado con etiquetas humanas locales.

Latencia y coste

  • p50, p95, p99 en cada etapa del pipeline. Un buen comienzo es p95 para el SLO y p99 para las alertas; cambia cualquiera de los dos cuando conozcas el recorrido del usuario, el volumen de tráfico y el presupuesto de errores.
  • Tiempo hasta el primer token (TTFT) frente al tiempo total de generación. A los usuarios les importa el TTFT en una experiencia con streaming.
  • Desglose por etapas: recuperación, reranking, generación y posprocesamiento. Usa la traza para localizar la cola en lugar de suponer qué etapa la causó; registra el dispositivo y el tamaño de lote del reranker al comparar ejecuciones.
  • $/consulta total = embedding + recuperación + reranking + generación + almacenamiento amortizado. Mide p50 y p99; la cola larga es donde se va el presupuesto.
  • Tasas de aciertos de caché en la caché de embeddings, la caché de recuperación y la KV cache. Fija objetivos distintos a partir de la repetición observada, la política de invalidación y el coste que se evita en cada capa.

Notebook 08 muestra p50/p95/p99 de la etapa retrieve instrumentada por evaluation/runner.py. El módulo evaluation/latency.py puede resumir otras etapas cuando las instrumentas explícitamente. make benchmark barre ajustes de chunking y embeddings sin llamadas a API. La comparación de LLM requiere una clave de OpenAI y uv run python -m rag_evals.scripts.benchmark --skip-chunking --skip-embedding --live.

Pruebas A/B

  • Unidad de aleatorización: define primero el resultado que quieres estimar. Después pregúntate si las respuestas anteriores pueden cambiar el comportamiento posterior y si la versión asignada a un usuario puede afectar a la experiencia de otro. Usa la asignación por usuario o por sesión cuando la exposición repetida pueda cambiar el comportamiento o crear una experiencia incoherente. La asignación por consulta solo es defendible cuando esos efectos son despreciables y el análisis modela las observaciones repetidas.
  • Métricas primarias, de salvaguarda y exploratorias: regístralas de antemano. Elige la medida primaria a partir del resultado del producto; entre los indicadores indirectos de satisfacción están los pulgares, las regeneraciones y el tiempo de permanencia. Trata la latencia y el coste como salvaguardas cuando limiten la experiencia.
  • Tamaño de la muestra: haz un análisis de potencia antes de lanzar, a partir del efecto mínimo que merece la pena detectar, la varianza de referencia, 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 conjunto de oro cubre tres intenciones y el tráfico de producción abarca doce, el Recall@10 solo mide esas tres. Peor aún, un conjunto de pruebas sobreajustado a preguntas fáciles (“¿Cuál es la política de reembolso de la empresa?”) puede aprobar un sistema que falla con las difíciles (“¿Cuándo corresponde un reembolso por una cancelación parcial según las normas de protección del consumidor de la UE, facturada en EUR, para un cliente en Irlanda?”). La puntuación agregada 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 cualquiera de los dos sentidos. Si el verdadero conjunto relevante es {a, b} pero las etiquetas solo contienen {a}, recuperar {a} puntúa 1,0 en lugar de 0,5; recuperar {b} puntúa cero en lugar de 0,5. Versiona los juicios y revisa la evidencia recién recuperada y sin juzgar antes de interpretar una diferencia.

Construye primero el conjunto de pruebas en torno a la distribución real de las consultas y a su dificultad. Después elige las métricas que respondan a los modos de fallo objetivo y ajusta el sistema con ellas.

Generación de consultas sintéticas

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

  • Por fragmento: “Genera 3 preguntas que un usuario podría hacer y que este fragmento responda.”
  • Multisalto: muestrea dos fragmentos y genera una pregunta que requiera ambos.
  • Adversarias: genera preguntas con entidades distractoras, formulaciones casi duplicadas y menciones ambiguas.

La generación de pruebas de Ragas usa escenarios basados en grafos con consultas de un solo salto y de varios saltos, y con necesidades de información específicas o abstractas. DataMorgana genera benchmarks sintéticos configurables a partir de categorías de usuarios y de preguntas. Los datos sintéticos son útiles para los arranques en frío y las pruebas de cobertura. No pueden sustituir a las consultas reales de los usuarios.

Construcción del conjunto de oro

Los datos revisados por personas forman la base del conjunto de referencia.

  1. Muestrea consultas reales de usuarios (o simuladas, si aún no has lanzado) estratificadas por intención.
  2. Pide a expertos en la materia (SMEs) que respondan cada pregunta e identifiquen qué documentos contienen la respuesta.
  3. Empieza con unos cientos de consultas (200–500) y comprueba el intervalo. Con 200 consultas y una tasa de aciertos cercana a 0,8, el intervalo del 95 % es de aproximadamente ±5,5 puntos (1,96 × √(0,8 × 0,2 / 200)). Si el cambio que necesitas detectar es menor, añade consultas a los subconjuntos que deciden el lanzamiento.
  4. Añade consultas de producción revisadas (Parte 10) cuando las comprobaciones de deriva o los nuevos tipos de consulta muestren una laguna en la matriz de cobertura que figura más abajo.

Mantén cuatro conjuntos separados:

  • Desarrollo: ajusta prompts, k y el chunking.
  • Calibración del juez: etiquetas humanas que comprueban al juez.
  • Lanzamiento: se puntúa una sola vez por candidato y nunca se usa para ajustar.
  • Monitorización: tráfico de producción reciente, revisado a ciegas.

Agrupa los documentos de origen y las sesiones que compartan antes de dividir. Cuando una consulta o una etiqueta guía el ajuste, pasa a ser dato de desarrollo, así que un conjunto de lanzamiento usado para ajustar pasa a ser un conjunto de desarrollo. Reserva casos intactos para validar la configuración elegida y el juez.

Registra las versiones del corpus, de las consultas, de las etiquetas de relevancia y de la política, la unidad de k, el presupuesto de contexto suministrado, las revisiones del puntuador y del juez, y las reglas de agregación. Compara las diferencias por consulta emparejadas sobre la misma población, con su incertidumbre y el recuento de cada subconjunto. Incluye todas las consultas esperadas: un tiempo de espera agotado, un resultado ausente o una respuesta del juez que no se puede puntuar deben seguir visibles en el recuento de ejecuciones completadas y de errores. No dejes que una puntuación de lanzamiento mejore descartando fallos en silencio. ARES ofrece un enfoque de investigación con jueces automáticos, validación humana e inferencia potenciada por predicciones para estimar el sistema cuando la anotación es escasa; una suite local revisada puede empezar de forma más sencilla.

Conjuntos de pruebas adversarios

  • Contrafactuales: intercambia las entidades clave de la consulta. ¿Recupera el sistema los fragmentos correctos para la consulta modificada?
  • Distractores: consultas en las que el corpus contiene una respuesta plausible pero errónea que no debería recuperarse. Esto es lo que somete a prueba RGB (Chen et al., AAAI 2024): robustez al ruido, rechazo negativo, integración de información y robustez contrafactual.
  • Negación y cuantificadores: consultas con “no”, “excepto” y “solo”. Los recuperadores densos suelen tener dificultades con ellas.
  • Fuera de alcance: consultas sin respuesta en el corpus. El sistema debería decir “no lo sé”, no alucinar. NoMIRACL aporta pruebas de relevancia y de posibilidad de respuesta a nivel de pasaje; añade etiquetas separadas de fuera de alcance a nivel de corpus. Evalúa la abstención de forma explícita con los tipos de consulta de tu producción.

Cobertura y evaluación continua

  • Construye una matriz de cobertura: intención de la consulta × tipo de documento × rama de la ontología. Una consulta por celda es un inventario de cobertura inicial, no potencia estadística suficiente para una decisión de lanzamiento. Las celdas vacías revelan huecos de cobertura; dimensiona los subconjuntos con datos según la incertidumbre que puedas tolerar.
  • Cadencia inicial: un subconjunto de unas 50 consultas en cada PR, el conjunto de oro completo cada noche o con cada candidato a versión, y comprobaciones semanales de deriva sobre una muestra de producción. El comando make eval-fast del repositorio complementario ejecuta el subconjunto de 50 consultas. Cambia el calendario cuando conozcas el coste de la evaluación y la rapidez con que cambia el tráfico.
  • Para la deriva, usa una muestra de producción móvil y estratifica por feedback en lugar de cambiar en silencio la distribución objetivo.

Parte 10: Monitorización en producción

La suite de evaluación que lanzas describe el sistema en el momento del 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 KPI de calidad positivos o negativos, hasta que se correlacionen con una revisión ciega o con feedback explícito en una muestra local.
  • Tasa de clics o de apertura en las fuentes citadas (si tu interfaz las expone).
  • Tiempo de permanencia en la respuesta.
  • Tasa de regeneración: porcentaje de respuestas que el usuario vuelve a preguntar o pide al sistema que rehaga. Trátala como una señal más de insatisfacción y calíbrala con conversaciones revisadas.
  • Tasas de copiar, compartir y exportar: señales implícitas candidatas que pueden representar utilidad, verificación, traspaso o insatisfacción. Mide su asociación y su intervalo de confianza antes de asignarles un sentido.
  • Patrones de seguimiento: usa “¿Seguro?” o “¿Y qué pasa con X?” como estratos de revisión y etiqueta después su asociación con la desconfianza o con una necesidad sin resolver.
  • Pulgar arriba o abajo con categorías de motivo opcionales (incorrecto, incompleto, fuera de tema, dañino, lento). Las ediciones en línea pueden conservar más contexto de diagnóstico; evalúa ese valor con muestras revisadas.

Detección de deriva

  • Deriva de las consultas: compara los embeddings de las consultas con una ventana de referencia mediante MMD o un clasificador validado de referencia frente a actual. KL requiere un estimador de probabilidad definido, como histogramas elegidos; las coordenadas crudas de un embedding no son probabilidades. Calibra las alarmas con desplazamientos conocidos y después inspecciona los subconjuntos afectados.
  • Deriva de los embeddings: fija una representación y un conjunto de sondas, y mide después la estabilidad de los vecinos y la calidad de la recuperación. Las distintas versiones de un modelo no tienen por qué compartir dimensiones ni una base de coordenadas, así que el coseno entre versiones puede carecer de sentido. Migra juntos los codificadores de consultas y de documentos, evalúa el nuevo índice y conserva instantáneas versionadas para poder revertir.
  • Deriva del rendimiento: sigue a lo largo del tiempo métricas equivalentes a las de producción (la tasa de regeneración por intención). Los cambios bruscos y los graduales sugieren hipótesis distintas, pero su forma no establece la causa; inspecciona los cambios en los datos, el tráfico, el proveedor, las políticas y los despliegues.

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

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

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

  • Muestrea las salidas de baja confianza hacia una cola de revisión.
  • Incluye una muestra aleatoria del tráfico de producción para revisión ciega; el 1–2 % del tráfico es una tasa inicial, que se ajusta según el volumen de tráfico, el riesgo y la capacidad de los revisores.
  • Sobremuestrea las salidas con pulgar abajo para revisarlas junto con la muestra aleatoria.
  • Usa las salidas revisadas para ampliar el conjunto de oro.

Conserva la muestra aleatoria: sin ella, el conjunto de oro solo gana respuestas de las que los usuarios se quejaron.

El conjunto mínimo de salvaguardas

Elige las prioridades y los umbrales de las alertas a partir del daño al usuario, los SLO y el rendimiento validado de los detectores. Entre las señales candidatas están:

  1. Puntuación de fidelidad o de HHEM por debajo del umbral en una muestra móvil de producción.
  2. Latencia p95 por encima del SLO.
  3. Tasa de falsa exclusión 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 tráfico, la estacionalidad y el presupuesto de falsas alertas.
  5. Coste por consulta por encima del presupuesto.

Una primera regla de triaje: una alerta sin cambios de código ni de modelo apunta a una deriva; una alerta justo después de un lanzamiento apunta a una regresión. Confirma cualquiera de las dos en las trazas y en los subconjuntos afectados, porque un despliegue puede coincidir con una deriva del tráfico, y puede haber un cambio de proveedor o de datos sin un lanzamiento de la aplicación.


Salvedades

  • Los objetivos son locales, no universales. Los valores iniciales de esta guía, incluidos los umbrales del repositorio complementario, son primeros ajustes que debes afinar con tus propios datos. Las cifras ilustrativas son resultados de ejemplo. Calibra los umbrales de lanzamiento según tu dominio, lo que está en juego, la incertidumbre del conjunto de evaluación y las expectativas de los usuarios.
  • El panorama de frameworks cambia rápido. Las versiones de HHEM, los nombres de las métricas de RAGAS, las fichas de los modelos y el orden de las clasificaciones pueden cambiar tras la publicación. Vuelve a consultar la fuente enlazada y repite el benchmark antes de comprometerte.
  • Las cifras de acuerdo del LLM como juez traen asteriscos. El 80 % de acuerdo entre GPT-4 y las personas procede de las condiciones de MT-Bench / Chatbot Arena. Ese resultado no establece el acuerdo en un dominio de nicho ni en un subconjunto adversario. Usa jueces para puntuar más casos de los que las personas pueden revisar y sigue revisando a mano una muestra.
  • Las mejoras que publican los proveedores en sus benchmarks a menudo no se pueden reproducir de forma independiente. Reprodúcelas con tus propios datos antes de creerte una cifra, sobre todo en rerankers y sistemas de OCR recientes.
  • Ninguna métrica sustituye a mirar las salidas. Empieza leyendo 50 respuestas de producción al azar cada semana. Las métricas escalan ese hábito; no lo reemplazan.

Referencias

Frameworks y benchmarks

Recuperación y ranking

Generación, fidelidad y jueces

Deriva y producción

Código complementario

  • slavadubrov/rag-evals-demo: harness ejecutable que separa recuperación en SciFact, generación sobre QA sintética etiquetada y comprobaciones offline con datos de prueba fijos, con barridos de chunking y embeddings y una comparación en vivo opcional entre dos modelos de OpenAI. Los notebooks 00–09 explican los procesos; las pruebas unitarias fijan algunos ejemplos resueltos, mientras que los datos antiguos del filtro usan otra población autorizada. El índice Qdrant integrado funciona sin Docker.