Vous avez mis en place un système de Retrieval-Augmented Generation (RAG) pour permettre à un assistant IA de puiser des réponses dans votre base de connaissances. Pourtant, vous constatez que les résultats ne sont pas toujours à la hauteur : parfois l’information cruciale n’est pas dans les tout premiers documents retournés, ou au contraire vous envoyez trop de texte inutile au modèle.
Comment fournir à votre LLM exactement le bon contexte, sans l’inonder de données non pertinentes ?
C’est le défi que j’ai rencontré récemment en développant mon propre agent RAG. Ma solution : un reranker sémantique en C#, que je viens d’implémenter et de publier en open source sur GitHub.
Dans cet article, je vous propose de découvrir ce mécanisme de reranking, son intérêt pour améliorer les performances des RAG, et les détails techniques de mon implémentation. Embarquez dans l’optimisation de nos assistants ! 🚀
Le problème : rappel vs fenêtre de contexte
Lorsqu’on interroge une base documentaire avec un vecteur sémantique (embedding), on récupère en général les K documents jugés les plus similaires à la question. Cette technique de recherche vectorielle est très efficace pour balayer rapidement de larges volumes de texte. Cependant, en pratique, le “bon” passage n’est pas toujours tout en haut des résultats. Il arrive qu’un document très pertinent soit classé 5ᵉ ou 10ᵉ, surtout si on limite K à une valeur modeste pour préserver le contexte. Augmenter K (c’est-à-dire élargir le rappel de la recherche) permettrait de ne manquer aucun élément utile… mais envoyer 10 ou 20 documents dans le prompt du LLM pose un autre souci.
En effet, les LLM ont une fenêtre de contexte limitée (par ex. 4K, 16K tokens, etc.), et leur capacité à retenir l’information décroît quand on bourre trop de texte dans le prompt. On parle souvent de “LLM recall” (rappel du modèle) pour désigner la faculté du LLM à retrouver dans le contexte fourni ce qui répond à la question. Des recherches ont montré que ce rappel interne se dégrade quand le contexte s’allonge démesurément. Autrement dit, ajouter trop de documents peut rendre le modèle confus ou diluer les infos essentielles. Sans compter l’augmentation du coût en tokens et en temps de calcul pour chaque requête.
Il y a donc un équilibre à trouver : récupérer suffisamment de documents pour ne pas passer à côté de l’information pertinente (maximiser le rappel de la recherche), tout en n’en servant que très peu au LLM pour qu’il reste focalisé et efficace (minimiser le contexte à traiter). Comment concilier ces objectifs opposés ? C’est là qu’intervient le reranking, ou reclassement sémantique des documents.
Le reranking : principe et bénéfices
Un reranker est un module de deuxième étape qui va affiner les résultats de la recherche initiale. Concrètement, on effectue d’abord la recherche vectorielle “classique” pour obtenir, par exemple, un Top K=10 de documents candidats. Puis, on applique un modèle différent qui va analyser en profondeur chaque document par rapport à la question, et reclasser ces résultats par pertinence. On peut alors ne garder que le Top N (par ex. les 3 meilleurs) de ce re-rank pour les passer au LLM. On parle de recherche à deux étages : un premier filtrage large et rapide, suivi d’un second tri beaucoup plus précis.
Question utilisateur
│
▼
[Recherche initiale] → Top-K candidats (p.ex. via embeddings)
│
▼
[Reranker BM25]
- Prétraitement NLP (Catalyst)
- Index inversé + DF / longueurs
- Score BM25(q, doc) pour chaque doc
│
▼
Sélection Top-N (N << K)
│
▼
Contexte compact → LLM → Réponse
Pourquoi ce reranker est-il plus précis que la recherche initiale ?
Ce n’est pas qu’elle est plus performante en tant que telle mais surtout parce qu’elle est complémentaire à la recherche vectorielle ou hybride.
- Recherche vectorielle (embeddings)
Elle excelle pour capter la proximité sémantique : même si les mots sont différents, deux textes proches en sens ressortent ensemble. Problème : elle peut parfois mettre en avant des passages “globalement similaires” mais pas directement liés à la question. - BM25 (lexical)
Cet algorithme repose sur la fréquence inverse des termes (IDF) et la normalisation par longueur. Il privilégie les documents qui contiennent exactement les mots de la requête, en tenant compte de leur rareté et de leur concentration. Cela corrige certains biais des embeddings, par exemple quand un mot technique ou un chiffre exact est indispensable.
👉 En combinant les deux, ton pipeline bénéficie du rappel sémantique large (ne rien rater) et de la précision lexicale fine (ne garder que le pertinent).
BM25 applique deux raffinements qui le rendent souvent plus fiable en reclassement :
- Saturation de fréquence (paramètre
k1)
Si un mot est répété 10 fois, cela augmente son importance, mais de manière décroissante. On évite ainsi qu’un document “spammé” d’un mot-clé monte artificiellement. - Correction par longueur (
b)
Un document très long n’est pas automatiquement favorisé. BM25 équilibre la pertinence entre courts extraits très concentrés et longs textes dilués.
Ces ajustements assurent que les passages concis et vraiment pertinents montent en haut du classement.
Implémentation en C#
Voyons maintenant comment j’ai intégré ce mécanisme dans mon application C#. N’ayant pas de reranker “tout fait” dans Semantic Kernel, j’ai développé une solution sur mesure (disponible sur mon dépôt GitHub SemanticKernel.Reranker).
Installation du package
dotnet add package SemanticKernel.Reranker.BM25
La solution s’intègre assez simplement avec Semantic Kernel. On va d’abord exécuter la recherche initiale via le Kernel (ou votre vecteur store favori), puis appeler le reranker sur le résultat. Par exemple, pour un kernel configuré avec une collection de vecteurs :
// 1) Récupérer un pool de K candidats (ex: via vector store)
var candidates = await collection.SearchAsync(searchVector, top: 100);
// 2) Reranker BM25 sur le contenu textuel des candidats
var texts = candidates.Select(c => c.Record.Text).ToList();
var bm25 = new BM25Reranker(texts); // k1=1.5, b=0.75 par défaut
IEnumerable<(int Index, double Score)> ranked = await bm25.RankAsync(query, topN: 5);
// 3) Conserver les N meilleurs passages pour le prompt final
var selected = ranked.Select(r => texts[r.Index]).ToList();
// 4) Construire le message système/utilisateur avec "selected"
// puis appeler le LLM comme d’habitude (via SK)
Pour améliorer la robustesse du lexical matching, la Le ranker que je propose applique avant scoring :
- Détection de langue (multi‑FR/EN/DE, etc.) ;
Afin d’appliquer le bon jeu de règles linguistiques (stop-words, modèles de lemmatisation, tokenisation spécifiques à chaque langue)
- Tokenisation + lemmatisation (ex. “courir” = “cours”, “running” = “run”) ;
Afin de transformer une phrase en une liste de mots distincts, en séparant correctement les apostrophes, les traits d’union ou la ponctuation. Vient ensuite la lemmatisation, qui réduit chaque mot à sa forme canonique : « courait », « courrons » et « cours » sont ramenés au lemme « courir », de même que « running » ou « ran » deviennent « run ». Ce processus garantit que BM25 considère toutes ces variantes comme un seul et même terme, renforçant ainsi le rappel lexical
- Filtrage de stop‑words ;
Qui permet de supprimer les mots trop fréquents et non discriminants comme « le », « de », « and », « the », qui n’apportent aucune valeur informative et risqueraient de biaiser les scores.
- POS‑tagging pour ignorer ponctuation/symboles
Qui permet d’identifier la nature des tokens (nom, verbe, ponctuation, symbole, etc.) et de filtrer ceux qui n’apportent rien au sens, comme la ponctuation ou les caractères spéciaux
Exemple concret
Pour bien comprendre l’intérêt du pipeline NLP appliqué avant BM25, prenons un exemple simple en anglais.
Query (utilisateur) :
“How can I run faster while I am training for a marathon?”
Document (base de connaissance) :
“Studies show that intensive endurance training and consistent practice help athletes improve their running speed and stamina. Regular workouts also reduce fatigue, making it easier to sustain higher speeds.”
Texte brut
La requête et le document apparaissent tels quels. Problème : les différences de forme (« run » vs « running »), les synonymes partiels (« workouts » vs « training ») ou les mots vides (« how », « can », « for », « a ») risquent de brouiller la comparaison lexicale si l’on applique BM25 directement.
Tokenisation
Le texte est découpé en unités lexicales (tokens).
Document →
["Studies", "show", "that", "intensive", "endurance", "training", "and",
"consistent", "practice", "help", "athletes", "improve", "their", "running",
"speed", "and", "stamina", "Regular", "workouts", "also", "reduce",
"fatigue", "making", "it", "easier", "to", "sustain", "higher", "speeds"]
Lemmatisation
Chaque mot est réduit à sa racine :
- « running » → « run »
- « workouts » → « workout »
- « speeds » → « speed »
La requête devient :
["run", "fast", "train", "marathon"]
Stop-words
Les mots fréquents mais peu discriminants (« how », « can », « for », « a », « their ») sont retirés.
Document réduit :
["study", "show", "intensive", "endurance", "training", "consistent",
"practice", "help", "athlete", "improve", "run", "speed", "stamina",
"regular", "workout", "reduce", "fatigue", "make", "easy", "sustain",
"high", "speed"]
POS-tagging
Chaque token est annoté grammaticalement (nom, verbe, adjectif, etc.). Ici, on conserve uniquement les termes informatifs, en ignorant ponctuation et symboles.
Résultat final exploité par BM25
- Query →
["run", "fast", "train", "marathon"] - Document →
["study", "show", "intensive", "endurance", "training", "consistent", "practice", "help", "athlete", "improve", "run", "speed", "stamina", "regular", "workout", "reduce", "fatigue", "make", "easy", "sustain", "high", "speed"]
Grâce à cette normalisation, BM25 repère immédiatement plusieurs correspondances fortes : run, train/training, speed. Les variantes sont harmonisées, le bruit lexical supprimé, et la pertinence du document est mieux reflétée dans son score. Ce document aura donc toutes les chances de remonter dans le Top-N présenté au LLM.
Résultats et perspectives
En intégrant ce reranker dans mon pipeline, j’ai constaté des améliorations significatives. Mes réponses d’assistant sont plus précises, moins “hors sujet”, et je maîtrise beaucoup mieux la taille des prompts envoyés au LLM. En réduisant le bruit contextuel, j’ai pu observer des réponses plus cohérentes et factuelles, là où auparavant le modèle partait parfois sur des tangentes dues à des documents non pertinents glissés dans le lot. Côté performances, le gain en efficacité se ressent aussi par une légère baisse du coût en tokens par requête, sans impact négatif sur le temps de réponse global – voire une latence réduite dans certains cas, le LLM ayant moins de texte à analyser.
Bien sûr, on pourrait aller encore plus loin. Par exemple, entraîner un reranker spécialisé sur son domaine de documents, ou combiner plusieurs modèles de rerank (certains travaux récents explorent des rerankers de type LLM zero-shot), ou encore des modèles de type cross-encoder, c’est-à-dire un modèle qui prend en entrée la question et un document, et produit un score de similarité sémantique spécifique à cette paire. On peut également ajuster dynamiquement K et N en fonction de la question (si la question est très large, prendre plus de documents avant rerank). L’architecture mise en place permet ces évolutions assez facilement.
En attendant, si vous aussi vous faites face à des limitations de votre pipeline RAG, n’hésitez pas à essayer ce reranker ! 🧩 Intégré en quelques lignes, il peut grandement augmenter la pertinence de vos assistants exploitant une base de connaissances. Le code source complet est disponible sur mon dépôt GitHub– vos retours et contributions sont les bienvenus. J’espère que cet article vous aura éclairé sur l’intérêt du reranking sémantique et donné envie de l’expérimenter dans vos propres projets. 🔎🤖 Avec les bons documents au bon endroit, vos LLM n’en seront que plus intelligents !