Cinq techniques de reranking pour RAG : de la récupération rapide à un contexte précis.
Pourquoi la récupération en première étape échoue à trouver des réponses et comment y remédier : encodeurs croisés, RRF, Cohere Rerank, ColBERT et LLM-as-a-judge, ainsi qu’un pipeline en plusieurs étapes pour commencer.
Sources: **5 Reranking Techniques in RAG: From Fast Retrieval to Accurate Context**
Imaginez un pipeline RAG correctement conçu : des documents fragmentés, un modèle d’embedding choisi, une base de données vectorielle intégrée. Une question arrive, le récupérateur renvoie 50 fragments, et les 5 meilleurs sont inclus dans le prompt. La réponse est acceptable, mais pas exceptionnelle. Parfois, elle est complètement manquante.
En examinant ce qui a été récupéré, on constate souvent que le fragment contenant la réponse se trouve à la position 23. Le modèle d’embedding l’a jugé modérément pertinent, mais pas suffisamment pour figurer parmi les 5 meilleurs, ce qui explique pourquoi le LLM ne l’a jamais vu. Ce n’est pas un cas rare : c’est l’un des modes de défaillance les plus fréquents dans les systèmes RAG en production.
Le récupérateur n’est généralement pas la cause du problème. Ce qui se passe après la récupération l’est.
Le goulot d’étranglement après la récupération
La récupération en première étape est optimisée pour la vitesse. Que ce soient des vecteurs denses, un algorithme BM25 spars ou une combinaison des deux, l’objectif est de réduire un million de documents à un ensemble utilisable en quelques millisecondes. Le prix à payer est un jugement rapide et superficiel de la pertinence : le modèle examine la requête et chaque document séparément, les évalue et renvoie celui qui est le plus proche.
« Le plus proche » et « le plus utile pour répondre à cette question » ne sont pas la même chose. La récupération initiale rencontre des difficultés pour quatre raisons :
- Évaluation superficielle. Les embeddings mesurent la similarité, pas l’utilité ; ainsi, un fragment peut se trouver près de la requête sans contenir d’informations utiles.
- Aucune interaction entre requête et document. Les bi-encodeurs encodent la requête et les documents séparément sans jamais les voir ensemble, ce qui cache des relations subtiles.
Le réclassement est la solution.
Quel est le rôle du réclassement
Le réclassement correspond à une deuxième étape. Après que la recherche rapide et large ait fourni un ensemble de candidats, un modèle plus puissant les réordonne en fonction de leur adéquation réelle avec la requête spécifique. On peut l’imaginer comme un contrôle de qualité avant que le contexte n’atteigne le LLM.
Le processus comporte trois étapes :
- Récupération initiale : une recherche rapide et étendue fournit de 50 à 100 fragments candidats.
- Réclassement : un modèle plus intelligent évalue chaque candidat par rapport à la requête et réordonne la liste.
On obtient ainsi la rapidité d’une recherche approximative associée à la précision d’une évaluation approfondie. Le système de récupération n’est pas remplacé, mais simplement complété.
Récupération et réclassement en parallèle
| 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 |
L’idée est d’éviter de consacrer des ressources de calcul importantes à un million de documents pour les concentrer sur les 50 les plus prometteurs.
Cinq techniques de réclassement
Les outils de réclassement vont des solutions peu coûteuses et rapides aux méthodes plus gourmandes en ressources mais plus performantes. Le choix dépend de votre budget en termes de latence, de vos exigences en matière de précision et de vos contraintes opérationnelles. Ces cinq techniques couvrent toute cette gamme.
1. Réclassement avec encodeur croisé
Les encodeurs croisés constituent la référence en termes de précision. Un bi-encodeur encode séparément la requête et le document, tandis qu’un encodeur croisé les traite ensemble comme une seule entrée. Cela lui permet de détecter des interactions au niveau des tokens, telles que la résolution des pronoms, les négations et les relations implicites, que les outils de récupération manquent complètement.
Les étapes :
- L’outil de récupération renvoie 50 fragments candidats.
- Pour chaque fragment, on combine la requête et le fragment sous la forme
[CLS] requête [SEP] fragment [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}")
Choisissez des encodeurs croisés lorsque la précision est primordiale et qu’une augmentation de 50 à 200 ms supplémentaires est acceptable. Pour la plupart des systèmes RAG en production, c’est le meilleur point de départ. La limite est que chaque paire requête-document est traitée séparément, ce qui fait que la latence augmente en fonction du nombre de candidats.
2. Fusion du rang réciproque (RRF)
Supposons que vous préfériez ne pas utiliser du tout de modèle neuronal. La Fusion du rang réciproque combine les classements provenant de plusieurs récupérateurs à l’aide d’une formule déterministe : pas d’entraînement, pas d’inférence, uniquement des calculs arithmétiques.
La RRF convient le mieux lorsque des méthodes de récupération hybrides sont déjà en place, comme une recherche vectorielle dense associée à une recherche par mots-clés BM25. Chaque récupérateur fournit son propre classement, et la RRF les fusionne en une liste améliorée.
La formule :
RRF_score(d) = somme sur les récupérateurs de 1 / (k + rank_r(d))
Ici, k est une constante, généralement égale à 60, et rank_r(d) représente la position du document d dans le moteur de recherche r. Les documents bien classés par plusieurs moteurs de recherche atteignent les premières positions.
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 est l’option la plus rapide ici, avec pratiquement aucune latence supplémentaire. L’utilisez lorsque vous utilisez déjà une recherche hybride et que vous souhaitez améliorer les résultats sans avoir recours à l’inférence de modèles. Il ne surpassera pas un encodeur croisé en termes de précision brute, mais il fonctionne de manière surprenante bien pour une solution ne reposant pas sur l’apprentissage automatique.
3. API Cohere Rerank
Certaines équipes ne souhaitent pas héberger ni ajuster un modèle de reranking. Si votre priorité est le produit plutôt que les opérations liées aux modèles, une API de reranking gérée est attrayante, et celle de Cohere est la plus établie dans ce domaine.
L’API accepte une requête ainsi qu’une liste de documents et les renvoie dans l’ordre de pertinence. Derrière elle fonctionne un modèle de reranking moderne doté d’optimisations qui prendraient des semaines à développer soi-même. Il gère le contenu multilingue et les documents longs, et s’adapte en fonction de la demande.
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}")
Choisissez-le lorsque vous souhaitez une grande précision sans charge opérationnelle importante. Les compromis consistent en un coût par appel et une dépendance externe ; de plus, pour un trafic très élevé, la latence des appels réseau peut être un facteur déterminant. Pour la plupart des équipes, le temps gagné grâce à l’hébergement et à l’optimisation compense ces coûts.
4. ColBERT
Les encodateurs croisés sont précis mais lents, tandis que les bi-encodeurs sont rapides mais peu profonds. ColBERT trouve un équilibre grâce à la « interaction tardive ».
- Hors ligne : chaque document est encodé token par token en embeddings contextuels, qui sont précalculés et stockés dans un index.
- En ligne : lorsqu’une requête arrive, elle fait l’objet du même traitement au niveau des tokens.
- Évaluation : chaque token de la requête est comparé au token du document le plus proche (étape MaxSim), puis ces valeurs maximales sont additionnées pour obtenir le score de pertinence.
Comme les embeddings de documents sont précalculés, cela est beaucoup plus rapide qu’un cross-encoder. Étant donné qu’il conserve les interactions au niveau des tokens, il surpasse les bi-encoders en termes de précision.
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 convient aux systèmes à grande échelle qui nécessitent une précision supérieure à celle des bi-encoders et une latence inférieure à celle des cross-encoders. Le coût réside dans la création et la maintenance de l’index. Si vous maîtrisez les bases de données vectorielles et souhaitez une meilleure qualité sans latence croissante en fonction du nombre de candidats, c’est la solution idéale.
5. LLM en tant que juge
Dans les domaines à haut risque, tels que le diagnostic médical, la recherche juridique et la conformité financière, même un cross-encoder peut se révéler insuffisant. Les requêtes sont complexes, le contexte subtil et une réponse erronée a de lourdes conséquences. C’est là que le reranker le plus précis peut être le LLM lui-même.
L’idée consiste à inciter un grand modèle (GPT-4, Claude ou Gemini) à évaluer le degré de pertinence de chaque candidat par rapport à la requête. Grâce à ses vastes connaissances et à sa forte capacité de raisonnement, il peut juger de la pertinence de manières impossibles pour des rerankers plus petits. Un exemple de prompt typique :
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:
Exécutez-le pour chaque candidat, analysez les scores et retournez les k meilleurs.
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}")
C’est la technique la plus précise ici, mais aussi la plus lente et la plus coûteuse. Utilisez-la pour des requêtes complexes à haute valeur, où une réponse erronée entraîne des pertes financières réelles ou des risques importants. Pour la plupart des applications, les cross-encoders ou les API gérées offrent environ 90 % de la précision à 10 % du coût (c’est une estimation approximative, pas un critère de référence). Réservez les juges basés sur des LLM aux cas qui le justifient vraiment.
Choix entre eux
Aucun outil de reranking ne gagne partout. Ce tableau résume les contraintes :
| 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 judicieux consiste à commencer par un cross-encoder, car il offre la plus grande amélioration en termes de précision avec le moins de complexité. Si la latence devient un problème, envisagez ColBERT ou une API gérée comme Cohere. Ajoutez RRF si vous utilisez un système de récupération hybride, et réservez l’LLM en tant que juge pour les requêtes où la précision maximale justifie le coût.
Les équipes ont rarement besoin de choisir uniquement l’un d’eux. Une architecture de production courante se présente comme suit :
- Récupération hybride (dense plus BM25) avec fusion RRF.
- Réclassement par un cross-encoder sur les 50 meilleurs candidats.
- Utilisation d’un LLM en tant que juge en cas de requêtes à haut risque identifiées.
Cela assure de la vitesse dans les cas ordinaires et une précision essentielle lorsque c’est crucial.
Pensées finales
Le réclassement est probablement la partie la plus sous-estimée du RAG de production. Les équipes passent des mois à ajuster la taille des blocs et les modèles d’incorporation, tandis que leur meilleur contexte reste enfoui à la position 23 ; un bon outil de réclassement peut y remédier avec peu d’efforts techniques.
Les techniques vont de celles qui sont gratuites et rapides (RRF) à celles qui sont coûteuses mais puissantes (LLM-as-a-Judge), et le réclassement n’a pas besoin d’être soit tout, soit rien. Commencez par quelque chose de simple, ajoutez progressivement des méthodes et n’augmentez la complexité que lorsque vos métriques l’exigent.
- La récupération en première étape permet de collecter rapidement des candidats ; elle ne se contente pas de choisir des réponses.
- Le réclassement sert de contrôle qualité, une seconde passe qui évalue la véritable pertinence.
- Les encodeurs croisés constituent le meilleur point de départ, offrant une haute précision et une complexité modérée.
- RRF offre des avantages gratuits lorsque vous utilisez déjà une récupération hybride.
Si vous exécutez RAG en production sans réclassement, la qualité des réponses est compromise. Choisissez une technique, mettez-la en œuvre cette semaine et mesurez la différence.