Comment améliorer la précision d’un RAG en production ?

Un RAG fiable se construit en nettoyant les données, en améliorant la récupération et en filtrant les résultats. Je détaille ici des techniques pré-retrieval, retrieval et post-retrieval pour réduire hallucinations, augmenter le rappel et gagner en robustesse en production.


Besoin d'aide ? Découvrez les solutions de notre agence Openai GPT.

Pourquoi le RAG basique échoue ?

Le RAG basique échoue parce qu’il repose sur un seul vecteur par document et envoie les top-K bruts au LLM, ce qui entraîne faible rappel, hallucinations et biais de position.

Voici concrètement ce qu’on observe en production et pourquoi cela nuit à l’expérience utilisateur.

  • Rappel insuffisant (information manquante). Les réponses oublient des faits pertinents lorsque l’information est dispersée sur plusieurs chunks. Exemple : «Quel est le processus X?» Réponse erronée parce que la partie expliquant l’étape 3 n’a pas été récupérée.
  • Hallucinations liées à fragments insuffisants ou bruités. Le LLM invente des détails quand les passages fournis sont incomplets ou contiennent du bruit. Exemple : «Quel est le montant du contrat?» Réponse inventée car seul un extrait général a été envoyé.
  • Biais vers le début/fin des chunks. Les systèmes favorisent souvent les premières ou dernières phrases d’un chunk, oubliant le centre du document. Exemple : «Quelle est la limitation Y?» Réponse focalisée sur l’introduction, pas sur la section dédiée.
  • Manque d’adaptation au domaine et réponses superficielles ou répétitives. Le modèle répète des formulations génériques faute de passages spécialisés. Exemple : «Comment configurer le service Z?» Réponse vague car les extraits techniques n’ont pas été mis en avant.
  • Causes techniques. Vecteur unique par document qui moyenne signaux distincts. Chunks mal calibrés qui brisent le contexte. Absence de métadonnées (date, section, auteur) empêchant le filtrage. Recherche purement dense (ex. DPR) ou purement sparse (ex. BM25) sans hybridation. Absence de ré-ordonnancement final avant le passage au LLM.
  • Travaux de référence. Voir Lewis et al. 2020 pour RAG : https://arxiv.org/abs/2005.11401. Voir Karpukhin et al. 2020 pour DPR (Dense Passage Retrieval) : https://arxiv.org/abs/2004.04906. Voir BM25 pour recherche sparse : https://en.wikipedia.org/wiki/Okapi_BM25.

Objectifs d’amélioration : augmenter le rappel pour couvrir toutes les sources pertinentes; réduire les hallucinations en fournissant plus de contexte et de vérifiabilité; améliorer l’exactitude en domain-adaptant les signaux de récupération; accroître la traçabilité en liant chaque phrase aux passages sources.

SymptômeCauseMétriques à suivre
Rappel insuffisantVecteur unique, chunks mal calibrésRappel, couverture documentaire, F1
HallucinationsPassages insuffisants/bruités, pas de ré-ordonnancementTaux d’hallucination, précision factuelle, exactitude
Biais de positionChunking naïf, absence de métadonnéesDistribution des positions récupérées, rappel par segment
Réponses superficiellesPas d’adaptation domaine, mélange dense/sparse absentQualité perçue, score BLEU/ROUGE adapté, temps de latence

Comment préparer et indexer les données ?

On prépare les données en nettoyant, densifiant et en découpant pour maximiser la couverture utile lors de la recherche.

Augmenter la densité d’information via LLMs aide à compresser le sens utile d’un texte et à produire des Q/A hypothétiques qui améliorent le recall lors de la recherche. Workflow pratique : nettoyer le texte, chunker grossièrement, appeler un LLM pour générer un résumé dense puis 3–7 questions/réponses par chunk, dédupliquer et stocker le résumé + Q/A comme métadonnée.

Exemple de prompt LLM pour résumé dense et 5 questions :

Résumé dense (1–2 phrases) du texte suivant, puis génère 5 questions pertinentes avec leurs réponses courtes.
Texte : {CHUNK}
Format :
Résumé : ...
Q1 : ... | A1 : ...
Q2 : ... | A2 : ...
...
  • Data chunking : Taille fixe 200–500 tokens pour la plupart des docs, fenêtre glissante (overlap 20–30%) pour préserver contexte entre chunks, et découpe hiérarchique/récursive (par section, puis paragraphes) pour docs longs. Les choix sont des compromis : plus petit augmente la précision locale mais multiplie les vecteurs et le coût d’indexation ; plus grand conserve le contexte mais masque des réponses fines à l’intérieur du chunk.
  • Self-query et métadonnées : Enrichir chaque chunk avec auteur, date, source, tags sémantiques et champs de recherche permet des filtres (par ex. date>2023) et améliore la fraîcheur et la pertinence. Self-query signifie générer automatiquement tags et requêtes possibles via LLMs pour améliorer le matching sémantique.
  • Indexer : Solutions pratiques : FAISS (local, rapide), Milvus (scalable), Pinecone (SaaS), Weaviate (graph + vector).

Exemple Python concis :

# Sliding window chunking, embeddings et insertion FAISS (pseudocode)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('all-MiniLM-L6-v2')
def sliding_chunks(text, size=400, overlap=100):
    # découpe simple
    tokens = tokenize(text)
    for i in range(0, len(tokens), size-overlap):
        yield detokenize(tokens[i:i+size])
for chunk in sliding_chunks(doc):
    emb = model.encode(chunk)
    faiss_index.add_with_ids(emb, metadata={'text':chunk,'source':src,'date':date})
StratégieTailleLatenceCoût d’indexationRappel attendu
Petit fixe200–300 tokensMoyenneÉlevéHaut (précis sur micro-questions)
Fenêtre glissante300–500 tokens (overlap 20–30%)Plus élevéTrès élevéTrès haut (meilleur contexte)
HiérarchiqueSections puis paragraphesVariableMoyenBon (meilleur pour docs longs)

Points de contrôle qualité indispensables : logs d’indexation détaillés, tests de rappel sur un jeu d’exemples métier (KPI : rappel/precision), monitoring des latences, et rotation/actualisation régulière des embeddings pour données fraîches (par ex. ré-embed toute donnée modifiée depuis 24–48h).

Quelles méthodes de retrieval amélioreront la pertinence ?

La pertinence monte en combinant dense et sparse, en reformulant la requête, et en procédant par passes successives.

Commencer par un index sparse (BM25) et un index dense (embeddings) en parallèle, puis fusionner les résultats permet d’obtenir un meilleur rappel et une précision plus élevée. BM25 est un modèle probabiliste basé sur la fréquence des termes (Robertson et al.), utile pour des requêtes proches du texte. DPR (Dense Passage Retrieval) repose sur des embeddings appris pour capturer la similarité sémantique (Karpukhin et al., 2020).

  • Hybrid search : Exécuter BM25 et nearest-neighbors sur embeddings, normaliser chaque score (min-max ou z-score), puis combiner par pondération (score_final = α·score_dense + (1−α)·score_sparse). Tester α entre 0.3 et 0.7 selon le corpus.
  • Query rewriting and expansion : Reformuler la requête via un LLM pour clarifier intention, extraire entités nommées et ajouter synonymes ou contraintes temporelles. Exemple de prompt : « Reformule la requête suivante en 2 versions plus précises, ajoute 3 synonymes et précise la contrainte temporelle si pertinente : ». Ajouter termes issus de WordNet ou d’alias métier.
  • Multi-stage retrieval : Pipeline large->filtre->rerank : 1) coarse retrieval (BM25 + ANN) pour obtenir ~200 candidats, 2) filtrage heuristique (dates, type de document), 3) reranking par cross-encoder (MonoBERT style) pour top10. Avantage : latence contrôlée avec qualité élevée sur top results.
  • Graph RAG : Construire un graphe où nœuds = entités ou snippets et arêtes = relations sémantiques (cooccurrence, citation). Utiliser Knowledge Graph + embeddings pour navigation contextuelle et expliquer la « big picture ». Outils : Neo4j, RDF + embeddings.
  • Multi-hop : Chaîner recherches successives pour répondre aux questions nécessitant plusieurs faits (HotpotQA comme benchmark). Prévenir la dérive d’information en conserver un historique de requêtes/réponses, en appliquer contraintes d’entité et en valider chaque saut par score de confiance.
# Pseudocode Python
# 1) BM25 via rank_bm25
from rank_bm25 import BM25Okapi
tokenized_corpus = [doc.split() for doc in corpus]
bm25 = BM25Okapi(tokenized_corpus)
bm25_scores = bm25.get_scores(query.split())

# 2) Embeddings via sentence-transformers
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer('all-MiniLM-L6-v2')
emb_query = model.encode(query, convert_to_tensor=True)
emb_corpus = model.encode(corpus, convert_to_tensor=True)
dense_scores = util.pytorch_cos_sim(emb_query, emb_corpus)[0].cpu().numpy()

# 3) Fusion des scores (min-max norm)
def minmax(a): return (a - a.min())/(a.max()-a.min()+1e-9)
scores = 0.5*minmax(dense_scores) + 0.5*minmax(bm25_scores)

# 4) Seconde passe avec cross-encoder pour reranking
from sentence_transformers import CrossEncoder
cross = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
candidates = [corpus[i] for i in argsort(scores)[-200:]]
pairs = [[query, c] for c in candidates]
rerank_scores = cross.predict(pairs)
final = sort_by(rerank_scores)[:10]
MéthodeComplexitéLatence typiqueGains attendusScénarios recommandés
BM25 (sparse)FaibleQuelques ms par requête (index local)Bon rappel pour requêtes littéralesCorpus technique, recherche factuelle
Embeddings (dense)Moyenne (ANN)10-100 ms selon ANNMeilleure similarité sémantiqueParaphrases, questions ouvertes
Hybrid (BM25 + dense)Moyenne20-150 ms+10-30% qualité top-kProduction RAG équilibrée
Multi-stage + cross-encoderÉlevée100 ms – 1sMeilleure précision top-5Cas où précision critique (support, compliance)
Graph RAG / Multi-hopÉlevéeVariable (s selon complexité)Contexte & chain-of-thought améliorésAnalyse causalité, synthèse multi-document

Que faire après la récupération pour garantir une réponse fiable ?

Après récupération, on nettoie, réordonne et filtre les documents pour réduire les hallucinations et garantir traçabilité.

  • Re-ranking : Utiliser un cross-encoder spécialisé permet de réévaluer la pertinence en considérant l’interaction complète entre la requête et chaque snippet. Le cross-encoder (modèle qui encode la paire requête+snippet ensemble) offre généralement une meilleure précision que les bi-encoders au prix d’un coût calcul plus élevé. Références connues : MonoBERT (Nogueira & Cho, 2019) pour le re-ranking et ColBERT (Khattab & Zaharia, 2020) pour une interaction tardive efficace. Exemple de pipeline : récupérer top-N avec un retriever dense, appliquer un cross-encoder pour scorer les paires (requête, snippet), trier par score et garder top-K pour la synthèse.
  • Nettoyage et déduplication : Regrouper les snippets similaires par clustering (par exemple cosine similarity > 0.9) pour fusionner redondances. Supprimer le boilerplate par règles (nav, footer, disclaimers) ou modèles appris. Résoudre contradictions internes en priorisant snippets plus récents ou provenant de sources hautement fiables.
  • Filtrage basé sur la confiance et la source : Appliquer des seuils numériques sur les scores de similarité et de re-ranking. Vérifier la fraîcheur via les métadonnées (date, version) et rejeter domaines ou formats non fiables. Demander au LLM une auto-évaluation de confiance (score 0-1) et exiger que chaque affirmation clé soit accompagnée d’une citation explicite vers le snippet source.
  • Synthèse contrôlée : Imposer au LLM un template de prompt clair pour synthétiser uniquement à partir des snippets fournis, formater les citations et indiquer un score de confiance et d’incertitude. Exemple de template inclus ci-dessous.
  • Vérification automatique : Détecter contradictions internes via models de NLI (Natural Language Inference) et lancer des requêtes additionnelles pour fact-checking externe quand la confiance est basse. Escalader vers un humain si le score de confiance < 0.4 ou si contradictions non résolues persistent.
  • Exemple de code (ré-ranking + filtrage + appel LLM) :
    # Ré-ranking avec CrossEncoder (sentence-transformers)
    from sentence_transformers import CrossEncoder
    query = "Quelle est la politique de confidentialité ?"
    snippets = ["Snippet A...", "Snippet B...", "Snippet C..."]
    model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')  # cross-encoder optimisé MS MARCO
    pairs = [[query, s] for s in snippets]
    scores = model.predict(pairs)  # scores float
    # Filtre de confiance numérique
    kept = [(s, sc) for s, sc in zip(snippets, scores) if sc >= 0.5]  # seuil 0.5
    kept_sorted = sorted(kept, key=lambda x: x[1], reverse=True)[:5]
    # Appel LLM avec template de synthèse (pseudo-code)
    prompt = f"Utilise uniquement ces snippets : {kept_sorted}. Rédige une synthèse avec citations et indique un score de confiance 0-1."
    # llm.generate(prompt)  # remplacer par l'API LLM choisie
ÉtapeOutils recommandésMétriques de contrôle
Re-rankingCross-encoder (MonoBERT, ColBERT, cross-encoder/ms-marco)Précision (target >85%), Temps de réponse (ms)
Nettoyage / DédupClustering cosine, règles de boilerplateRappel utile, Réduction de redondance (%)
Filtrage source/confianceScore thresholds, métadonnéesTaux d’hallucination (target <5%), fraîcheur
Synthèse contrôlée & Vérif.Prompts contraints, NLI, fact-check APIsTaux d’acceptation humaine, confiance moyenne

Prêt à rendre votre RAG plus précis et fiabilisé ?

J’ai résumé une approche pragmatique pour faire évoluer un RAG basique vers un système fiable : nettoyer et densifier les données, découper et indexer intelligemment, combiner retrieval dense et sparse, appliquer multi-stage retrieval et ré-ranker/filtrer les résultats avant synthèse. Ces étapes réduisent hallucinations, augmentent le rappel et donnent des réponses traçables. En appliquant ce plan, vous gagnez en précision métier et en confiance opérationnelle — bénéfice concret : moins d’erreurs, moins d’interventions manuelles et une adoption utilisateur accélérée.

FAQ

  • Qu’est-ce que RAG et pourquoi l’utiliser ?
    RAG (Retrieval-Augmented Generation) combine une base de connaissance externe (documents indexés) et un LLM qui synthétise les informations récupérées. On l’utilise pour limiter les hallucinations, fournir des citations et permettre des réponses basées sur des sources contrôlées.
  • Quelle taille de chunk privilégier pour l’indexation ?
    Il n’y a pas de taille universelle : 200–500 tokens est un bon point de départ. Utilisez fenêtre glissante pour capturer le contexte lorsque le document est long, et testez le rappel métier pour ajuster.
  • Vector DB ou moteur full-text : lequel choisir ?
    Les deux sont complémentaires : les vecteurs capturent la similarité sémantique, le full‑text (BM25) assure la précision sur les termes exacts. En production, un système hybride est souvent la meilleure option.
  • Le ré‑ranking est-il indispensable ?
    Pour les usages critiques ou métier, oui. Un cross‑encoder dédié (ré‑entrainé si possible) améliore fortement l’ordre des résultats et réduit le travail côté LLM.
  • Comment mesurer l’amélioration d’un RAG ?
    Utilisez des métriques classiques : rappel et précision sur un jeu d’exemples métier, taux d’hallucination évalué manuellement, latence et coût. Ajoutez des tests automatisés de cohérence et des checks de citations pour valider la fiabilité.

 

 

A propos de l’auteur

Franck Scandolera — expert & formateur en tracking server-side, Analytics Engineering, automatisation No/Low Code (n8n) et intégration de l’IA en entreprise. Responsable de l’agence webAnalyste et de l’organisme de formation Formations Analytics. Références clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Française de Football, Texdecor. Dispo pour aider les entreprises => contactez moi.

Retour en haut
Le Web Analyste