Tous les articles Patterns RAG

Au-delà du RAG basique : trois patterns pour des agents qui répondent vraiment

L'équipe RagNight · 8 min de lecture · 08 mai 2026

Le RAG en démo impressionne, en production il déçoit. Trois patterns changent tout : recherche hybride vectoriel + BM25 avec fusion RRF, reranking cross-encoder obligatoire, et décomposition des questions complexes en sous-requêtes.

Le RAG en tutoriel tient en une ligne : on embedde la question, on fait une recherche top-k, on stuffe les chunks dans le prompt, et on prie. En démo, sur dix questions choisies, ça impressionne. En production, sur des milliers de requêtes réelles posées par des utilisateurs qui ne connaissent pas votre vocabulaire interne, ça déçoit presque systématiquement. La similarité vectorielle seule rate les références exactes, remonte des passages plausibles mais hors-sujet, et s'effondre dès que la question contient plusieurs intentions.

La bonne nouvelle : on sait pourquoi, et on sait quoi faire. Trois patterns transforment un prototype RAG en système fiable. Aucun n'est exotique en 2026 : ils sont devenus le socle attendu de toute architecture de récupération sérieuse. Voici, dans l'ordre où vous devriez les implémenter, la recherche hybride, le reranking obligatoire et la décomposition de requête.

Pattern 1 : recherche hybride (vectoriel + BM25)

La recherche vectorielle comprend le sens. Demandez « comment résilier mon abonnement » et elle retrouvera un passage intitulé « mettre fin à votre contrat », même sans mot commun. C'est sa force, et c'est pour ça qu'elle a tout emporté. Mais elle a un angle mort structurel : elle écrase le littéral. Un numéro de référence INV-2024-08831, un nom de produit RagNight Enterprise, un code d'erreur PG::ConnectionBad, un nom propre rare — autant de chaînes dont l'embedding ne capture quasiment rien de distinctif. Deux références de facture différentes ont des vecteurs presque identiques. Le dense est aveugle aux détails qui comptent le plus en entreprise.

BM25, l'algorithme de ranking lexical qui équipe Elasticsearch, OpenSearch ou pg_search, fait exactement l'inverse. Il score sur la correspondance exacte des termes, pondérée par leur fréquence dans le document (TF) et leur rareté dans le corpus (IDF). Un terme rare et exact comme un numéro de série fait exploser le score BM25, là où il est invisible pour le dense. BM25 ne comprend rien au sens, mais il ne rate jamais un identifiant.

La recherche hybride n'est pas un raffinement optionnel : c'est la reconnaissance qu'une seule représentation ne peut capter à la fois le sens et le littéral. Le dense pour la sémantique, le lexical pour l'exactitude.

Fusionner les deux : RRF

Reste un problème concret : comment combiner deux listes de résultats dont les scores ne sont pas comparables ? Un score cosinus de 0,82 et un score BM25 de 14,3 ne vivent pas dans la même échelle, et normaliser des distributions aussi différentes est fragile. La réponse devenue standard est le Reciprocal Rank Fusion (RRF). RRF ignore les scores bruts et ne regarde que le rang de chaque document dans chaque liste. La formule, pour un document d :

score_RRF(d) = Σ  1 / (k + rang_i(d))
              i∈listes

k est une constante d'amortissement (typiquement 60) qui empêche les toutes premières positions d'écraser tout le reste. Un exemple chiffré, avec k = 60 :

Document Rang vectoriel Rang BM25 Calcul RRF Score
A 1 3 1/61 + 1/63 0,0323
B 2 1 1/62 + 1/61 0,0325
C 2 0 + 1/62 0,0161
D 3 1/63 + 0 0,0159

Le document B remonte en tête : il est bien classé dans les deux signaux, même sans être premier nulle part. Le document A, premier en vectoriel mais moyen en lexical, passe juste derrière. Les documents trouvés par une seule liste (C, D) restent présents mais nettement décotés. C'est exactement le comportement voulu : RRF récompense le consensus entre signaux et reste robuste sans le moindre réglage de normalisation.

def hybrid_search(query, k: 20)
  dense  = vector_search(query, limit: 50)   # liste classée par cosinus
  sparse = bm25_search(query, limit: 50)     # liste classée par BM25

  rrf = Hash.new(0.0)
  [dense, sparse].each do |results|
    results.each_with_index do |chunk, rank|
      rrf[chunk.id] += 1.0 / (60 + rank + 1)
    end
  end

  rrf.sort_by { |_id, score| -score }.first(k).map(&:first)
end

En pratique, sur la plupart des bases de connaissances d'entreprise — documentation technique, contrats, tickets support truffés d'identifiants — l'hybride apporte de l'ordre de 15 à 30 % de précision supplémentaire par rapport au vectoriel seul, le gain étant d'autant plus marqué que le corpus contient de jargon, de codes et de références exactes.

Pattern 2 : le reranking n'est pas optionnel

Voici la vérité inconfortable : le top-k de votre vector store n'est pas votre vrai top-k. Les cinq premiers résultats d'une recherche dense ou hybride sont suffisamment proches de la requête, pas les plus pertinents. La cause est architecturale.

Bi-encoder contre cross-encoder

Une recherche vectorielle repose sur un bi-encoder : la requête et chaque document sont encodés séparément en vecteurs, puis comparés par produit scalaire. C'est ce qui rend la recherche rapide — on pré-calcule tous les embeddings de documents, et à la requête il ne reste qu'une comparaison vectorielle sur un index HNSW. Mais la requête et le document ne se « voient » jamais : leur interaction se résume à un seul nombre, la similarité. Les nuances se perdent.

Un cross-encoder fait le contraire : il prend la paire (requête, document) ensemble en entrée d'un même modèle Transformer, qui peut faire attention à chaque mot de la question face à chaque mot du passage. Le résultat est un score de pertinence bien plus fin. Le coût : impossible de pré-calculer quoi que ce soit ; il faut une passe d'inférence par paire. Scorer un million de documents serait absurde — mais en scorer 50 est trivial.

Bi-encoder pour récupérer large et vite. Cross-encoder pour reclasser fin et juste. C'est l'entonnoir : récupérez 50 candidats grossièrement, reclassez-en 50 finement, gardez-en 5.

L'entonnoir récupérer-large / reranker-fin

def retrieve(query)
  candidates = hybrid_search(query, k: 50)        # large, rapide, imprécis
  reranked   = reranker.rerank(query, candidates) # fin, lent, précis
  reranked.first(5)                               # le vrai top-k
end

Le reranker corrige les erreurs du premier étage. Un passage qui contenait les bons mots-clés mais répondait à côté est rétrogradé ; un passage classé 23ᵉ par le bi-encoder mais qui répond exactement à la question remonte au sommet. C'est l'étape qui, à elle seule, fait le plus pour la qualité perçue d'un RAG.

Côté modèles concrets en 2026 :

  • Cohere Rerank 3 — API managée, multilingue, excellent rapport qualité/latence, le défaut raisonnable si vous acceptez un appel externe.
  • bge-reranker-v2-m3 (BAAI) — open-weights, multilingue, auto-hébergeable. Le choix souveraineté : il tourne sur votre propre GPU, aucune donnée ne sort. Qualité très solide pour un modèle ouvert.
  • Voyage rerank — autre option API de bonne facture, souvent compétitive sur les corpus techniques.

Le reranking ajoute une latence (quelques dizaines à quelques centaines de millisecondes selon le modèle et le nombre de candidats), mais sur 50 candidats elle reste maîtrisable et le gain de pertinence la justifie presque toujours. Réglez le nombre de candidats récupérés (40–60 est un bon point de départ) pour arbitrer entre rappel et coût.

Pattern 3 : décomposition pour les questions complexes

Une recherche unique répond bien à une question unique. Le problème : les vraies questions d'utilisateurs sont souvent plusieurs questions déguisées. Prenez :

« Comment notre politique de remboursement se compare-t-elle à celle d'AWS ? »

Aucun chunk de votre base ne contient cette comparaison — elle n'existe nulle part en tant que telle. C'est en réalité trois questions :

  1. Quelle est notre politique de remboursement ?
  2. Quelle est la politique de remboursement d'AWS ?
  3. La synthèse comparative des deux.

Une recherche unique sur la question entière produira un embedding « moyen » qui ne ressemble à aucun des documents pertinents, et remontera probablement un fourre-tout médiocre. La décomposition résout ça par un pas à pas :

  1. Planifier — un LLM décompose la question en sous-questions atomiques.
  2. Récupérer — chaque sous-question déclenche sa propre recherche hybride + reranking (les patterns 1 et 2, réutilisés).
  3. Composer — le LLM reçoit les contextes de chaque sous-question et rédige la réponse comparative finale.
def answer_complex(question)
  sub_questions = planner.decompose(question)
  # => ["Notre politique de remboursement ?",
  #     "Politique de remboursement d'AWS ?"]

  contexts = sub_questions.map do |sq|
    { question: sq, chunks: retrieve(sq) }   # hybride + rerank par sous-question
  end

  composer.synthesize(question, contexts)
end

La décomposition est la première marche vers l'agentic RAG : plutôt qu'un pipeline figé, un agent décide quoi récupérer, évalue si les résultats suffisent, lance des recherches supplémentaires si besoin, et choisit quand s'arrêter. La décomposition statique en sous-questions en est la version la plus simple et la plus prévisible — un excellent point de départ avant de donner à l'agent une vraie latitude de décision. Réservez-la aux questions qui en ont besoin : détectez les requêtes multi-intentions (comparaisons, énumérations, conditions composées) et laissez les questions simples sur le chemin rapide à une seule recherche, pour ne pas payer de latence inutile.

Décomposez quand la question contient plusieurs intentions ; ne décomposez pas une question simple. Le coût d'une mauvaise décomposition est réel : latence multipliée et risque de dilution du contexte.

Pourquoi nous en parlons

Ces trois patterns ne sont pas des options de configuration avancées : ce sont les fondations d'un RAG qui répond vraiment, par opposition à un RAG qui démo bien. Recherche hybride pour ne plus jamais rater une référence exacte. Reranking pour que votre top-5 soit le vrai top-5. Décomposition pour les questions qui en cachent plusieurs.

RagNight implémente ces trois patterns par défaut. La recherche hybride avec fusion RRF, le reranking cross-encoder (modèle managé ou open-weights auto-hébergé pour les contraintes de souveraineté) et la décomposition des requêtes complexes sont câblés dans le pipeline de récupération. Vous ne devriez pas avoir à les recoder à chaque projet — et vous ne devriez pas avoir à les redécouvrir en production.

Pour aller plus loin

  • Architecture RAG de production : du chunking au reranking, le guide complet
  • Recherche hybride et reranking : pourquoi la similarité vectorielle seule échoue
  • Agentic RAG : quand l'agent décide quoi récupérer (et quand s'arrêter)

Prêt à brancher vos agents sur vos données ?

Démarrez gratuitement. Premier audit Knowledge Pulse en 60 secondes.

Démarrer gratuitement