Derniere mise a jour le 14 janvier 2026
Le reranking constitue une etape de raffinement qui reordonne les documents recuperes par un systeme RAG selon leur pertinence reelle pour la requete utilisateur. Contrairement au retrieval initial qui utilise des approximations rapides comme la similarite cosinus sur des embeddings, le reranking applique des modeles plus sophistiques capables d’evaluer finement l’adequation entre une question et un passage candidat.
Le retrieval initial sacrifie la precision pour la vitesse et l’echelle
Les systemes RAG modernes s’appuient sur un retrieval en deux etapes. La premiere etape, souvent appelee “retrieval dense”, encode les documents et la requete dans un espace vectoriel commun puis recupere les k documents les plus proches par similarite cosinus. Cette approche permet de filtrer rapidement des millions de documents en quelques millisecondes grace aux index ANN (Approximate Nearest Neighbors) comme HNSW ou IVF.
Le probleme reside dans le compromis fondamental du retrieval dense. Les embeddings bi-encodeurs encodent la requete et le document independamment avant de comparer leurs representations. Cette independance permet le pre-calcul des embeddings documents mais empeche toute interaction fine entre les tokens de la requete et ceux du document. Un document peut contenir exactement l’information recherchee mais dans une formulation que l’embedding ne capture pas parfaitement.
Prenons un exemple concret. Si un utilisateur demande “Quelles sont les contraintes de temperature pour le stockage du vaccin Pfizer ?”, le retrieval dense va recuperer des documents mentionnant vaccines, stockage, et temperature. Mais il peut aussi remonter des documents sur d’autres vaccins, sur le stockage de medicaments generiques, ou sur des contraintes de temperature dans d’autres contextes. Le top-10 initial contient probablement la reponse, mais pas necessairement en premiere position.
Les recherches sur les benchmarks de retrieval montrent que les modeles bi-encodeurs atteignent leurs meilleures performances sur recall@k avec k relativement eleve, mais que la precision@1 reste souvent insuffisante pour des applications critiques. Le reranking intervient precisement pour corriger ce decalage entre le recall et la precision.
Le reranking reordonne les candidats grace a une evaluation fine de la pertinence
Le reranking opere sur le sous-ensemble de documents recuperes par l’etape initiale. Au lieu de traiter des millions de documents, le reranker n’evalue que quelques dizaines a quelques centaines de candidats. Cette reduction drastique du volume permet d’appliquer des modeles plus couteux mais plus precis.
L’architecture typique d’un pipeline RAG avec reranking procede en trois temps. Le retriever initial recupere un top-k large, typiquement 50 a 100 documents. Le reranker evalue chaque paire (requete, document) et attribue un score de pertinence. Le systeme conserve les top-n documents apres reranking, avec n bien plus petit que k (souvent 3 a 10). Ces documents finaux alimentent le contexte du LLM pour la generation.
from sentence_transformers import CrossEncoder
from typing import List, Tuple
class PipelineRAGReranking:
"""
Pipeline RAG avec etape de reranking entre retrieval et generation.
Le reranking ameliore la precision sans modifier l'index vectoriel.
"""
def __init__(
self,
retriever,
reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2",
top_k_retrieval: int = 50,
top_n_final: int = 5
):
self.retriever = retriever
self.reranker = CrossEncoder(reranker_model)
self.top_k = top_k_retrieval
self.top_n = top_n_final
def recuperer_et_reranker(self, requete: str) -> List[Tuple[str, float]]:
"""
Recupere les documents puis les reordonne par pertinence.
Retourne les top_n documents avec leurs scores de reranking.
"""
# Etape 1 : Retrieval initial large
candidats = self.retriever.search(requete, top_k=self.top_k)
# Etape 2 : Preparation des paires pour le reranking
paires = [(requete, doc["contenu"]) for doc in candidats]
# Etape 3 : Scoring par le cross-encoder
scores = self.reranker.predict(paires)
# Etape 4 : Reordonnancement et selection
candidats_scores = list(zip(candidats, scores))
candidats_scores.sort(key=lambda x: x[1], reverse=True)
resultats = [
(doc["contenu"], score)
for doc, score in candidats_scores[:self.top_n]
]
return resultats
L’avantage du reranking reside dans son decouplage du systeme de retrieval. On peut ameliorer la precision sans reconstruire l’index vectoriel, sans changer le modele d’embedding, sans modifier les chunks existants. Le reranker s’insere comme une couche additionnelle qui filtre et reordonne ce que le retriever a deja trouve.
Les cross-encoders evaluent conjointement la requete et le document
Les cross-encoders representent l’approche la plus directe pour le reranking. Contrairement aux bi-encodeurs qui encodent separement requete et document, le cross-encoder concatene les deux textes et les traite ensemble dans un seul modele transformer. Chaque token de la requete peut “voir” chaque token du document via les mecanismes d’attention.
Cette interaction complete entre requete et document permet une evaluation bien plus fine de la pertinence. Le modele peut detecter des correspondances semantiques subtiles, des negations, des conditions, des nuances que les embeddings independants manquent. La contrepartie est le cout computationnel : chaque paire requete-document necessite un forward pass complet du transformer.
Les modeles cross-encoder entraines sur MS MARCO dominent les benchmarks de reranking depuis plusieurs annees. MS MARCO (Microsoft MAchine Reading COmprehension) contient des millions de paires requete-passage avec des annotations de pertinence. Les modeles comme ms-marco-MiniLM-L-6-v2 offrent un bon compromis entre performance et latence. Les modeles plus larges comme bge-reranker-v2-m3 ou les modeles Cohere Rerank atteignent des scores plus eleves au prix d’une latence superieure.
from transformers import AutoModelForSequenceClassification, AutoTokenizer
import torch
class CrossEncoderReranker:
"""
Cross-encoder pour le reranking de documents RAG.
Evalue chaque paire (requete, document) conjointement.
"""
def __init__(self, model_name: str = "BAAI/bge-reranker-v2-m3"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForSequenceClassification.from_pretrained(model_name)
self.model.eval()
# Detection GPU si disponible
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.model.to(self.device)
def scorer_paires(
self,
requete: str,
documents: List[str],
batch_size: int = 16
) -> List[float]:
"""
Calcule les scores de pertinence pour chaque document.
Traite par batch pour optimiser l'utilisation GPU.
"""
scores = []
for i in range(0, len(documents), batch_size):
batch_docs = documents[i:i + batch_size]
# Tokenisation des paires
inputs = self.tokenizer(
[requete] * len(batch_docs),
batch_docs,
padding=True,
truncation=True,
max_length=512,
return_tensors="pt"
).to(self.device)
with torch.no_grad():
outputs = self.model(**inputs)
# Les logits representent le score de pertinence
batch_scores = outputs.logits.squeeze(-1).cpu().tolist()
if isinstance(batch_scores, float):
batch_scores = [batch_scores]
scores.extend(batch_scores)
return scores
La latence du cross-encoder depend lineairement du nombre de documents a evaluer. Avec 50 documents et un modele MiniLM sur GPU, l’etape de reranking prend typiquement 50-100ms. Sur CPU, ce temps peut monter a plusieurs centaines de millisecondes voire quelques secondes. Cette latence reste acceptable pour la plupart des use cases, mais devient problematique si le top-k initial depasse la centaine de documents.
ColBERT introduit l’interaction tardive pour un meilleur compromis vitesse-qualite
ColBERT (Contextualized Late Interaction over BERT), presente par Khattab et Zaharia dans leur paper arXiv:2004.12832 (2020), propose une architecture intermediaire entre bi-encodeurs et cross-encoders. L’idee centrale : encoder requete et documents separement comme les bi-encodeurs, mais calculer la similarite via une interaction token-a-token plutot qu’une simple similarite cosinus globale.
Dans ColBERT, chaque token de la requete et du document recoit un embedding contextualise par BERT. Au moment du scoring, le systeme calcule la similarite maximale entre chaque token de la requete et tous les tokens du document. Le score final est la somme de ces similarites maximales. Cette “late interaction” permet de capturer des correspondances fines sans le cout d’un forward pass conjoint.
L’avantage de ColBERT est la possibilite de pre-calculer les embeddings des tokens de chaque document. Seuls les embeddings de la requete necessitent un calcul au moment de l’inference. L’interaction token-a-token reste couteuse mais bien moins qu’un cross-encoder complet. ColBERTv2 et les travaux subsequents ont optimise le stockage et les calculs pour rendre cette approche pratique a grande echelle.
import torch
from transformers import AutoModel, AutoTokenizer
class ColBERTReranker:
"""
Reranker base sur l'architecture ColBERT (late interaction).
Compromis entre precision des cross-encoders et vitesse des bi-encodeurs.
"""
def __init__(self, model_name: str = "colbert-ir/colbertv2.0"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModel.from_pretrained(model_name)
self.model.eval()
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.model.to(self.device)
def encoder_texte(self, texte: str, max_length: int = 256) -> torch.Tensor:
"""Encode un texte en embeddings token-level."""
inputs = self.tokenizer(
texte,
return_tensors="pt",
max_length=max_length,
truncation=True,
padding="max_length"
).to(self.device)
with torch.no_grad():
outputs = self.model(**inputs)
# Normaliser les embeddings pour similarite cosinus
embeddings = torch.nn.functional.normalize(
outputs.last_hidden_state,
p=2,
dim=-1
)
return embeddings.squeeze(0) # [seq_len, hidden_dim]
def scorer_late_interaction(
self,
embeddings_requete: torch.Tensor,
embeddings_doc: torch.Tensor
) -> float:
"""
Calcule le score ColBERT via late interaction.
Pour chaque token de la requete, trouve la similarite max avec les tokens du doc.
"""
# Similarite entre tous les tokens [query_len, doc_len]
similarites = torch.matmul(embeddings_requete, embeddings_doc.T)
# MaxSim : pour chaque token requete, prendre le max sur les tokens doc
max_sims = similarites.max(dim=1).values
# Score final = somme des MaxSim
score = max_sims.sum().item()
return score
def reranker_documents(
self,
requete: str,
documents: List[str]
) -> List[Tuple[str, float]]:
"""Reranke une liste de documents pour une requete."""
# Encoder la requete une seule fois
emb_requete = self.encoder_texte(requete)
resultats = []
for doc in documents:
emb_doc = self.encoder_texte(doc)
score = self.scorer_late_interaction(emb_requete, emb_doc)
resultats.append((doc, score))
resultats.sort(key=lambda x: x[1], reverse=True)
return resultats
Les benchmarks publies montrent que ColBERT atteint des performances proches des cross-encoders sur les taches de reranking tout en etant significativement plus rapide. La difference de latence devient particulierement marquee quand les embeddings documents sont pre-calcules, ce qui permet un reranking quasi temps-reel meme sur des ensembles larges.
Le reranking par LLM exploite les capacites de raisonnement des grands modeles
Une approche plus recente consiste a utiliser directement un LLM pour le reranking. Le principe est simple : demander au modele de langage d’ordonner les documents par pertinence, en explicitant son raisonnement. Cette methode, souvent appelee “listwise reranking” ou “LLM-as-a-judge”, peut atteindre des performances elevees sur des requetes complexes ou le raisonnement semantique compte plus que la correspondance lexicale.
Le paper RankGPT et les travaux similaires ont explore differentes strategies de prompting pour le reranking. L’approche la plus directe consiste a presenter les documents au LLM et lui demander de les ordonner du plus au moins pertinent. Des variantes utilisent des comparaisons par paires (“Le document A est-il plus pertinent que le document B pour cette question ?”) ou des scores absolus (“Note la pertinence de 1 a 10”).
from openai import OpenAI
import re
class LLMReranker:
"""
Reranking via LLM avec prompting explicite.
Adapte aux cas ou le raisonnement semantique complexe est necessaire.
"""
def __init__(self, model: str = "gpt-4o-mini"):
self.client = OpenAI()
self.model = model
def reranker_listwise(
self,
requete: str,
documents: List[str],
top_n: int = 5
) -> List[Tuple[str, int]]:
"""
Demande au LLM d'ordonner les documents par pertinence.
Retourne les top_n documents avec leur rang.
"""
# Formater les documents avec des identifiants
docs_formates = []
for i, doc in enumerate(documents):
# Tronquer pour le contexte
doc_tronque = doc[:500] + "..." if len(doc) > 500 else doc
docs_formates.append(f"[Doc {i+1}]: {doc_tronque}")
prompt = f"""Etant donne la question suivante et les documents candidats,
ordonne les documents du plus pertinent au moins pertinent pour repondre a la question.
Question: {requete}
Documents:
{chr(10).join(docs_formates)}
Retourne uniquement les numeros des documents dans l'ordre de pertinence decroissante,
separes par des virgules. Par exemple: 3, 1, 5, 2, 4
Classement:"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0
)
# Parser la reponse pour extraire l'ordre
ordre_texte = response.choices[0].message.content.strip()
numeros = re.findall(r'\d+', ordre_texte)
indices_ordonnes = [int(n) - 1 for n in numeros if int(n) <= len(documents)]
# Construire les resultats
resultats = []
for rang, idx in enumerate(indices_ordonnes[:top_n]):
if 0 <= idx < len(documents):
resultats.append((documents[idx], rang + 1))
return resultats
def reranker_pointwise(
self,
requete: str,
documents: List[str]
) -> List[Tuple[str, float]]:
"""
Evalue chaque document individuellement avec un score de pertinence.
Plus precis mais plus couteux en tokens.
"""
resultats = []
for doc in documents:
prompt = f"""Evalue la pertinence du document suivant pour repondre a la question.
Question: {requete}
Document: {doc[:800]}
Donne un score de pertinence de 0 a 10, ou:
- 0-2: Non pertinent
- 3-4: Marginalement pertinent
- 5-6: Partiellement pertinent
- 7-8: Tres pertinent
- 9-10: Parfaitement pertinent
Reponds uniquement avec le score numerique."""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0
)
try:
score = float(response.choices[0].message.content.strip())
except ValueError:
score = 0.0
resultats.append((doc, score))
resultats.sort(key=lambda x: x[1], reverse=True)
return resultats
Le reranking par LLM presente des avantages et des inconvenients distincts. Cote avantages, le LLM peut gerer des requetes complexes necessitant un raisonnement multi-etapes, il comprend les nuances et le contexte implicite, et il peut etre guide par des instructions specifiques au domaine. Cote inconvenients, le cout en tokens et en latence depasse largement les autres approches, les resultats peuvent varier selon le prompting, et le modele peut introduire ses propres biais.
Le choix du reranker depend du contexte applicatif et des contraintes operationnelles
Chaque approche de reranking repond a des besoins differents. Le tableau suivant resume les caracteristiques principales des trois familles de rerankers.
| Critere | Cross-Encoder | ColBERT | LLM Reranking |
|---|---|---|---|
| Precision relative | Elevee | Elevee | Variable selon prompting |
| Latence (50 docs) | 50-200ms GPU | 20-50ms GPU | 2-10s |
| Cout inference | Modere | Faible | Eleve |
| Personnalisation | Fine-tuning | Fine-tuning | Prompting |
| Comprehension complexe | Bonne | Bonne | Excellente |
| Deploiement on-premise | Facile | Facile | Possible mais couteux |
Pour les applications a faible latence et fort volume, ColBERT ou un cross-encoder leger (MiniLM) constituent les choix naturels. Les systemes temps-reel comme les chatbots ou les moteurs de recherche beneficient de leur rapidite sans trop sacrifier la qualite.
Pour les applications ou la precision prime sur la latence, les cross-encoders plus larges (bge-reranker-large, Cohere Rerank) offrent les meilleures performances sur les benchmarks standards. Les cas d’usage B2B internes, les assistants documentaires, ou les systemes de conformite entrent dans cette categorie.
Le reranking par LLM convient aux scenarios ou les requetes sont complexes et rares. L’analyse de contrats, la recherche juridique, ou les questions techniques pointues peuvent justifier le surcout si la qualite de la reponse l’exige. Le cout prohibitif pour le volume rend cette approche inadaptee aux applications grand public.
L’implementation pratique combine plusieurs strategies selon les besoins
Un systeme de production mature peut combiner plusieurs approches de reranking. Une strategie courante consiste a utiliser un reranker rapide (ColBERT ou MiniLM) pour un premier filtrage, puis un reranker plus precis sur le top-10 resultant si la requete le justifie.
class PipelineRerankinMultiEtapes:
"""
Pipeline de reranking en cascade pour optimiser precision et latence.
Utilise un reranker rapide puis un reranker precis sur les meilleurs candidats.
"""
def __init__(
self,
retriever,
reranker_rapide: CrossEncoder,
reranker_precis: CrossEncoder,
top_k_initial: int = 100,
top_k_intermediaire: int = 20,
top_n_final: int = 5
):
self.retriever = retriever
self.reranker_rapide = reranker_rapide
self.reranker_precis = reranker_precis
self.top_k_initial = top_k_initial
self.top_k_intermediaire = top_k_intermediaire
self.top_n_final = top_n_final
def rechercher(self, requete: str) -> List[dict]:
"""Execute le pipeline complet de retrieval et reranking."""
# Etape 1: Retrieval initial large
candidats = self.retriever.search(requete, top_k=self.top_k_initial)
# Etape 2: Premier reranking rapide
paires = [(requete, doc["contenu"]) for doc in candidats]
scores_rapides = self.reranker_rapide.predict(paires)
candidats_filtres = sorted(
zip(candidats, scores_rapides),
key=lambda x: x[1],
reverse=True
)[:self.top_k_intermediaire]
# Etape 3: Reranking precis sur les meilleurs
docs_filtres = [c[0] for c in candidats_filtres]
paires_finales = [(requete, doc["contenu"]) for doc in docs_filtres]
scores_precis = self.reranker_precis.predict(paires_finales)
resultats_finaux = sorted(
zip(docs_filtres, scores_precis),
key=lambda x: x[1],
reverse=True
)[:self.top_n_final]
return [
{**doc, "score_reranking": score}
for doc, score in resultats_finaux
]
Le monitoring en production doit suivre plusieurs metriques pour evaluer l’efficacite du reranking. La comparaison des positions avant/apres reranking (position lift) indique si le reranker apporte de la valeur. Le temps de reranking par requete permet de detecter les regressions de performance. Le feedback utilisateur (clics, reformulations) valide l’amelioration percue.
Les limites du reranking ne doivent pas etre ignorees
Le reranking n’est pas une solution miracle. Sa premiere limite est fondamentale : le reranker ne peut reordonner que ce que le retriever a trouve. Si le document pertinent n’est pas dans le top-k initial, aucun reranking ne le fera apparaitre. Un mauvais retriever ne peut pas etre compense par un excellent reranker.
La deuxieme limite concerne la semantique des documents longs. Les rerankers traitent des passages de quelques centaines de tokens. Si l’information pertinente est diluee dans un document de plusieurs pages, le passage recupere peut ne pas la contenir, et le reranker evaluera un contenu non pertinent. Le chunking en amont reste crucial.
La troisieme limite est le cout. En production, chaque milliseconde compte. Un reranker ajoute une etape d’inference qui consomme du compute et de la latence. Pour les systemes a tres fort volume (millions de requetes/jour), ce cout peut devenir prohibitif. L’arbitrage entre precision et cout operationnel doit etre explicite.
Enfin, les rerankers entraines sur MS MARCO ou des datasets similaires peuvent mal generaliser sur des domaines specifiques. Un reranker performant sur des requetes web generiques peut sous-performer sur du vocabulaire technique, medical, ou juridique. Le fine-tuning sur des donnees domaine-specifiques devient alors necessaire.
Et maintenant ?
Le reranking represente une optimisation accessible qui ameliore substantiellement les pipelines RAG existants. L’integration d’un cross-encoder prend quelques lignes de code et n’impacte pas l’infrastructure de retrieval. Pour les equipes qui observent des reponses RAG imprecises malgre un retrieval correct, le reranking constitue souvent le premier levier a activer.
Racine AI integre des strategies de reranking adaptatives dans ses solutions de traitement documentaire. Nos pipelines combinent retrieval vectoriel, reranking multi-etapes, et validation par VLM pour garantir que les informations extraites correspondent precisement aux besoins metier. Le choix du reranker, sa configuration, et son integration dans l’architecture globale font partie de notre expertise en intelligence documentaire.