Guide technique

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.

18 février 202618 min de lectureÉquipe d'Ingénierie Oronts

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éfaillanceCe qui se passeFréquence
Qualité des chunksDes limites de chunks incorrectes coupent le contexte, la réponse repose sur une information partielleTrès courant
Données obsolètesIndex non mis à jour, la réponse repose sur un document périméCourant
Échec de récupérationLe document pertinent existe mais la similarité des embeddings ne le remonte pasCourant
Hallucination malgré la récupérationLe modèle ignore le contexte récupéré et génère à partir de ses données d'entraînementCourant
Débordement de la fenêtre de contexteTrop de chunks récupérés, le modèle perd le filModéré
Confusion inter-documentsDes chunks de documents différents sont mélangés, le modèle fusionne des faits contradictoiresModéré
Explosion des coûtsLes coûts d'embedding + récupération + génération augmentent avec le volume de requêtesProgressif
Pics de latenceRecherche vectorielle + reranking + génération prend trop de temps pour un usage interactifModé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ésQualité de la réponseLatenceCoût
1-2Risque de contexte manquantRapideFaible
3-5Meilleur compromis (recommandé)ModéréModéré
5-10Rendements décroissants, du bruitPlus lentPlus élevé
10+Dilution du contexte, modèle confusLentÉ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êteIndexStratégieModèle
Question produitIndex produitsHybride (texte + vecteur)Modèle rapide (GPT-4o-mini)
Question juridique/conformitéIndex politiquesVecteur uniquement (précis)Modèle précis (GPT-4o)
Support techniqueIndex base de connaissancesHybride + rerankModèle rapide
Requête multilingueIndex multilingueVecteur avec filtre de langueModè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 :

ComposantFacteur de coûtPlage typique
Embedding de la requêtePar requête (inférence modèle)0,0001 $ par requête
Recherche vectoriellePar requête (calcul + I/O)0,0005 $ par requête
RerankingPar requête * candidats (inférence modèle)0,001 $ par requête
Génération LLMTokens en entrée (contexte) + tokens en sortie0,01 - 0,10 $ par requête
Embedding de documentsUne 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 :

ÉtapeBudgetOptimisation
Analyse de la requête50 msBasée sur des règles ou petit modèle
Vérification du cache30 msIndex vectoriel en mémoire
Recherche vectorielle100 msCluster de recherche dédié
Recherche textuelle100 msEn parallèle avec la recherche vectorielle
Reranking200 msPetit cross-encoder, limiter les candidats
Assemblage du contexte10 msEn mémoire
Génération LLM1 500 msStreaming, modèle rapide
Validation de la sortie100 msBasé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énarioMeilleure approchePourquoi
FAQ statique (< 50 questions)Correspondance par mots-clés + réponse templatePlus rapide, moins cher, déterministe
Requêtes sur données structuréesRequête SQL/API + templateLe LLM ajoute de la latence et un risque d'hallucination
Données en temps réel (cours de bourse, stocks)Appel API directLes embeddings sont obsolètes par définition
Classification simpleClassificateur fine-tunéMoins cher, plus rapide, plus fiable
Résumé de documentAppel 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

  1. 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.

  2. 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.

  3. 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.

  4. 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é.

  5. 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.

  6. 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.

  7. 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.

  8. 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

RAG productionlimites du RAGfiabilité IAproblèmes retrieval augmented generationarchitecture RAGrecherche hybrideorchestration RAGhallucination RAG

Prê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