La génération augmentée par la récupération (RAG) est devenue l'architecture standard pour ancrer les grands modèles de langage (LLM) dans des données propriétaires. Cependant, un goulot d'étranglement courant dans les pipelines RAG est l'étape de récupération initiale. Les moteurs de recherche vectoriels standard, qui s'appuient sur des algorithmes de plus proches voisins approximatifs (ANN), sont rapides mais constituent des approximations efficaces sur le plan computationnel. Ils récupèrent souvent des documents pertinents sur le plan thématique mais superficiels sur le plan sémantique, ce qui conduit à des scénarios de type « garbage in, garbage out » où le LLM génère des hallucinations ou des réponses non pertinentes.
C'est ici qu'intervient le ré-indexation (re-ranking). En introduisant une deuxième couche de filtrage plus rigoureuse, les développeurs peuvent améliorer considérablement la précision du contexte fourni au LLM sans sacrifier la vitesse de la récupération initiale.
L'architecture de récupération à deux niveaux
Pour comprendre le ré-indexation, il est essentiel de visualiser l'architecture moderne du RAG comme un entonnoir à deux étapes :
- Étape 1 : Récupération des candidats (Rappel). Cette étape utilise des méthodes légères et rapides, telles que la recherche vectorielle creuse (BM25) ou la recherche vectorielle dense (similarité cosinus). L'objectif est de récupérer un grand ensemble de candidats (par exemple, 100 documents) à partir d'un vaste corpus.
- Étape 2 : Ré-indexation (Précision). Cette étape prend la requête et les N meilleurs candidats de l'Étape 1, puis applique un modèle plus complexe pour évaluer leur pertinence avec plus de précision. Les K meilleurs résultats (par exemple, 5 documents) sont transmis au LLM.
Sans ré-indexation, si vous demandez : « Comment réparer une fuite de tuyau ? », le système pourrait retourner des articles sur les « fuites d'eau dans les logiciels » ou le « drainage des eaux pluviales ». Avec le ré-indexation, le système reconnaît que « fuite de tuyau » implique la plomberie physique et relègue le contenu lié aux logiciels.
Pourquoi les Cross-Encoders ?
La norme de l'industrie pour le ré-indexation implique l'utilisation de Cross-Encoders. Contrairement aux Bi-Encoders (utilisés à l'Étape 1), qui encodent les requêtes et les documents indépendamment en vecteurs, les Cross-Encoders traitent la requête et le document ensemble lors d'une seule passe avant. Cela permet au modèle de prêter attention aux interactions spécifiques entre les mots de la requête et les mots du document, capturant ainsi des relations sémantiques nuancées.
Par exemple, dans un Bi-Encoder, « Apple » (le fruit) et « Apple » (l'entreprise) pourraient avoir des représentations vectorielles similaires si elles apparaissent dans des contextes similaires en général. Un Cross-Encoder examinant la requête « nutrition des fruits » et le document « cours des actions technologiques » ne verra presque aucune interaction, ce qui se traduira par un score de pertinence faible.
Mise en œuvre d'un ré-indexeur avec Python
Mettre en œuvre le ré-indexation est simple à l'aide de bibliothèques telles que sentence-transformers ou pyserini. Voici un exemple pratique utilisant la bibliothèque populaire sentence-transformers, qui fournit des modèles Cross-Encoder optimisés.
from sentence_transformers import CrossEncoder
# Initialiser le modèle Cross-Encoder
# 'cross-encoder/ms-marco-MiniLM-L-6-v2' est un choix rapide et efficace
model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
# La requête et une liste de documents candidats récupérés à l'Étape 1
query = "Comment implémenter OAuth2 en Python ?"
candidates = [
"Python est un langage de programmation.",
"Un guide étape par étape pour implémenter l'authentification OAuth2 dans les applications Python en utilisant Flask.",
"Comprendre les bases des requêtes HTTP.",
"Document de spécification OAuth2 version 2.0."
]
# Encoder les paires requête-document
# Le modèle renvoie un score de pertinence pour chaque paire
scores = model.predict([(query, doc) for doc in candidates])
# Associer les scores aux documents et trier par pertinence
ranked_results = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
# Imprimer le meilleur résultat
print("Meilleur résultat classé :")
print(f"Document : {ranked_results[0][0]}")
print(f"Score : {ranked_results[0][1]:.4f}")
Dans cet exemple, le deuxième document, qui mentionne explicitement « OAuth2 » et « Python », obtiendra un score nettement supérieur à celui des documents génériques sur « Python » ou « HTTP », garantissant ainsi que le LLM reçoit les informations les plus contextuellement précises.
Équilibrer latence et précision
Les Cross-Encoders sont coûteux sur le plan computationnel car ils traitent les paires de texte séquentiellement plutôt qu'en parallèle. Si votre corpus est massif et que le volume de requêtes est élevé, l'exécution d'un Cross-Encoder sur des milliers de candidats est impossible. C'est pourquoi l'approche à deux étapes est cruciale : la première étape réduit l'espace de recherche de millions à centaines, et la deuxième étape affine cet petit ensemble.
Pour les environnements de production, envisagez d'utiliser des API de ré-indexation dédiées ou des modèles optimisés tels que ms-marco-TinyBERT-L-2-v2 pour une inférence plus rapide, ou des stratégies de mise en cache pour les questions fréquemment posées.
Conclusion
Le ré-indexation n'est pas seulement une optimisation ; c'est une nécessité pour construire des applications RAG fiables et prêtes pour la production. En découplant le rappel de la précision, les développeurs peuvent tirer parti de la vitesse des bases de données vectorielles tout en maintenant la précision d'une compréhension sémantique approfondie. La mise en œuvre d'un ré-indexeur Cross-Encoder est l'un des changements offrant le meilleur retour sur investissement pour améliorer la qualité des réponses de votre LLM, réduisant ainsi les hallucinations et augmentant la confiance des utilisateurs.