Auditoría de la recuperación RAG en busca de fragmentos obsoletos, desviaciones y contexto ruidoso.
Una auditoría en Python para la recuperación RAG: consultas ideales, filtros de relevancia por coseno, verificaciones de desviación de Kolmogorov-Smirnov y eficiencia del contexto, además de cómo leer el informe.
Fuentes: I Built a RAG System That Audits Itself. Here’s How (With Code).
El tipo silencioso de fallo en RAG
Imagínese un bot de soporte que le dice a un cliente que el plazo de devolución es de 30 días cuando en realidad es de 14. El documento de la política fue modificado en enero y el índice de incrustación se modificó por última vez en octubre. Nadie resulta perjudicado ni se presenta demanda alguna, pero lo preocupante es que nadie puede saber desde cuándo se está dando esa respuesta incorrecta. ¿Una semana? ¿Tres meses? Sin una forma de verificarlo, es imposible saberlo.
Ese es el fallo que merece atención aquí, y no es el dramático en el que un modelo genera alucinaciones o habla sin rumbo. Es el silencioso. Los paneles de control muestran indicadores positivos, y una pequeña proporción de usuarios ha sido engañada durante meses sin que se emita ni una sola alerta.
La mayoría de los equipos que desarrollan RAG miden lo incorrecto. Preguntan si la respuesta final suena bien, pero pasan por alto si el material que sirvió de base para esa respuesta era de calidad. En su mayoría, esto se debe a que nadie les proporcionó una herramienta sencilla para ello. Lo que sigue es una de esas herramientas: un script en Python que audita automáticamente la capa de recuperación de información, creado con bibliotecas de código abierto y un archivo JSON con consultas de prueba; su configuración tarda aproximadamente 30 minutos y no requiere ninguna plataforma de observabilidad ni conocimientos avanzados en estadística.
Enfócate en la recuperación de información, no en el modelo
Cuando un sistema RAG funciona mal, la reacción instintiva es culpar al modelo: prompt incorrecto, temperatura inadecuada, versión equivocada. Se ajusta el prompt o se pasa a un modelo más reciente, y las cosas mejoran durante unas semanas, pero luego el problema vuelve, porque la verdadera causa está en otro lugar. La cadena de procesamiento en los sistemas en producción suele funcionar de esta manera:
Un documento cambia, pero el índice de incrustación no se actualiza. Se recupera un fragmento obsoleto, el LLM responde con confianza a partir de un contexto desactualizado, la respuesta es incorrecta y nadie puede explicar por qué.
El modelo no miente. Hace exactamente lo que se le indica, que es generar una respuesta a partir de los fragmentos proporcionados. Si se le proporcionan fragmentos de seis meses de antigüedad, la respuesta también tendrá seis meses de antigüedad, presentada de manera fluida y con una gramática impecable.
Lo engañoso radica en el método de evaluación habitual, que es de extremo a extremo: se ingresa una consulta y se evalúa la respuesta que sale. RAGAS, uno de los marcos de evaluación más adoptados, está diseñado para todo el proceso y mide la precisión del contexto, la fidelidad y la relevancia de la respuesta (Es et al., 2023, EACL 2024). Es un marco sólido. Sin embargo, evalúa al sistema en su conjunto, no únicamente a la capa de recuperación, por lo que una puntuación positiva de extremo a extremo puede ocultar una disminución lenta en la eficiencia de recuperación cuando el modelo, ocasionalmente, tiene suerte y logra compensarla.
Existe un segundo problema relacionado con ese primero. Liu y colaboradores informaron en 2024 que los modelos de lenguaje rinden significativamente peor cuando la información relevante está oculta en medio de un contexto largo, incluso en los modelos entrenados específicamente para entradas extensas. Por lo tanto, recuperar cinco fragmentos de los cuales tres son solo marginalmente relevantes perjudica activamente al sistema, y los tokens dedicados a ellos afectan la calidad de las respuestas. Ninguno de estos problemas se manifiesta a menos que se examine directamente la capa de recuperación, que es precisamente lo que se planea hacer.
Tres modos de fallo que las métricas estándar pasan por alto
Envenenamiento de fragmentos
El nombre “envenenamiento de chunks” no proviene de ningún artículo académico. Se refiere a esos chunks que parecen relevantes porque su redacción coincide con la consulta, por lo que obtienen una alta clasificación en búsquedas vectoriales aunque no contienen la respuesta. Una causa común es una división que deja las palabras clave en un chunk y el contenido real en otro. El chunk rico en palabras clave obtiene buenas puntuaciones en similitud de coseno, mientras que el chunk útil queda fuera de los resultados principales, y el modelo elabora una respuesta plausible pero incorrecta.
Contenido obsoleto
Supongamos que una sección de Preguntas Frecuentes actualizó su política de reembolsos en enero y que el índice se actualizó por última vez en octubre. El chunk con la política antigua sigue apareciendo en las búsquedas relacionadas con reembolsos y sigue siendo considerado “relevante” por el modelo. Los usuarios terminan recibiendo respuestas contradictorias por parte del equipo de soporte, aunque las métricas no muestran nada anómalo.
Deriva de los embeddings
Lento y acumulativo, este es el más peligroso de los tres. Un modelo de incrustación registra el corpus tal como estaba en el momento de la indexación. Cada edición de documento, cada nuevo término que entra al dominio y cada cambio en la forma en que los usuarios formulan consultas erosionan un poco la precisión de esa instantánea. La similitud coseno promedio puede descender de 0.82 a 0.74 en seis meses. Nada se rompe, no se activa ninguna alerta, y la degradación solo se hace evidente cuando las respuestas empeoran visiblemente, para entonces el sistema ya está meses atrasado.
El ruido en el contexto también tiene un costo. Con cinco fragmentos en la ventana y tres de ellos irrelevantes, se desperdician tokens y la calidad disminuye, ya que el contenido en medio de un contexto largo recibe menos atención que el contenido en los extremos, como lo documentaron Liu y colaboradores.
Qué hace la auditoría
Tres verificaciones se alinean con esos tres modos de fallo.
- Relevancia y resultados obtenidos. Para cada consulta del conjunto de pruebas, el script obtiene los mejores K fragmentos y calcula la similitud coseno entre el embedding de la consulta y el embedding de cada fragmento. También verifica si los fragmentos que se sabe que contienen la respuesta aparecen en los resultados. Si no es así, la consulta se marca como fallida.
- Desviación con respecto a una línea de base. Cada ejecución almacena su distribución de puntuaciones de similitud, y las ejecuciones posteriores comparan la nueva distribución con esa línea de base almacenada utilizando la prueba de Kolmogorov-Smirnov. Si hay una diferencia significativa, se emite una alerta de desviación. No es necesario seguir los cálculos matemáticos: un valor p inferior a 0.05 indica que algo ha cambiado y merece ser analizado.
La auditoría genera un informe JSON estructurado. Basta con un job cron semanal, prestando atención a la tasa de marcas y al análisis de desviaciones. La mayoría de los sistemas RAG nunca han contado con este tipo de capa de monitoreo.
El conjunto de consultas ideal
Antes de escribir cualquier código, se necesita un conjunto de consultas ideal: un archivo JSON con consultas representativas, cada una emparejada con los IDs de los fragmentos que deberían aparecer en sus resultados.
[
{
"query": "What is the refund policy for digital products?",
"expected_chunk_ids": ["faq_doc_chunk_12", "faq_doc_chunk_13"],
"notes": "Customer FAQ, policy updated 2024-Q1"
},
{
"query": "How do I reset my API key?",
"expected_chunk_ids": ["api_docs_chunk_07"],
"notes": "API documentation, stable"
}
]
El tamaño es importante. Al principio, veinte consultas parecían suficientes, pero no lo eran, ya que la prueba de deriva requiere más puntos de datos que eso, y 20 generan una distribución demasiado escasa como para ser fiable. Considere 50 como mínimo y 100 o más como el punto en el que las señales de deriva adquieren significado.
Comience tomando las consultas de los usuarios más frecuentes de los registros de la aplicación. Ejecute la recuperación de cada una a mano, examine los resultados y anote qué IDs de fragmentos se encuentran en las posiciones más altas. Es un trabajo monótono que lleva unas horas, pero es la única parte que no puede automatizarse, y todo lo demás depende de ella. Algunos hábitos ayudan:
- Distribuya las consultas en diferentes categorías temáticas en lugar de agruparlas alrededor de un único documento.
- Agregue algunas consultas que no deberían devolver ningún resultado, ya que los ejemplos negativos permiten detectar recuperaciones falsamente positivas.
- Actualice el conjunto dorado cuando el corpus cambie de manera sustancial, no de forma continua. Se trata de una referencia estable y no de un conjunto de datos en tiempo real.
La estructura del proyecto y el script principal
El repositorio está organizado de la siguiente manera:
rag-retrieval-audit/
├── audit/
│ ├── __init__.py # Package exports
│ ├── rag_audit.py # Main audit logic — all three checks
│ └── config.py # All thresholds and model settings
├── examples/
│ ├── golden_queries.json # Sample golden set (10 queries)
│ └── run_audit.py # End-to-end demo — runs without an existing collection
├── tests/
│ └── test_audit.py # Unit tests for all scoring functions
├── requirements.txt # sentence-transformers, chromadb, scipy, numpy
└── README.md # Setup, usage, how to read the report
Y aquí está la estructura de rag_audit.py, el archivo que contiene las tres verificaciones. Se trata de un esquema orientativo y no de la implementación completa.
# rag_audit.py — structure overview
# Full implementation: https://github.com/satyam671/rag-retrieval-audit
# ── CONFIGURATION (tune to your pipeline)
EMBEDDING_MODEL = "all-MiniLM-L6-v2" # must match your index
TOP_K = 5
RELEVANCE_THRESHOLD = 0.70
DRIFT_P_THRESHOLD = 0.05
EFFICIENCY_THRESHOLD = 0.40
# ── CHECK 1: Are the right chunks coming back?
def score_relevance(query_embedding, chunk_embeddings, expected_ids, retrieved_ids):
"""Cosine similarity per chunk + expected chunk hit/miss against golden set."""
...
# ── CHECK 2: Has retrieval quality shifted over time?
def detect_drift(current_sims, baseline_path=None):
"""Two-sample KS test comparing current similarity distribution to baseline."""
...
# ── CHECK 3: How much of the context window is signal?
def score_efficiency(retrieved_docs, answer):
"""Token overlap between retrieved chunks and the LLM answer."""
...
# ── RUNNER
def run_audit(golden_set_path, collection_name, chroma_persist_dir,
baseline_path=None, answers_path=None, output_path="rag_audit_report.json"):
"""Runs all three checks, writes a structured JSON report, saves the baseline."""
...
La página README del script explica cómo clonarlo, dirigirlo hacia una colección de ChromaDB y ejecutarlo, incluyendo cómo reemplazarlo por otro almacén de vectores.
Revisión detallada de cada verificación
Evaluación de relevancia por bloques
La verificación de relevancia realiza dos acciones que la gente suele pasar por alto al crearla por sí misma.
Primero, calcula la similitud coseno directamente a partir de las representaciones vectoriales y no utiliza los valores de distancia que devuelve ChromaDB. La métrica predeterminada de ChromaDB es L2, no la similitud coseno, y las dos no son intercambiables. Mezclarlas genera puntuaciones que parecen razonables pero miden en realidad otra cosa.
En segundo lugar, confirma que los IDs de los fragmentos esperados del conjunto dorado aparecieron en el top-K, y eso es lo que revela el envenenamiento de fragmentos. Una similitud promedio de 0.75 parece aceptable hasta que se descubre que el fragmento destinado a responder a la consulta obtuvo una puntuación de 0.62 y quedó en sexto lugar, justo por encima del umbral. El modelo nunca obtuvo la respuesta correcta, aunque las cifras de similitud seguían pareciendo aceptables.
La diferencia práctica entre aplicar y no aplicar el mecanismo de control está clara en el código.
# WITHOUT the relevance gate — standard approach most teams use
def build_context_naive(query, collection, top_k=5):
results = collection.query(
query_embeddings=[embed(query)],
n_results=top_k,
include=["documents"]
)
# Pass everything back, no quality check
return "\n\n".join(results["documents"][0])
# WITH the relevance gate - what the audit tells you to build
def build_context_gated(query, collection, model, top_k=5, threshold=0.70):
q_emb = model.encode([query], normalize_embeddings=True)[0]
results = collection.query(
query_embeddings=[q_emb.tolist()],
n_results=top_k,
include=["documents", "embeddings", "ids"]
)
passed_chunks = []
for i, chunk_emb in enumerate(results["embeddings"][0]):
sim = cosine_sim(q_emb, np.array(chunk_emb))
if sim >= threshold:
passed_chunks.append(results["documents"][0][i])
# Empty context is better than wrong context
return "\n\n".join(passed_chunks)
En la versión ingenua, los fragmentos mejor clasificados pasan sin importar cuán buenos sean. En la versión con control, cualquier fragmento que no haya ganado su lugar es retenido. Una respuesta como “No tengo suficiente información para responder a eso” desde un contexto vacío es mejor que una respuesta errónea pero segura basada en ruido.
Detección de deriva en los embeddings
La verificación de deriva se basa en la prueba de Kolmogorov-Smirnov. Compara la forma de la distribución de similitud actual con una línea de referencia guardada y marca cualquier divergencia significativa. ¿Por qué distribuciones y no puntuaciones promedio? Porque un solo número oculta demasiada información. El promedio podría mantenerse estable en 0.78 mientras la distribución subyacente se aplana y se expande, lo que significa que algunas consultas empeoran mucho mientras otras compensan. La prueba KS detecta cambios en las distribuciones que las medias promedio pasan por alto.
El gráfico de ejemplo muestra la deriva en el incrustamiento en un sistema RAG en producción seis meses después de las actualizaciones del corpus sin realizar un nuevo índice: una estadística KS de 0.21 y un valor p de 0.003.
Una advertencia: la prueba necesita suficientes datos para ser fiable. Con 15 consultas los valores p varían mucho y generan una confianza falsa, mientras que 50 o más proporcionan una señal estable. Esa es otra razón por la cual el tamaño del conjunto de referencia es más importante de lo que parece.
Eficiencia de la ventana de contexto
Este es el más riguroso de los tres análisis, con límites reales. La superposición a nivel de palabra entre los fragmentos recuperados y la respuesta del modelo es solo un indicador aproximado. Cuando un modelo parafrasea en gran medida, como lo hacen los modelos de clase GPT-4, la superposición subestima cuánto del contexto se utilizó realmente.
Aun así, detecta problemas sistemáticos. Si las puntuaciones de eficiencia se mantienen consistentemente por debajo del 40 %, algo en la etapa anterior está mal: los fragmentos son demasiado grandes, el valor de top-K es demasiado alto, o la recuperación incluye texto relacionado con el tema que no aborda la consulta. La verificación no es precisa, pero apunta en la dirección correcta, y eso es suficiente para iniciar una investigación.
Levantamiento del informe
El resultado es un archivo JSON, y estos campos merecen atención:
flag_ratees la fracción de consultas que provocaron una condición de fallo. Un valor superior al 20 % indica que la recuperación necesita atención, y un aumento de 8 % una semana a 22 % la siguiente significa que algo ha cambiado.drift_analysis.drift_detectedes el valor booleano a vigilar. Si estrue, revisemean_deltapara determinar la dirección del cambio. Un delta negativo significa que la similitud está disminuyendo, lo cual es la señal para volver a indexar.drift_analysis.ks_statisticaumenta con la magnitud del desplazamiento de la distribución. Un valor inferior a 0.10 probablemente sea ruido. Un valor superior a 0.20 indica un problema real, independientemente del valor de p.query_results[i].relevance.expected_chunk_hitrepresenta la verdad de referencia para cada consulta. Un valorfalsesignifica que el fragmento que debería haber respondido a la consulta no formó parte del top-K. Si esto ocurre en más del 15 % del conjunto de referencia, realice un reindexado.
El ejemplo se ejecuta con una sola orden, como se muestra aquí.
python -m examples.run_audit
Una hoja de referencia resume la auditoría en una sola imagen.
Ejecútela semanalmente y conserva todas las líneas de referencia. La tendencia entre las ejecuciones dice más que cualquier ejecución individual.
Una lección sobre el tamaño de la muestra
Al realizar la detección de desviaciones con un conjunto de referencia de 15 consultas, se obtuvieron valores p que no tenían sentido. La prueba se ejecutaba técnicamente, pero de forma poco fiable, y era fácil perder dos días sospechando de la implementación estadística antes de darse cuenta de que los datos eran simplemente insuficientes.
Con menos de 50 consultas, las cifras relativas a las desviaciones no son fiables, por lo que primero se debe crear el conjunto y luego activar la verificación de desviaciones. Un corpus que abarca varios dominios temáticos separados requiere un conjunto de referencia por dominio, auditado por separado, ya que una distribución en un corpus mixto puede ocultar una disminución en un único dominio que sería evidente si se analizara por separado.
Límites de este enfoque
El script verifica que se recuperen los fragmentos adecuados. No evalúa si los límites de los fragmentos están bien colocados, si su tamaño es adecuado para los tipos de consulta, ni si la segmentación semántica debería reemplazar la división por tamaño fijo.
Esas cuestiones son importantes. Las pruebas de NVIDIA en cinco conjuntos de datos, resumidas en un análisis de FloTorch, revelaron que el mejor tamaño de fragmento varía según el tipo de consulta: las consultas de tipo factual alcanzan un máximo de 256 a 512 tokens, mientras que las analíticas requieren 1024 o más. Cualquier tamaño de fragmento único representa un compromiso, y un conjunto óptimo que combine ambos tipos de consulta termina generando una media que refleja una falta de adecuación básica. Probar directamente la estrategia de segmentación y comparar la segmentación semántica con la división por tamaño fijo utilizando cifras reales es un ejercicio aparte.
Referencias
- El artículo RAGAS sobre la evaluación automática de la generación mejorada por recuperación de información (arXiv 2309.15217, 2023; EACL 2024).
- El estudio “perdido en el medio” sobre el comportamiento en contextos largos en los modelos de lenguaje, publicado en el volumen 12 de TACL en 2024.
- La investigación aplicada de NVIDIA sobre estrategias de fragmentación en diferentes tipos de documentos, desde 2024.