Cinco técnicas de reclasificación para RAG: desde la recuperación rápida hasta el contexto preciso.
Por qué la recuperación en la primera etapa no encuentra respuestas y cómo solucionarlo: codificadores cruzados, RRF, Cohere Rerank, ColBERT y LLM-as-a-judge, además de un pipeline en capas para comenzar.
Fuentes: **5 Reranking Techniques in RAG: From Fast Retrieval to Accurate Context**
Imagínese un pipeline RAG construido correctamente: los documentos se dividen en fragmentos, se elige un modelo de embedding y se conecta una base de datos vectorial. Llega una pregunta, el recuperador devuelve 50 fragmentos y los 5 mejores se incluyen en la prompt. La respuesta es aceptable, pero no excelente; ocasionalmente falla por completo.
Si examina los fragmentos recuperados, a menudo encontrará que el que contiene la respuesta está en la posición 23. El modelo de embedding lo calificó como moderadamente relevante, pero no como uno de los 5 más relevantes, por lo que el LLM nunca lo vio. Esto no es un caso excepcional; es uno de los modos de fallo más comunes en los sistemas RAG en producción.
Suele ser el proceso de recuperación y no el recuperador mismo el culpable. Lo que ocurre después de la recuperación es lo problemático.
El cuello de botella después de la recuperación
La recuperación en la primera etapa está optimizada para la velocidad. Con vectores densos, BM25 disperso o una combinación de ambos, el objetivo es reducir un millón de documentos a un conjunto manejable en cuestión de milisegundos. El precio que se paga es un juicio rápido y superficial sobre la relevancia: el modelo analiza la consulta y cada documento por separado, los califica y devuelve el más cercano.
“El más cercano” y “el más útil para responder a esta pregunta” son cosas diferentes. La recuperación inicial tropieza por cuatro razones:
- Evaluación superficial. Los embeddings miden la similitud, no la utilidad, por lo que un fragmento puede estar cerca de la consulta y aún así no contener nada útil.
- Falta de interacción consulta-documento. Los bi-encoders codifican la consulta y los documentos por separado y nunca los ven juntos, lo que oculta relaciones sutiles.
La solución es el reclasificación.
Qué hace la reclasificación
La reclasificación es un segundo paso. Después de que una búsqueda rápida y amplia genere un conjunto de candidatos, un modelo más potente los reordena según cuán bien se ajustan realmente a la consulta específica. Piénselo como un control de calidad antes de que el contexto llegue al LLM.
El proceso consta de tres pasos:
- Búsqueda inicial: una búsqueda rápida y amplia devuelve de 50 a 100 fragmentos candidatos.
- Reclassificación: un modelo más inteligente evalúa cada candidato en relación con la consulta y reordena la lista.
Se obtiene la velocidad de la búsqueda aproximada junto con la precisión de la puntuación detallada. El recuperador no se reemplaza, solo se complementa.
Recuperación y reclasificación lado a lado
| Aspect | Initial Retrieval | Reranking |
| ------------------- | ------------------------ | ----------------------------- |
| Goal | Find candidates fast | Judge true relevance |
| Speed | Milliseconds | Tens to hundreds of milliseconds |
| Input | Query + index | Query + top-k candidates |
| Scoring depth | Shallow (embedding dot product) | Deep (cross-attention, token interaction) |
| Cost | Low (local compute) | Higher (model inference) |
| When to use | Every query | On top-k candidates only |
La idea es evitar gastar muchos recursos de procesamiento en un millón de documentos y destinarlos a los 50 más prometedores.
Cinco técnicas de reclasificación
Los sistemas de reclasificación van desde los económicos y rápidos hasta los complejos y potentes. Elige el adecuado según tu presupuesto de latencia, tus necesidades de precisión y tus limitaciones operativas. Estas cinco técnicas abarcan ese rango.
1. Reclasificación con codificador cruzado
Los codificadores cruzados son el estándar de precisión. Un bi-codificador incrusta por separado la consulta y el documento, mientras que un codificador cruzado los lee juntos como una sola entrada. Esto le permite capturar interacciones a nivel de token, como la resolución de pronombres, las negaciones y las relaciones implícitas, que los sistemas de recuperación pasan por alto completamente.
Los pasos:
- El sistema de recuperación devuelve 50 fragmentos candidatos.
- Para cada fragmento, combina la consulta y el fragmento como
[CLS] consulta [SEP] fragmento [SEP].
cross-encoder/ms-marco-MiniLM-L-6-v2.from sentence_transformers import CrossEncoder
# Load a cross-encoder reranker
# ms-marco-MiniLM-L-6-v2 is fast and accurate for general use
cross_encoder = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
def rerank_with_cross_encoder(query: str, retrieved_docs: list[str], top_k: int = 5):
"""
Rerank retrieved documents using a cross-encoder.
Args:
query: The user question
retrieved_docs: List of document chunks from initial retrieval
top_k: Number of documents to return after reranking
Returns:
List of (document, score) tuples, sorted by relevance
"""
# Create query-document pairs
pairs = [[query, doc] for doc in retrieved_docs]
# Get relevance scores
scores = cross_encoder.predict(pairs)
# Combine docs with scores and sort
scored_docs = list(zip(retrieved_docs, scores))
scored_docs.sort(key=lambda x: x[1], reverse=True)
return scored_docs[:top_k]
# Example usage
query = "What are the side effects of amoxicillin?"
retrieved = [
"Amoxicillin is a penicillin antibiotic used to treat bacterial infections.",
"Common side effects include nausea, vomiting, and diarrhea.",
"The drug was first discovered in 1958 by researchers at Beecham.",
"Patients with penicillin allergies should avoid amoxicillin.",
"Side effects may include rash, itching, and in rare cases, anaphylaxis.",
]
top_docs = rerank_with_cross_encoder(query, retrieved, top_k=3)
for doc, score in top_docs:
print(f"Score: {score:.4f} | {doc}")
Elija los codificadores cruzados cuando la precisión sea lo más importante y se pueda aceptar un retraso adicional de 50 a 200 ms. Para la mayoría de los sistemas RAG en producción, este es el mejor punto de partida. La limitación es que cada par consulta-documento se procesa por separado, por lo que la latencia aumenta en proporción al número de candidatos.
2. Fusión de rangos recíprocos (RRF)
Supongamos que prefiere no utilizar ningún modelo neuronal. La Fusión de rangos recíprocos combina las clasificaciones de varios recuperadores mediante una fórmula determinística: sin entrenamiento, sin inferencia, solo cálculos aritméticos.
RRF es la opción más adecuada cuando ya se cuenta con una recuperación híbrida, como la búsqueda vectorial densa junto con la búsqueda por palabras clave BM25. Cada recuperador proporciona su propia clasificación, y RRF las combina en una lista mejor.
La fórmula:
RRF_score(d) = suma sobre los recuperadores de 1 / (k + rank_r(d))
Aquí k es una constante, generalmente 60, y rank_r(d) representa la posición del documento d en el recuperador r. Los documentos que obtienen buenas calificaciones de varios recuperadores ocupan los primeros puestos.
def reciprocal_rank_fusion(rankings: list[list[str]], k: int = 60) -> list[tuple[str, float]]:
"""
Merge multiple document rankings using Reciprocal Rank Fusion.
Args:
rankings: List of rankings, where each ranking is a list of document IDs
ordered from most to least relevant
k: RRF constant (default 60, as recommended in the original paper)
Returns:
List of (document_id, rrf_score) tuples, sorted by fused score
"""
scores = {}
for ranking in rankings:
for rank, doc_id in enumerate(ranking, start=1):
if doc_id not in scores:
scores[doc_id] = 0.0
# RRF formula: 1 / (k + rank)
scores[doc_id] += 1.0 / (k + rank)
# Sort by score descending
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
# Example: merging BM25 and vector search results
bm25_results = ["doc_5", "doc_2", "doc_8", "doc_1", "doc_9"]
vector_results = ["doc_1", "doc_5", "doc_3", "doc_8", "doc_7"]
fused = reciprocal_rank_fusion([bm25_results, vector_results])
print("Fused ranking:")
for doc_id, score in fused:
print(f" {doc_id}: {score:.4f}")
# Notice: doc_5 and doc_1 appear in both retrievers and get boosted to the top
RRF es la opción más rápida aquí, con prácticamente cero latencia adicional. Úsala cuando ya utilices recuperación híbrida y quieras una mejora económica sin necesidad de inferencia de modelos. No superará a un codificador cruzado en términos de precisión bruta, pero funciona sorprendentemente bien para algo que no incluye aprendizaje automático.
3. API de reclasificación Cohere
Algunos equipos no desean alojar ni ajustar un modelo de reclasificación. Si tu prioridad es el producto y no las operaciones del modelo, una API de reclasificación gestionada resulta atractiva, y la de Cohere es la más consolidada en este ámbito.
La API acepta una consulta junto con una lista de documentos y los devuelve en orden de relevancia. Detrás de ella funciona un modelo moderno de reclasificación con optimizaciones que tomarían semanas construir por uno mismo. Maneja contenido multilingüe y documentos largos, además de escalar según sea necesario.
import cohere
from dotenv import load_dotenv
import os
load_dotenv()
# Initialize Cohere client
co = cohere.Client(os.getenv("COHERE_API_KEY"))
def rerank_with_cohere(query: str, documents: list[str], top_k: int = 5):
"""
Rerank documents using Cohere's managed Rerank API.
Args:
query: The user question
documents: List of document chunks from initial retrieval
top_k: Number of documents to return
Returns:
List of (document, relevance_score) tuples
"""
response = co.rerank(
model="rerank-v3.5",
query=query,
documents=documents,
top_n=top_k,
return_documents=True
)
results = []
for result in response.results:
results.append((
result.document.text,
result.relevance_score
))
return results
# Example usage
query = "How do I handle authentication in a FastAPI app?"
docs = [
"FastAPI is a modern web framework for building APIs with Python.",
"To add authentication, use OAuth2PasswordBearer and JWT tokens.",
"Pydantic models in FastAPI provide automatic request validation.",
"The OAuth2PasswordBearer class expects a token URL endpoint.",
"FastAPI was created by Sebastián Ramírez and released in 2018.",
]
ranked = rerank_with_cohere(query, docs, top_k=3)
for doc, score in ranked:
print(f"Score: {score:.4f} | {doc}")
Elija esta opción cuando desee una alta precisión sin carga operativa. Las desventajas son el costo por llamada y la dependencia externa; además, en casos de tráfico muy alto, la latencia de las llamadas a la red puede ser un factor importante. Para la mayoría de los equipos, el tiempo ahorrado en alojamiento y optimización supera esos costos.
4. ColBERT
Los codificadores cruzados son precisos pero lentos, mientras que los bi-codificadores son rápidos pero superficiales. ColBERT encuentra un camino intermedio a través de la “interacción tardía”.
- Offline: cada documento se codifica token por token en embeddings contextuales, los cuales se calculan previamente y se almacenan en un índice.
- Online: cuando llega una consulta, se le aplica el mismo proceso a nivel de tokens.
- Evaluación: cada token de la consulta se compara con el token del documento más similar (paso MaxSim), y luego se suman esas similitudes máximas para obtener la puntuación de relevancia.
Dado que los embeddings de documentos se calculan por adelantado, esto es mucho más rápido que un cross-encoder. Al conservar las interacciones a nivel de token, supera a los bi-encoders en precisión.
from colbert import Searcher
from colbert.infra import Run, RunConfig
def setup_colbert_searcher(index_path: str, checkpoint: str):
"""
Initialize a ColBERT searcher for late-interaction reranking.
Args:
index_path: Path to the pre-built ColBERT index
checkpoint: Path to the ColBERT model checkpoint
Returns:
Configured Searcher instance
"""
with Run().context(RunConfig(nranks=1, experiment="reranking")):
searcher = Searcher(
index=index_path,
checkpoint=checkpoint
)
return searcher
def rerank_with_colbert(searcher, query: str, doc_ids: list[str], top_k: int = 5):
"""
Rerank documents using ColBERT's late interaction.
Args:
searcher: Initialized ColBERT Searcher
query: The user question
doc_ids: List of document IDs from initial retrieval
top_k: Number of documents to return
Returns:
List of (doc_id, score) tuples
"""
# Search within the candidate set
results = searcher.search(
query,
k=top_k,
filter_fn=lambda pid: pid in doc_ids # Only rerank candidates
)
return list(zip(results[0], results[2])) # doc_ids, scores
# Note: ColBERT requires a pre-built index and model checkpoint.
# For production use, build the index once and load it at startup.
ColBERT es adecuado para sistemas a gran escala que necesitan mayor precisión que los bi-encoders y menor latencia que los cross-encoders. El costo radica en el trabajo de crear y mantener el índice. Si tienes experiencia con bases de datos vectoriales y deseas mejor calidad sin una latencia que crezca de forma lineal con las opciones disponibles, este es el equilibrio ideal.
5. LLM como juez
En áreas de alta importancia, como el diagnóstico médico, la investigación legal y el cumplimiento financiero, incluso un cross-encoder puede resultar insuficiente. Las consultas son complejas, el contexto es sutil y una respuesta incorrecta tiene consecuencias graves. En estos casos, el reordenador más preciso puede ser el propio LLM.
La idea consiste en solicitar a un modelo grande (GPT-4, Claude o Gemini) que evalúe la relevancia de cada opción en relación con la consulta. Gracias a su amplio conocimiento del mundo y su sólido razonamiento, puede determinar la relevancia de maneras que los reordenadores más pequeños no pueden. Un ejemplo típico de prompt:
You are evaluating documents for a retrieval system.
Query: {query}
Document: {document}
Rate how relevant this document is for answering the query.
Respond with a single integer from 1 to 10, where 10 means perfectly relevant.
Relevance score:
Ejecútelo para cada candidato, analiza las puntuaciones y devuelve los k mejores.
from openai import OpenAI
import os
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
def score_document_with_llm(query: str, document: str) -> int:
"""
Ask an LLM to score a document's relevance to a query.
Args:
query: The user question
document: A candidate document chunk
Returns:
Integer relevance score from 1-10
"""
prompt = f"""You are evaluating documents for a retrieval system.
Query: {query}
Document: {document}
Rate how relevant this document is for answering the query.
Respond with a single integer from 1 to 10, where 10 means perfectly relevant.
Be strict: only give high scores to documents that directly help answer the query.
Relevance score:"""
response = client.chat.completions.create(
model="gpt-4.1-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
max_tokens=5
)
try:
score = int(response.choices[0].message.content.strip())
return max(1, min(10, score)) # Clamp to 1-10
except ValueError:
return 5 # Default on parse failure
def rerank_with_llm_judge(query: str, documents: list[str], top_k: int = 3):
"""
Rerank documents using an LLM as a relevance judge.
Args:
query: The user question
documents: List of candidate document chunks
top_k: Number of documents to return
Returns:
List of (document, score) tuples, sorted by relevance
"""
scored = []
for doc in documents:
score = score_document_with_llm(query, doc)
scored.append((doc, score))
scored.sort(key=lambda x: x[1], reverse=True)
return scored[:top_k]
# Example usage
query = "What are the tax implications of RSU vesting for employees in California?"
docs = [
"RSUs are restricted stock units granted to employees as part of compensation.",
"In California, RSU income is taxed as ordinary income at vesting, not at grant.",
"Employers typically withhold federal and state taxes at vesting time.",
"Stock options and RSUs have different tax treatments under IRS rules.",
"California has one of the highest state income tax rates in the US.",
]
ranked = rerank_with_llm_judge(query, docs, top_k=3)
for doc, score in ranked:
print(f"Score: {score}/10 | {doc}")
Esta es la técnica más precisa aquí, pero también la más lenta y costosa. Úsela para consultas complejas y de alto valor en las que una respuesta incorrecta cuesta dinero real o conlleva un riesgo real. Para la mayoría de las aplicaciones, los cross-encoders o las APIs gestionadas ofrecen alrededor del 90% de la precisión a un 10% del costo (una cifra aproximada, no un punto de referencia). Reserve los jueces basados en LLM para los casos que realmente los justifiquen.
Elegir entre ellos
Ningún reordenador gana en todas las situaciones. Esta tabla resume las limitaciones:
| Technique | Best For | Latency | Cost |
| --------------------- | ------------------------------------------------- | ------------ | -------------- |
| Cross-Encoder | Maximum quality on top-k candidates | 50-200ms | Local GPU/CPU |
| RRF | Hybrid retrieval without adding model inference | ~0ms | Free |
| Cohere Rerank API | Speed without operational overhead | 100-300ms | Per API call |
| ColBERT | Large-scale, low-latency use cases | 20-100ms | Index + GPU |
| LLM-as-a-Judge | Complex, high-value queries (medical, legal) | 1-5 seconds | Per API call |
Un plan sensato es comenzar con un cross-encoder, ya que ofrece el mayor aumento en precisión con la menor complejidad. Si la latencia se convierte en un problema, considere ColBERT o una API gestionada como Cohere. Añada RRF si utiliza recuperación híbrida, y reserve al LLM como juez para las consultas en las que la precisión máxima justifique el costo.
Rara vez los equipos tienen que elegir solo uno. Una capa de producción común se estructura de la siguiente manera:
- Recuperación híbrida (densa más BM25) con fusión RRF.
- Reclasificación mediante cross-encoder de los 50 candidatos principales.
- Un LLM como juez como solución de respaldo para las consultas críticas marcadas.
Eso brinda velocidad en los casos normales y precisión cuando es crucial.
Pensamientos finales
El reclasificado es probablemente la parte más subestimada del RAG de producción. Los equipos pasan meses ajustando el tamaño de los fragmentos y los modelos de incrustación, mientras que su mejor contexto queda oculto en la posición 23; un buen reclasificador soluciona esto con poco esfuerzo técnico.
Las técnicas van desde las gratuitas y rápidas (RRF) hasta las costosas y potentes (LLM-as-a-Judge), y el reclasificado no tiene por qué ser algo extremo. Comience de forma sencilla, añada métodos gradualmente y solo incorpore mayor complejidad cuando sus métricas lo exijan.
- La recuperación en la primera etapa reúne candidatos rápidamente; no se limita a seleccionar respuestas.
- El reclasificado funciona como un control de calidad, una segunda pasada que evalúa la verdadera relevancia.
- Los codificadores cruzados constituyen el mejor punto de partida, con alta precisión y complejidad moderada.
- RRF ofrece ventajas sin costo cuando ya se utiliza una recuperación híbrida.
Si ejecutas RAG en producción sin realizar reclasificación, la calidad de las respuestas se verá afectada. Elige una técnica, implementala esta semana y mide la diferencia.