Combina resultados RAG con fusión de rangos recíprocos
En esta página
Un asistente de un centro de ayuda busca instrucciones para recuperar una cuenta. La búsqueda por palabras clave encuentra páginas con los términos exactos; la búsqueda vectorial aporta otro orden. ¿Qué documentos deberían entrar en el contexto de la IA? En vez de sumar puntuaciones de relevancia distintas, combina las dos listas ordenadas mediante fusión de rangos recíprocos (RRF).
Este tutorial construye y prueba esa combinación en Python, sin conexión. No ejecuta un buscador ni genera embeddings: las entradas son clasificaciones escritas a mano, no resultados medidos de un modelo. En un flujo RAG, este paso va después de recuperar documentos y antes de preparar el contexto de la respuesta.
1. Prepara listas ordenadas comparables
Usa Python 3.10 o posterior; no hacen falta paquetes, cuentas ni claves API. Nuestra consulta imaginaria es «¿Cómo recupero el acceso a mi cuenta?». Los identificadores corresponden a cuatro fragmentos: reset contiene instrucciones de restablecimiento, password trata la recuperación de contraseñas, billing contiene facturas y security aborda la protección de cuentas.
Ambos sistemas deben utilizar los mismos identificadores estables de fragmento y la misma versión del corpus, y devolver primero los mejores resultados. La introducción a la recuperación con ModernBERT explica la recuperación semántica; aquí solo necesitamos el orden resultante.
No sumes directamente una puntuación de palabras clave y una similitud vectorial. RRF utiliza posiciones, por lo que sus escalas originales no necesitan coincidir. A cambio, descarta las diferencias entre puntuaciones: una ventaja pequeña y una grande se convierten en la misma diferencia de posición, como explica la documentación de OpenSearch.
2. Combina los rangos localmente
Por cada aparición, suma 1 / (rank_constant + rank) al total de ese identificador. Las posiciones empiezan en uno; un identificador ausente no aporta nada. Esta es la regla del artículo original sobre RRF. Utilizamos su constante de 60, sin dar por hecho que sea óptima en todos los casos.
Guarda lo siguiente como rrf_demo.py. Proporciona una lista de listas con identificadores de texto no vacíos. Un identificador puede aparecer en ambas listas, pero nunca dos veces en una misma lista.
from collections import defaultdict
from fractions import Fraction
def rrf(rankings, rank_constant=60, limit=3):
if type(rank_constant) is not int or rank_constant < 1:
raise ValueError("rank_constant must be a positive integer")
if type(limit) is not int or limit < 1:
raise ValueError("limit must be a positive integer")
scores = defaultdict(Fraction)
for ranking in rankings:
if any(not isinstance(doc_id, str) or not doc_id for doc_id in ranking):
raise ValueError("IDs must be nonempty strings")
if len(set(ranking)) != len(ranking):
raise ValueError("duplicate ID within one ranking")
for rank, doc_id in enumerate(ranking, start=1):
scores[doc_id] += Fraction(1, rank_constant + rank)
return sorted(scores.items(), key=lambda item: (-item[1], item[0]))[:limit]
if __name__ == "__main__":
keyword = ["reset", "billing", "password"]
vector = ["password", "reset", "security"]
for doc_id, score in rrf([keyword, vector]):
print(f"{doc_id}: {float(score):.6f}")
Fraction mantiene la aritmética exacta en esta pequeña demostración. La ordenación usa puntuación descendente y después identificador ascendente para que los empates exactos sean reproducibles. Convierte las puntuaciones a decimales solo para mostrarlas. Una puntuación de fusión sirve para ordenar; no es la probabilidad de que un documento sea correcto.
3. Comprueba la aritmética y los casos límite
Guarda lo siguiente como test_rrf_demo.py, junto al primer archivo. Comprueba el orden completo de los tres primeros resultados, una puntuación calculada a mano, entradas vacías, empates deterministas y el rechazo de duplicados.
from fractions import Fraction
from rrf_demo import rrf
rankings = [
["reset", "billing", "password"],
["password", "reset", "security"],
]
fused = rrf(rankings)
assert [doc_id for doc_id, _ in fused] == ["reset", "password", "billing"]
assert fused[0][1] == Fraction(1, 61) + Fraction(1, 62)
assert rrf([[], []]) == []
assert [doc_id for doc_id, _ in rrf([["z"], ["a"]])] == ["a", "z"]
try:
rrf([["reset", "reset"]])
except ValueError as error:
assert str(error) == "duplicate ID within one ranking"
else:
raise AssertionError("duplicate was accepted")
print("PASS: ranking, arithmetic, empty inputs, ties, duplicate guard")
Ejecuta ambos archivos desde ese directorio:
python3 -B rrf_demo.py
python3 -B test_rrf_demo.py
Salida estándar esperada, reproducida en las pruebas locales:
reset: 0.032522
password: 0.032266
billing: 0.016129
PASS: ranking, arithmetic, empty inputs, ties, duplicate guard
Ambos comandos terminan con estado 0 y sin salida de error estándar. reset queda primero porque aporta 1/61 + 1/62; le sigue password con 1/63 + 1/61. Un duplicado dentro de una lista provoca un ValueError, que la prueba captura deliberadamente. Las listas vacías devuelven un resultado vacío, no documentos inventados.
Estas comprobaciones establecen el comportamiento del código, no la calidad de recuperación. Si las etiquetas humanas consideran relevantes reset y password, Recall@2 es 0,5 para palabras clave, 1,0 para vectores y 1,0 para la fusión. Aquí la fusión iguala, pero no supera, la mejor entrada. Usa Recall@k y MRR con consultas reservadas para evaluación antes de cambiar un sistema real.
4. Conecta la combinación con búsquedas reales
Sustituye las listas de ejemplo por identificadores devueltos para la misma consulta y aplica los mismos filtros de acceso y metadatos a ambas búsquedas. Recupera una ventana de candidatos de cada una, combínalas y selecciona después los fragmentos finales del contexto. Nunca utilices la fusión para eludir permisos de las fuentes.
La profundidad de candidatos y el número de resultados finales son controles distintos. La referencia de RRF de Elasticsearch distingue rank_window_size del tamaño final y actualmente usa 60 como constante predeterminada. Nuestro limit solo controla el número final; las listas proporcionadas definen las ventanas de candidatos. La fusión no puede recuperar un fragmento ausente de todas las entradas.
5. Resuelve clasificaciones inesperadas
- Una fuente aparece dos veces: unifica los identificadores de fragmento entre los sistemas. No combines solo por título: distintos fragmentos pueden compartirlo.
- Los resultados cambian entre ejecuciones: fija la versión del corpus, el orden de entrada, la profundidad de candidatos y la constante. Este desempate no estabiliza entradas cambiantes.
- Un fragmento irrelevante queda muy arriba: la coincidencia no demuestra relevancia. Revisa ambas entradas y las etiquetas humanas antes de ajustar parámetros.
- Un sistema no devuelve nada: este código utiliza la lista restante. Registra la entrada ausente y distingue entre falta de coincidencias y un fallo del servicio.
Conclusión práctica
Usa RRF para combinar evidencia ordenada sin fingir que las puntuaciones de distintos buscadores son comparables. Conserva identificadores estables, prueba la combinación y compara la clasificación fusionada con cada sistema mediante consultas reales etiquetadas antes de enviarla a un asistente de IA.
Artículos relacionados
Comprueba citas textuales de IA con fuentes locales y Python
Compara versiones de prompts de IA con una prueba de regresión en Python
Cómo evaluar la recuperación RAG con Recall@k y MRR
Comprueba las cifras de resúmenes de IA con una tabla de referencia
Evalúa probabilidades de IA con la puntuación de Brier en Python
Detecta texto repetido entre entrenamiento y prueba antes de evaluar una IA
Revisa planes de renombrado de IA sin cambiar archivos
Comprueba las horas de calendario de IA antes de crear eventos
Comprueba los marcadores de traducciones de IA con Python
Rechaza JSON ambiguo de IA antes de automatizar con Python
Prueba expresiones regulares de IA con casos pequeños en Python
Evalúa la clasificación de incidencias con una matriz de confusión
Últimos artículos
Evalúa campos extraídos por IA con precisión y recall de coincidencia exacta
Comprueba las rutas propuestas por IA antes de escribir con Python
Evita actualizaciones de IA obsoletas con un control de versión en SQLite
Comprueba las dependencias de tareas de IA antes de programarlas con Python
Revisa los cambios JSON de la IA con una lista de campos permitidos
Comprueba las horas de calendario de IA antes de crear eventos
Comprueba citas textuales de IA con fuentes locales y Python
Comprueba las cifras de resúmenes de IA con una tabla de referencia
Elige umbrales de revisión de IA con cobertura y tasas de error
Comprueba los marcadores de traducciones de IA con Python
Compara versiones de prompts de IA con una prueba de regresión en Python