Le RAG ne suffit pas : ce que les systèmes IA fiables nécessitent en plus
Où le RAG casse en production et quoi construire par-dessus. Qualité des chunks, couche d'orchestration, recherche hybride, limites d'hallucination, gestion des coûts, et quand ne pas utiliser le RAG.
Le piège de la démo RAG
Toutes les démos RAG fonctionnent. Tu uploades quelques PDF, tu les découpes en chunks, tu les transformes en embeddings, tu lances une requête, tu obtiens une réponse. La démo est toujours impressionnante. Les parties prenantes sont enthousiastes. L'équipe estime deux semaines pour la mise en production.
Six mois plus tard, le système est toujours instable. Les utilisateurs reçoivent des réponses fausses basées sur des chunks obsolètes. Le modèle cite en toute confiance des documents qui disent le contraire de la réponse affichée. Les coûts ont été multipliés par 10 par rapport à l'estimation initiale. Et personne ne comprend pourquoi la même question produit des réponses différentes selon le moment de la journée.
Le RAG n'est pas une solution. Le RAG est un pattern de récupération. Un système IA fiable a besoin d'une couche d'orchestration, de contrôles de qualité, de recherche hybride, de limites d'hallucination, de gestion des coûts et de monitoring par-dessus le RAG. Cet article couvre ce que nous avons appris en construisant des systèmes RAG en production.
Pour un contexte plus large sur l'architecture RAG en entreprise et la recherche vectorielle, ces guides couvrent les patterns fondamentaux. Cet article se concentre sur là où ces patterns cassent et ce dont tu as besoin au-delà.
Où le RAG casse
| Mode de défaillance | Ce qui se passe | Fréquence |
|---|---|---|
| Qualité des chunks | Des limites de chunks incorrectes coupent le contexte, la réponse repose sur une information partielle | Très courant |
| Données obsolètes | Index non mis à jour, la réponse repose sur un document périmé | Courant |
| Échec de récupération | Le document pertinent existe mais la similarité des embeddings ne le remonte pas | Courant |
| Hallucination malgré la récupération | Le modèle ignore le contexte récupéré et génère à partir de ses données d'entraînement | Courant |
| Débordement de la fenêtre de contexte | Trop de chunks récupérés, le modèle perd le fil | Modéré |
| Confusion inter-documents | Des chunks de documents différents sont mélangés, le modèle fusionne des faits contradictoires | Modéré |
| Explosion des coûts | Les coûts d'embedding + récupération + génération augmentent avec le volume de requêtes | Progressif |
| Pics de latence | Recherche vectorielle + reranking + génération prend trop de temps pour un usage interactif | Modéré |
La qualité des chunks, c'est tout
Le problème le plus sous-estimé. Si tes chunks coupent un paragraphe au milieu d'une idée, le contexte récupéré est incomplet. Si tes chunks sont trop grands, du contenu non pertinent dilue l'information utile. Si tes chunks ne préservent pas la structure du document (titres, tableaux, listes), le modèle perd le contexte organisationnel.
// Mauvais : les chunks de taille fixe cassent le contexte
function naiveChunk(text: string, size: number): string[] {
const chunks = [];
for (let i = 0; i < text.length; i += size) {
chunks.push(text.slice(i, i + size));
}
return chunks;
// Problème : coupe les phrases, paragraphes, tableaux en plein milieu
}
// Mieux : chunking sémantique avec recouvrement
function semanticChunk(text: string, options: ChunkOptions): Chunk[] {
const sections = splitByHeadings(text); // Respecter la structure du document
const paragraphs = sections.flatMap(s =>
splitByParagraphs(s, { maxSize: options.maxChunkSize })
);
return paragraphs.map((p, i) => ({
content: p.text,
metadata: {
section: p.sectionTitle,
pageNumber: p.pageNumber,
documentId: p.documentId,
position: i,
},
// Recouvrement : inclure les 2 dernières phrases du chunk précédent
prefix: i > 0 ? getLastSentences(paragraphs[i - 1].text, 2) : '',
}));
}
Le recouvrement est important. Sans lui, une question qui chevauche deux chunks obtient un contexte partiel de chaque côté et une réponse complète de nulle part. Avec un recouvrement de 2 à 3 phrases, le modèle a assez de contexte pour faire le pont entre les limites de chunks.
Les métadonnées des chunks sont tout aussi critiques. Chaque chunk doit porter l'ID du document source, le titre de la section, le numéro de page et la position. Sans métadonnées, tu ne peux pas dire à l'utilisateur d'où vient la réponse. Sans attribution de source, la réponse n'est pas vérifiable.
Qualité de récupération vs quantité de récupération
Récupérer plus de chunks ne veut pas dire de meilleures réponses. En pratique, nous avons constaté que 3 à 5 chunks de haute qualité surpassent systématiquement 10 à 15 chunks médiocres.
| Chunks récupérés | Qualité de la réponse | Latence | Coût |
|---|---|---|---|
| 1-2 | Risque de contexte manquant | Rapide | Faible |
| 3-5 | Meilleur compromis (recommandé) | Modéré | Modéré |
| 5-10 | Rendements décroissants, du bruit | Plus lent | Plus élevé |
| 10+ | Dilution du contexte, modèle confus | Lent | Élevé |
La solution : récupérer largement, puis reranker agressivement.
async function retrieveAndRerank(query: string, options: RetrievalOptions) {
// Étape 1 : Récupération large (obtenir 20 candidats)
const candidates = await vectorStore.search(query, { limit: 20 });
// Étape 2 : Reranking avec un cross-encoder (scorer chaque candidat par rapport à la requête)
const reranked = await reranker.rank(query, candidates, {
model: 'cross-encoder/ms-marco-MiniLM-L-12-v2',
});
// Étape 3 : Prendre les 5 meilleurs après reranking
const topChunks = reranked.slice(0, 5);
// Étape 4 : Filtrer par score de pertinence minimum
return topChunks.filter(c => c.score > options.minRelevanceScore);
}
Le reranker est un modèle cross-encoder qui évalue chaque candidat par rapport à la requête avec une précision bien supérieure à la similarité cosinus sur les embeddings. C'est plus lent (il fait une inférence par candidat), mais l'amélioration de la qualité est substantielle. Le lancer sur 20 candidats pour en sélectionner 5 ajoute 100 à 200 ms de latence, ce qui est acceptable pour la plupart des cas d'usage.
La couche d'orchestration dont le RAG a besoin
Le RAG brut, c'est : transformer la requête en embedding, chercher dans les vecteurs, injecter le contexte dans le prompt, générer. Un système en production a besoin d'une couche d'orchestration entre la récupération et la génération.
Requête utilisateur
│
▼
┌──────────────────┐
│ Analyse de │ Classifier l'intention, extraire les entités, détecter la langue
│ la requête │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Routage │ Quel index ? Quelle stratégie de récupération ? Cache hit ?
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Récupération │ Recherche vectorielle + recherche par mots-clés (hybride)
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Reranking │ Scoring cross-encoder, filtrer les chunks peu pertinents
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Assemblage du │ Ordonner les chunks, ajouter les métadonnées, respecter le budget de tokens
│ contexte │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Génération │ Appel LLM avec le contexte assemblé + prompt système
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Validation de │ Vérifier les hallucinations, valider les citations, scan PII
│ la sortie │
└────────┬─────────┘
│
▼
Réponse
Analyse de la requête
Toutes les requêtes n'ont pas besoin du RAG. Certaines sont conversationnelles ("bonjour", "merci"). Certaines portent sur le système lui-même ("comment utiliser cet outil ?"). Certaines sont ambiguës et nécessitent une clarification. L'analyseur de requête classifie l'intention avant de déclencher la récupération.
async function analyzeQuery(query: string): Promise<QueryAnalysis> {
// Classification rapide (peut être un petit modèle ou basé sur des règles)
const intent = await classifyIntent(query);
if (intent === 'greeting' || intent === 'meta') {
return { needsRetrieval: false, intent, response: getStaticResponse(intent) };
}
if (intent === 'ambiguous') {
return { needsRetrieval: false, intent, clarificationNeeded: true };
}
return {
needsRetrieval: true,
intent,
extractedEntities: await extractEntities(query),
detectedLanguage: await detectLanguage(query),
};
}
Routage
Différentes requêtes peuvent nécessiter différents index, différentes stratégies de récupération, ou différents modèles.
| Type de requête | Index | Stratégie | Modèle |
|---|---|---|---|
| Question produit | Index produits | Hybride (texte + vecteur) | Modèle rapide (GPT-4o-mini) |
| Question juridique/conformité | Index politiques | Vecteur uniquement (précis) | Modèle précis (GPT-4o) |
| Support technique | Index base de connaissances | Hybride + rerank | Modèle rapide |
| Requête multilingue | Index multilingue | Vecteur avec filtre de langue | Modèle multilingue |
Assemblage du contexte
Après la récupération et le reranking, les chunks doivent être assemblés dans un prompt qui respecte le budget de tokens du modèle.
function assembleContext(chunks: RankedChunk[], tokenBudget: number): string {
let context = '';
let tokensUsed = 0;
for (const chunk of chunks) {
const chunkTokens = estimateTokens(chunk.content);
if (tokensUsed + chunkTokens > tokenBudget) break;
context += `\n\n---\nSource: ${chunk.metadata.documentTitle} (${chunk.metadata.section})\n`;
context += chunk.content;
tokensUsed += chunkTokens;
}
return context;
}
Le budget de tokens doit tenir compte du prompt système, de la requête utilisateur, du contexte assemblé ET de la longueur de réponse attendue. Une erreur classique est de remplir toute la fenêtre de contexte avec des chunks récupérés, sans laisser de place pour une réponse de qualité.
Recherche hybride : texte + vecteur
La recherche vectorielle pure rate les requêtes spécifiques à des mots-clés. Un utilisateur qui cherche "code erreur E-4021" obtiendra de mauvais résultats avec la similarité d'embeddings parce que les codes erreur ne sont pas sémantiquement significatifs. La recherche textuelle pure rate les requêtes sémantiques. Un utilisateur qui cherche "comment résoudre les problèmes de connexion" ne trouvera pas un document intitulé "Guide de dépannage de l'authentification."
La recherche hybride combine les deux :
async function hybridSearch(query: string, options: SearchOptions) {
// Exécution parallèle
const [vectorResults, textResults] = await Promise.all([
vectorStore.search(query, { limit: options.vectorLimit }),
textIndex.search(query, { limit: options.textLimit }),
]);
// Reciprocal Rank Fusion (RRF) pour fusionner les résultats
const merged = reciprocalRankFusion(vectorResults, textResults, {
vectorWeight: 0.6,
textWeight: 0.4,
});
return merged.slice(0, options.totalLimit);
}
function reciprocalRankFusion(
vectorResults: SearchResult[],
textResults: SearchResult[],
weights: { vectorWeight: number; textWeight: number },
): SearchResult[] {
const scores = new Map<string, number>();
const k = 60; // Constante RRF
vectorResults.forEach((result, rank) => {
const score = (scores.get(result.id) || 0) + weights.vectorWeight / (k + rank + 1);
scores.set(result.id, score);
});
textResults.forEach((result, rank) => {
const score = (scores.get(result.id) || 0) + weights.textWeight / (k + rank + 1);
scores.set(result.id, score);
});
return Array.from(scores.entries())
.sort(([, a], [, b]) => b - a)
.map(([id, score]) => ({ id, score }));
}
Le ratio de pondération (vecteur 0.6, texte 0.4) est un point de départ. Ajuste-le en fonction de la distribution de tes requêtes. Si la plupart des requêtes sont orientées mots-clés (SKU produit, codes erreur), augmente le poids du texte. Si la plupart des requêtes sont en langage naturel, augmente le poids du vecteur.
Pour en savoir plus sur l'architecture de recherche dans des contextes e-commerce, consulte notre guide des plateformes e-commerce.
Limites d'hallucination
Le RAG réduit l'hallucination par rapport à la génération LLM pure. Il ne l'élimine pas. Le modèle peut encore :
- Ignorer le contexte récupéré et générer à partir de ses données d'entraînement
- Fusionner incorrectement des informations provenant de plusieurs chunks
- Inventer des citations qui n'existent pas dans le contexte récupéré
- Extrapoler au-delà de ce que le contexte supporte
Stratégies d'atténuation
1. Prompts système contraints :
Tu es un assistant de support. Réponds UNIQUEMENT en te basant sur le contexte fourni.
Si le contexte ne contient pas assez d'informations pour répondre, dis
"Je n'ai pas assez d'informations pour répondre à cette question."
N'utilise PAS d'informations provenant de tes données d'entraînement.
Chaque affirmation doit référencer une source spécifique du contexte.
2. Vérification des citations :
async function verifyCitations(response: string, chunks: RankedChunk[]): VerificationResult {
const citations = extractCitations(response);
const verified = [];
const unverified = [];
for (const citation of citations) {
const found = chunks.some(chunk =>
chunk.content.includes(citation.claimedText) ||
fuzzyMatch(chunk.content, citation.claimedText, 0.85)
);
(found ? verified : unverified).push(citation);
}
return {
allVerified: unverified.length === 0,
verified,
unverified,
confidenceScore: verified.length / (verified.length + unverified.length),
};
}
3. Score de confiance :
Si la réponse du modèle ne s'aligne pas bien avec le contexte récupéré (faible chevauchement, pas de citations directes), marque-la comme ayant un faible niveau de confiance. Affiche un avertissement à l'utilisateur ou escalade vers un humain.
Pour en savoir plus sur les modes de défaillance de l'IA et comment les gérer, consulte notre guide des modes de défaillance IA.
Coûts et latence
Les coûts du RAG augmentent avec le volume de requêtes sur trois dimensions :
| Composant | Facteur de coût | Plage typique |
|---|---|---|
| Embedding de la requête | Par requête (inférence modèle) | 0,0001 $ par requête |
| Recherche vectorielle | Par requête (calcul + I/O) | 0,0005 $ par requête |
| Reranking | Par requête * candidats (inférence modèle) | 0,001 $ par requête |
| Génération LLM | Tokens en entrée (contexte) + tokens en sortie | 0,01 - 0,10 $ par requête |
| Embedding de documents | Une seule fois par document (à l'ingestion) | 0,0001 $ par page |
La génération LLM domine le coût. Réduire la taille du contexte (moins de chunks, des chunks plus courts) réduit directement le composant le plus coûteux.
Stratégies de cache
// Cache sémantique : mettre en cache les réponses pour des requêtes similaires
async function cachedQuery(query: string): Promise<string | null> {
// Transformer la requête en embedding
const queryEmbedding = await embedder.embed(query);
// Chercher dans l'index de cache des requêtes similaires
const cached = await cacheIndex.search(queryEmbedding, {
minSimilarity: 0.95, // Seuil élevé pour les cache hits
limit: 1,
});
if (cached.length > 0) {
return cached[0].response; // Cache hit
}
return null; // Cache miss, continuer avec le pipeline RAG complet
}
Le cache sémantique fonctionne parce que beaucoup d'utilisateurs posent des questions similaires de manières légèrement différentes. "Comment réinitialiser mon mot de passe ?" et "Instructions de réinitialisation du mot de passe" sont des chaînes de caractères différentes mais sémantiquement identiques. Un seuil de similarité de 0,95 garantit que seules des requêtes quasi identiques obtiennent des réponses mises en cache.
Budget de latence
Pour un usage interactif (chatbot, assistant de support), le pipeline complet doit s'exécuter en moins de 3 secondes :
| Étape | Budget | Optimisation |
|---|---|---|
| Analyse de la requête | 50 ms | Basée sur des règles ou petit modèle |
| Vérification du cache | 30 ms | Index vectoriel en mémoire |
| Recherche vectorielle | 100 ms | Cluster de recherche dédié |
| Recherche textuelle | 100 ms | En parallèle avec la recherche vectorielle |
| Reranking | 200 ms | Petit cross-encoder, limiter les candidats |
| Assemblage du contexte | 10 ms | En mémoire |
| Génération LLM | 1 500 ms | Streaming, modèle rapide |
| Validation de la sortie | 100 ms | Basée sur des règles + petit modèle |
| Total | ~2 100 ms |
Streamer la réponse du LLM vers l'utilisateur pendant que la génération est en cours rend la latence perçue bien plus faible. L'utilisateur voit les premiers tokens en 300 à 500 ms même si la réponse complète prend 1 500 ms.
Quand ne pas utiliser le RAG du tout
Le RAG n'est pas toujours le bon pattern. Parfois, des approches plus simples fonctionnent mieux :
| Scénario | Meilleure approche | Pourquoi |
|---|---|---|
| FAQ statique (< 50 questions) | Correspondance par mots-clés + réponse template | Plus rapide, moins cher, déterministe |
| Requêtes sur données structurées | Requête SQL/API + template | Le LLM ajoute de la latence et un risque d'hallucination |
| Données en temps réel (cours de bourse, stocks) | Appel API direct | Les embeddings sont obsolètes par définition |
| Classification simple | Classificateur fine-tuné | Moins cher, plus rapide, plus fiable |
| Résumé de document | Appel LLM direct (sans récupération) | Le document complet EST le contexte |
Le RAG est pertinent quand tu as une large base de connaissances (des centaines à des milliers de documents), des requêtes en langage naturel qui ne peuvent pas être traitées par correspondance de mots-clés, et un besoin de réponses synthétisées à partir de sources multiples. Si ton cas d'usage ne correspond pas à ce profil, une approche plus simple sera plus fiable et moins chère.
Pour voir comment nous abordons ces décisions d'architecture dans notre pratique de services IA, et pour des patterns plus larges dans la conception de workflows IA, ces pages fournissent plus de contexte.
Pièges courants
-
Chunking à taille fixe. Des chunks qui coupent des paragraphes, tableaux ou blocs de code en plein milieu produisent une récupération inutilisable. Utilise un chunking sémantique qui respecte la structure du document.
-
Pas de recouvrement entre chunks. Sans recouvrement, les requêtes qui chevauchent les limites de chunks obtiennent un contexte partiel de chaque côté et une réponse complète de nulle part.
-
Pas de reranking. La similarité d'embeddings est un filtre grossier. Un reranker cross-encoder améliore drastiquement la qualité des 5 meilleurs résultats.
-
Remplir toute la fenêtre de contexte. Laisse de la place pour le prompt système, la requête utilisateur ET la réponse attendue. Un prompt bourré de 15 chunks ne laisse aucune place pour une réponse de qualité.
-
Pas de recherche hybride. La recherche vectorielle pure échoue sur les requêtes par mots-clés (codes erreur, SKU produit). La recherche textuelle pure échoue sur les requêtes sémantiques. Utilise les deux.
-
Pas de cache sémantique. Des questions similaires posées par différents utilisateurs déclenchent le pipeline RAG complet à chaque fois. Un cache sémantique avec un seuil de similarité de 0,95 réduit significativement les coûts.
-
Faire confiance au RAG sans vérification. Le RAG réduit l'hallucination. Il ne l'élimine pas. Vérifie les citations par rapport au contexte récupéré. Signale les affirmations non vérifiées.
-
Pas de monitoring. Tu dois suivre la qualité de la récupération (les bons chunks ont-ils été récupérés ?), la qualité des réponses (l'utilisateur a-t-il trouvé la réponse utile ?), la latence, le coût par requête et le taux de cache hit.
Points clés à retenir
-
Le RAG est un pattern de récupération, pas une solution. Un système fiable a besoin d'analyse de requête, de routage, de recherche hybride, de reranking, d'assemblage du contexte, de génération et de validation de la sortie par-dessus la récupération.
-
La qualité des chunks détermine la qualité des réponses. Le chunking sémantique avec recouvrement, métadonnées et préservation de la structure du document est la fondation. Tout le reste est construit par-dessus des chunks de qualité.
-
Récupérer largement, reranker agressivement. Obtiens 20 candidats avec la similarité d'embeddings. Score-les avec un cross-encoder. Prends les 5 meilleurs. Filtre par pertinence minimale.
-
La recherche hybride gère ce que les vecteurs ratent. Les mots-clés, codes erreur, ID produit et correspondances exactes nécessitent la recherche textuelle. Les requêtes sémantiques nécessitent la recherche vectorielle. Utilise les deux avec la fusion par rang réciproque.
-
La génération LLM domine le coût. Réduire la taille du contexte (moins de chunks, de meilleurs chunks) est l'optimisation de coût la plus efficace.
-
Parfois le RAG est le mauvais pattern. Les FAQ statiques, les requêtes sur données structurées, les données en temps réel et la classification simple ont toutes des solutions plus simples et plus fiables.
Nous construisons des systèmes RAG en production dans le cadre de nos pratiques de services IA et d'ingénierie des données. Si tu construis un système RAG ou que tu débogues un système instable, parle à notre équipe ou demande un devis. Tu peux aussi explorer notre page méthodologie pour voir comment nous abordons les projets IA.
Sujets couverts
Guides connexes
Systemes RAG Enterprise : Une Plongee Technique Approfondie
Guide technique pour construire des systemes RAG prets pour la production. Decouvre les strategies de chunking, modeles d'embedding et recherche hybride.
Lire le guideGuide Entreprise des Systèmes d'IA Agentiques
Guide technique des systemes d'IA agentiques en entreprise. Decouvre l'architecture, les capacites et les applications des agents IA autonomes.
Lire le guideCommerce Agentique : Comment laisser les agents IA acheter en toute securite
Comment concevoir un commerce agentique gouverne. Moteurs de politiques, portes d'approbation HITL, reçus HMAC, idempotence, isolation multi-tenant et le protocole Agentic Checkout complet.
Lire le guidePrêt à construire des systèmes IA prêts pour la production ?
Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.
Démarrer une conversation