Construire un système de génération augmentée par récupération (RAG) ne se limite pas à connecter un grand modèle de langage à une base de données vectorielle. Le véritable goulot d'étranglement de la précision réside dans la phase de récupération. Si le récupérateur apporte un contexte non pertinent au LLM, la réponse finale sera fautive, quelle que soit l'intelligence du modèle. Pour atteindre un RAG haute précision, nous devons aller au-delà de la simple correspondance de mots-clés ou sémantique en combinant les forces de plusieurs stratégies de récupération et en affinant les résultats par le réclassement.
Les limites de la récupération par stratégie unique
La plupart des implémentations initiales de RAG reposent soit sur la récupération creuse (comme BM25), soit sur la récupération dense (utilisant des embeddings comme Sentence-BERT). Chacune a des angles morts distincts.
- Récupération creuse (BM25) : Excèle dans la correspondance exacte des mots-clés et la compréhension de noms d'entités spécifiques, de codes ou de termes techniques. Cependant, elle échoue à capturer la similarité sémantique ou les reformulations.
- Récupération dense (Embeddings) : Capture bien le sens sémantique et le contexte, permettant de trouver des documents qui discutent du même concept mais utilisent des mots différents. Cependant, elle a souvent du mal avec les identifiants uniques, les SKU de produits spécifiques ou le jargon technique rare.
En n'utilisant qu'une seule stratégie, vous sacrifiez soit la précision sur des termes spécifiques, soit le rappel sur les variations sémantiques. La recherche hybride combine les deux signaux pour atténuer ces faiblesses.
Construire un récupérateur hybride dans Haystack
Le framework Haystack fournit une abstraction élégante pour combiner des récupérateurs. Vous pouvez orchestrer un BM25Retriever et un EmbeddingRetriever en parallèle, puis combiner leurs résultats.
from haystack import Pipeline
from haystack.components.retrievers.in_memory import BM25Retriever, EmbeddingRetriever
from haystack.components.routers import SwitchRouter
from haystack.components.joiners import DocumentJoiner
from haystack.components.builders import DocumentJoiner
# Initialisation des composants de récupération
# Note : Dans un pipeline réel, vous connecteriez ces composants à votre InMemoryDocumentStore
bm25_retriever = BM25Retriever(document_store, top_k=20)
embedding_retriever = EmbeddingRetriever(document_store, top_k=20)
# Initialisation du connecteur
# Ce composant fusionne les documents des deux récupérateurs
# Le score_type détermine comment les scores sont combinés (par ex., 'distributional', 'arithmetic')
doc_joiner = DocumentJoiner(
join_mode="concatenate",
duplicate_documents="skip",
score_normalization="min_max",
score_threshold=0.3
)
# Construction du pipeline de recherche hybride
pipeline = Pipeline()
pipeline.add_component("bm25", bm25_retriever)
pipeline.add_component("embedding", embedding_retriever)
pipeline.add_component("joiner", doc_joiner)
# Connexion des composants
# Les deux récupérateurs s'exécutent en parallèle et leurs sorties sont transmises au connecteur
pipeline.connect("bm25", "joiner")
pipeline.connect("embedding", "joiner")
Dans cette configuration, le pipeline interroge les deux index simultanément. Le DocumentJoiner fusionne ensuite les listes de documents. Il est crucial de normaliser les scores, car les scores BM25 (qui sont non bornés et dépendants de la requête) et les scores de similarité cosinus (bornés entre -1 et 1) ne sont pas directement comparables. L'utilisation de score_normalization="min_max" met à l'échelle les deux ensembles de scores sur une plage commune, permettant une compétition équitable lors du processus de fusion.
Le rôle crucial du réclassement
Bien que la recherche hybride améliore le rappel, les 20 ou 30 premiers documents récupérés restent un « sac de résultats ». Certains peuvent n'être que marginalement pertinents. Fournir tous ces documents au LLM gaspille de l'espace dans la fenêtre de contexte et peut introduire du bruit. C'est ici que le réclassement entre en jeu.
Les réclasseurs utilisent des modèles Cross-Encoder. Contrairement aux Bi-Encoders utilisés dans la récupération vectorielle (qui encodent la requête et le document indépendamment), les Cross-Encoders traitent la requête et le document ensemble dans une seule tête d'attention. Cela permet au modèle d'effectuer une interaction profonde au niveau des jetons, fournissant un score de pertinence nettement plus précis.
Intégration d'un réclasseur dans le pipeline
Nous ajoutons un SentenceTransformersReRanker ou un réclasseur basé sur une API dédiée (comme Cohere ou Jina) à la fin de l'étape de récupération. Le réclasseur prend les N meilleurs documents combinés de la recherche hybride et les réordonne, ne sélectionnant que les K plus pertinents pour le générateur.
from haystack.components.rerankers import TransformersSimilarityRanker
# Initialisation du réclasseur
# Utilisez un modèle cross-encoder puissant pour de meilleures performances
re_ranker = TransformersSimilarityRanker(
model="cross-encoder/ms-marco-MiniLM-L-6-v2",
top_k=5,
batch_size=16
)
# Mise à jour du pipeline
pipeline.add_component("re_ranker", re_ranker)
# Connexion du connecteur au réclasseur
pipeline.connect("joiner", "re_ranker")
En définissant top_k=5 dans le réclasseur, nous réduisons considérablement le contexte transmis au LLM. Au lieu d'envoyer 40 documents potentiellement bruités, nous n'envoyons que les 5 documents que le cross-encoder a vérifiés comme hautement pertinents. Cela entraîne généralement une amélioration mesurable de la précision des réponses et une réduction de la latence due à des tailles de prompt plus petites.
Considérations pratiques et optimisation
L'implémentation de cette architecture nécessite d'équilibrer plusieurs facteurs :
- Stratégie de découpage (Chunking) : Assurez-vous que vos documents sont découpés de manière appropriée avant l'indexation. Si les morceaux sont trop grands, le réclasseur peut avoir du mal à trouver la phrase spécifique pertinente. S'ils sont trop petits, vous perdez le contexte. Une plage de 200 à 500 jetons avec un chevauchement de 10 à 20 % est un bon point de départ.
- Seuils de score : Utilisez la sortie du réclasseur pour filtrer les correspondances faibles. Si le score le plus élevé après réclassement est inférieur à un certain seuil (par ex., 0,4 pour les scores normalisés), il est souvent préférable que le système admette « Je ne sais pas » plutôt que d'halluciner une réponse basée sur des preuves faibles.
- Sélection du modèle : Pour les environnements de production, envisagez d'utiliser des API de réclasseurs hébergés (tels que Cohere Rerank ou Jina AI Reranker) pour décharger le coût de calcul du cross-encoding de votre infrastructure locale, en particulier pour les systèmes à haut débit.
Conclusion
Le RAG haute précision n'est pas un tour de magie ; c'est le résultat de l'ingénierie d'un pipeline de récupération robuste. En exploitant l'architecture basée sur les composants de Haystack, vous pouvez intégrer sans effort la recherche hybride pour capturer à la fois les signaux sémantiques et lexicaux, suivie d'un réclasseur cross-encoder pour s'assurer que seul le contexte le plus pertinent atteint le LLM. Cette approche multi-étapes réduit considérablement les hallucinations et améliore la précision factuelle de vos applications IA, transformant un chatbot de base en un assistant de connaissances fiable de niveau entreprise.