Aller au contenu
Développement medium

RAG avancé : améliorer la qualité du retrieval

10 min de lecture

logo python

Un RAG basique fonctionne — jusqu'à ce qu'il rate un document pourtant pertinent. La cause est presque toujours le retrieval : la recherche n'a pas remonté le bon passage. Ce guide présente trois techniques qui réduisent ce taux d'échec. La recherche hybride combine la recherche par sens et la recherche par mots exacts. Le re-ranking réordonne les résultats avec un modèle plus précis. Le RAG agentique transforme la recherche unique en une boucle qui se corrige. Vous construirez la recherche hybride avec une fusion RRF, et situerez les deux autres techniques. Public visé : développeur dont le RAG de base marche, mais manque encore des réponses.

  • Pourquoi la recherche dense seule échoue sur certains cas.
  • Combiner dense et BM25 en une recherche hybride.
  • Fusionner deux classements avec la Reciprocal Rank Fusion.
  • Le principe du re-ranking par cross-encoder.
  • Quand passer au RAG agentique.

La recherche dense — par embeddings — cherche par sens. C'est sa force : « conserver les données » retrouve « volume Docker » sans mot commun. Mais c'est aussi sa faiblesse.

Un embedding résume un texte en un vecteur. Dans ce résumé, un terme rare et précis — un code d'erreur E_4042, un identifiant, une option --volumes-from — pèse peu : il se dilue dans le sens général de la phrase. Résultat : une question qui cite ce terme exact peut ne pas remonter le bon document, alors qu'il contient le terme mot pour mot.

La recherche lexicale, elle, ne cherche que les mots. L'algorithme BM25 classe les documents par correspondance de termes, pondérée par leur rareté. Sur un terme exact, il est imbattable — mais il ignore totalement le sens : une reformulation lui échappe.

Chacune échoue là où l'autre réussit. D'où l'idée de les combiner.

La recherche hybride lance les deux recherches en parallèle, puis fusionne leurs résultats. On obtient le meilleur des deux : le sens et les termes exacts.

Côté lexical, BM25 s'utilise directement — pas de modèle, pas de réseau.

import re
from rank_bm25 import BM25Okapi
def tokeniser(texte: str) -> list[str]:
"""Découpe en tokens — les options comme --volumes-from restent entières."""
return re.findall(r"[\w\-]+", texte.lower())
def recherche_bm25(question: str, corpus: list[str], k: int = 5) -> list[int]:
"""Classe les indices du corpus par correspondance de termes (BM25)."""
bm25 = BM25Okapi([tokeniser(doc) for doc in corpus])
scores = bm25.get_scores(tokeniser(question))
return sorted(range(len(corpus)), key=lambda i: scores[i], reverse=True)[:k]

La tokenisation mérite attention : le motif [\w\-]+ garde --volumes-from en un seul token. C'est la condition pour que BM25 retrouve une option ou un identifiant à l'identique. Côté dense, on réutilise la recherche par embeddings des guides précédents. Restent deux classements à réconcilier.

Comment fusionner un classement dense et un classement BM25 ? Additionner leurs scores est piégeux : un score cosinus et un score BM25 ne sont pas comparables — pas la même échelle, pas la même nature.

La Reciprocal Rank Fusion (RRF) contourne le problème : elle ignore les scores et ne regarde que les rangs. Chaque document gagne 1 / (k + rang) dans chaque liste où il apparaît ; on additionne ces contributions.

def fusion_rrf(classements: list[list[int]], k_rrf: int = 60) -> list[int]:
"""Fusionne plusieurs classements avec la Reciprocal Rank Fusion."""
scores: dict[int, float] = {}
for classement in classements:
for rang, doc in enumerate(classement):
scores[doc] = scores.get(doc, 0.0) + 1.0 / (k_rrf + rang)
return sorted(scores, key=lambda d: scores[d], reverse=True)

La logique est élégante. Un document bien classé dans les deux listes accumule deux fortes contributions : il remonte. Un document premier dans une seule liste reste honorablement placé. RRF récompense la constance — et comme elle ne manipule que des rangs, elle ne demande aucune normalisation.

def recherche_hybride(question, corpus, k=3):
dense = recherche_dense(question, corpus, k=5)
lexical = recherche_bm25(question, corpus, k=5)
fusion = fusion_rrf([dense, lexical])
return [corpus[i] for i in fusion[:k]]

La recherche — dense, lexicale ou hybride — est rapide mais approximative : elle compare des vecteurs ou des termes, sans vraiment « lire » la question face à chaque document. Le re-ranking ajoute une seconde passe, plus lente mais plus fine.

Le principe : la recherche remonte une présélection large — disons les 20 meilleurs candidats. Un modèle cross-encoder examine alors chaque paire (question, candidat) ensemble, et attribue un score de pertinence autrement plus juste. On garde le top 3 ou 5 de ce reclassement.

La différence tient au mode de calcul. La recherche compare des vecteurs calculés séparément — l'embedding de la question, celui du document, jamais confrontés. Le cross-encoder, lui, traite la paire en une seule fois : il « lit » la question et le document ensemble. Bien plus précis — mais bien trop lent pour l'appliquer à tout le corpus, d'où le schéma en deux temps : recherche large, puis re-ranking du petit lot.

ÉtapeRôleVitesse
Recherche (dense / hybride)Présélectionner un large lotRapide
Re-ranking (cross-encoder)Reclasser finement le lotLent, mais sur peu de candidats

Des modèles de re-ranking dédiés existent — la famille BGE reranker s'auto-héberge, d'autres s'utilisent par API. Le re-ranking a un coût en latence : on l'ajoute quand la qualité du retrieval, mesurée, le justifie — pas par principe.

Le RAG classique fait une seule recherche, puis répond. Si cette recherche est mauvaise — question ambiguë, mal formulée —, la réponse l'est aussi. Le RAG agentique brise ce schéma figé.

Au lieu d'un enchaînement linéaire, le RAG agentique confie le retrieval à un agent qui peut itérer : reformuler la question, relancer une recherche, juger si le contexte récupéré suffit, et recommencer sinon. La recherche devient une boucle qui se corrige, au lieu d'un coup unique.

C'est exactement le modèle des agents et de LangGraph : un graphe d'états où un nœud « rechercher » et un nœud « évaluer » s'enchaînent jusqu'à ce que le contexte soit jugé suffisant. La boucle de correction d'un superviseur s'applique ici telle quelle.

SymptômeCause probableSolution
Un identifiant exact n'est pas trouvéRecherche dense seuleAjouter BM25 (recherche hybride)
BM25 rate une reformulationRecherche lexicale seuleCombiner avec la recherche dense
Fusion incohérenteScores additionnés directementUtiliser RRF, qui fusionne des rangs
Re-ranking trop lentCross-encoder sur trop de candidatsRéduire la présélection avant re-ranking
RAG agentique lent et coûteuxItération sur des questions simplesLe réserver aux questions complexes
  • La recherche dense capte le sens mais rate les termes exacts ; BM25 fait l'inverse.
  • La recherche hybride lance les deux et fusionne — le meilleur des deux mondes.
  • La RRF fusionne des rangs, pas des scores — robuste, sans normalisation.
  • Le re-ranking par cross-encoder reclasse finement une présélection — précis mais lent.
  • Le RAG agentique transforme le retrieval en boucle qui se corrige — pour les questions complexes.
  • Chaque technique a un coût : on l'ajoute quand la mesure le justifie, pas par principe.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn