Aller au contenu
🔎 RAG #01

Anatomie d'un système RAG : du chunk à la réponse

Le pipeline RAG démonté pièce par pièce. 5 composants, l'analogie infra, le code minimal. Pour comprendre ce qui se passe quand tu interroges un copilote RAG.

18 min de lecture Publié le 25 avril 2026 Révisé le 12 septembre 2026 v2.2

Accroche

Tu as compris dans RAG-00 pourquoi le LLM a besoin d’une mémoire externe : il oublie après son cutoff training, il hallucine quand on lui demande des données qu’il n’a jamais vues, et on ne peut pas tout coller dans son prompt à cause de la fenêtre de contexte. La solution s’appelle RAG : on cherche les passages utiles dans tes documents, on les colle au prompt, le LLM répond en s’appuyant dessus.

Cette leçon démonte la machine. Cinq composants ordonnés : chunking → embedding → vector store → retrieval → augmentation+génération. À la fin, tu sauras nommer chaque pièce, dire ce qu’elle fait, et savoir laquelle changer en premier quand le copilote te répond mal.

Vue d’ensemble : deux pipelines, pas un

Première chose à voir : un RAG, ce ne sont pas cinq étapes en série exécutées à chaque question. Ce sont deux pipelines distincts qui partagent un même store.

L’indexation tourne hors-ligne, une fois (ou ré-exécutée quand le corpus bouge). Elle prépare la mémoire : découper les documents, calculer les embeddings, stocker dans un vector store.

La requête tourne en ligne, à chaque question utilisateur. Elle utilise la mémoire préparée : embedder la question, chercher les chunks proches, augmenter le prompt, générer la réponse.

Analogie infra : c’est exactement le pattern cache + endpoint API. Tu remplis le cache au build, hors trafic (indexation, lente). Tu interroges le cache au runtime, sous trafic (requête, rapide). La logique est différente entre les deux côtés, et c’est normal.

Schéma : Documents, chunking, embedding, Vector store, Question, retrieval, augment + génère, Réponse + sourcesREQUÊTE (online)INDEXATION (offline)lookupDocumentschunkingembeddingVector storeQuestionembeddingretrievalaugment + génèreRéponse + sources
Le schéma en texte
  • INDEXATION (offline) : Documents, chunking, embedding, Vector store
  • REQUÊTE (online) : Question, embedding, retrieval, augment + génère, Réponse + sources
  • Documents → chunking
  • chunking → embedding
  • embedding → Vector store
  • Question → embedding
  • embedding → retrieval
  • retrieval → augment + génère
  • augment + génère → Réponse + sources
  • Vector store → retrieval (lookup)
En clair

Indexation à gauche, requête à droite. Les deux pipelines se rencontrent au vector store : l’indexation y dépose, la requête y lit. Tout le reste de la complexité RAG (hybrid search, reranking, query rewriting…) sont des optimisations autour de ce squelette.

Composant 1 — Chunking : découper les documents

Tes documents sources sont rarement de la bonne taille pour le LLM. Un PDF de 60 pages, un wiki de 20k mots, un transcript de réunion : ça ne tient pas dans la fenêtre de contexte (rappel RAG-00 : buffer fixe), et même si ça tenait, on ne veut pas tout envoyer (coût + dilution).

On les découpe en chunks — typiquement 200 à 800 tokens chacun.

Définition : un chunk, c’est un morceau de document que le système RAG manipule comme unité atomique. Analogie infra : c’est l’équivalent d’un fragment HTTP, mais découpé sur un critère sémantique (paragraphe, section) au lieu d’un critère de transport (taille de paquet).

Trois stratégies dominent. Fixed-size : on coupe tous les N tokens, brut, rapide, aveugle aux frontières du texte. Recursive : on tente de couper aux séparateurs hiérarchiques (\n\n, puis \n, puis .) pour respecter la structure. Semantic : on coupe quand le sens change (similarité phrase-à-phrase qui chute), plus coûteux mais retrieval de meilleure qualité. Les détails : RAG-02.

Schéma : Document ~60 pages, split(), chunk 1 ~500 tok, chunk 2 ~500 tok, chunk 3 ~500 tokDocument~60 pagessplit()chunk 1~500 tokchunk 2~500 tokchunk 3~500 tok
Le schéma en texte
  • Document ~60 pages → split()
  • split() → chunk 1 ~500 tok
  • split() → chunk 2 ~500 tok
  • split() → chunk 3 ~500 tok

Code minimal — split fixe naïf, version 5 lignes :

# Découpe un texte long en morceaux de taille fixe (en caractères, pas en tokens — version naïve).
# `text` : le document complet, en string
# `size` : nombre de caractères par chunk (500 ≈ 100-150 tokens en français)
def split_fixed(text: str, size: int = 500) -> list[str]:
    # On parcourt le texte par pas de `size` caractères et on collecte chaque tranche
    return [text[i:i+size] for i in range(0, len(text), size)]
En clair

La taille de chunk est un compromis. Trop gros → retrieval imprécis (on ramène beaucoup de bruit autour de l’info utile). Trop petit → contexte perdu (la phrase pertinente arrive sans son cadre argumentatif). Cible courante : 200-500 tokens.

Composant 2 — Embedding : transformer le texte en vecteur

Maintenant que tu as N chunks, comment les rendre cherchables par sens et pas par mots-clés ? Réponse : tu les transformes en embeddings.

Définition : un embedding, c’est la représentation d’un texte sous forme de vecteur — une liste de nombres à virgule (typiquement 768, 1024 ou 1536 valeurs). L’invariant garanti par le modèle : deux textes proches en sens donnent deux vecteurs proches en géométrie.

L’analogie est centrale. Imagine que chaque chunk est transformé en une flèche dans un espace à N dimensions. Deux phrases qui parlent de la même chose pointent dans la même direction. Deux phrases sans rapport pointent dans des directions très différentes.

Analogie infra : un embedding, c’est un hash, mais sémantique. Avec MD5 ou SHA, changer un caractère à l’entrée fait exploser tout le hash. Avec un embedding, changer un mot par un synonyme produit un vecteur quasi-identique. Le hash classique préserve l’identité littérale ; l’embedding préserve la proximité de sens.

Définition : la dimension, c’est le nombre de valeurs dans le vecteur (768, 1024, 1536, 3072 selon le modèle). Plus la dimension est élevée, plus le modèle peut distinguer de nuances, mais plus le stockage et la comparaison coûtent. Analogie : c’est comme un vecteur 3D mais en 1024D — impossible à dessiner, mais ça marche pareil mathématiquement.

Schéma : Chunk 1 'la capitale française', embed(), Chunk 2 'Paris, ville centrale', Chunk 3 'recette du pain', vec1 [0.12, -0.4, ...], vec2 [0.13, -0.39, ...], vec3 [0.88, 0.21, ...]Chunk 1'la capitale française'embed()Chunk 2'Paris, ville centrale'Chunk 3'recette du pain'vec1[0.12, -0.4, ...]vec2[0.13, -0.39, ...]vec3[0.88, 0.21, ...]
Le schéma en texte
  • Chunk 1 'la capitale française' → embed()
  • Chunk 2 'Paris, ville centrale' → embed()
  • Chunk 3 'recette du pain' → embed()
  • embed() → vec1 [0.12, -0.4, ...]
  • embed() → vec2 [0.13, -0.39, ...]
  • embed() → vec3 [0.88, 0.21, ...]

Sur ce schéma, vec1 et vec2 sont presque identiques (sens proche), vec3 est très différent (autre sujet).

Code minimal :

# Modèle d'embedding pré-entraîné (BGE-M3 = open-source 2024, ~2 Go en mémoire, tourne sur CPU ; détails RAG-03).
# `embed_model.embed()` prend une string, retourne une liste de 1024 floats : le "vecteur".
from somelib import embed_model  # placeholder — détails dans RAG-03
vector = embed_model.embed("Texte à transformer en vecteur")
# vector = [0.123, -0.456, 0.789, ..., 0.234]   →   1024 floats au total

Outils cités : BGE-M3 et all-MiniLM-L6-v2 sont deux modèles d’embedding open-source courants — petits, hébergeables localement. Pour le choix selon corpus et langue : RAG-03.

Composant 3 — Vector store : stocker les vecteurs

Tu as maintenant N vecteurs (10 000 chunks → 10 000 vecteurs de 1024 floats = environ 40 Mo en mémoire). Tu pourrais les garder dans une liste Python et tout comparer à chaque requête, mais ça scale mal : à 1 million de vecteurs, comparer linéairement prend des secondes.

On utilise donc un vector store : une base de données spécialisée pour stocker des vecteurs et chercher rapidement les plus proches d’un vecteur-cible.

Définition : un vector store, c’est un index optimisé pour la recherche par similarité géométrique. Analogie infra : un Redis, mais indexé par similarité au lieu de clé. Au lieu de GET key123 on fait SEARCH "vecteur le plus proche de X".

Quatre options courantes en 2026, citées en une phrase chacune (détails RAG-04) :

  • sqlite-vec : extension SQLite pour vecteurs, fichier unique, idéal prototype local.
  • FAISS : librairie C++ Facebook AI, in-memory, ultra-rapide, pas de durabilité native.
  • Qdrant : service standalone avec API REST/gRPC, robuste production cloud.
  • pgvector : extension PostgreSQL qui ajoute le type colonne vector(N) au SQL classique.
Schéma : add(vec, id), Vector store (N vecteurs indexés), search(query_vec, k=5), top_k ids + scoresadd(vec, id)Vector store(N vecteurs indexés)search(query_vec, k=5)top_k ids+ scores
Le schéma en texte
  • add(vec, id) → Vector store (N vecteurs indexés)
  • search(query_vec, k=5) → Vector store (N vecteurs indexés)
  • Vector store (N vecteurs indexés) → top_k ids + scores

Le vector store gère deux opérations : ajouter un vecteur avec son identifiant (à l’indexation), et chercher les k vecteurs les plus proches d’un vecteur donné (à la requête). Le reste — algorithmes de recherche approximative (ANN, Approximate Nearest Neighbor, équivalent d’un index B-tree mais pour vecteurs), persistance, filtrage métadonnées — varie selon l’outil.

Composant 4 — Retrieval : retrouver les chunks pertinents

À la requête, l’utilisateur pose une question. On l’embedde avec le même modèle que celui utilisé à l’indexation (sinon les espaces vectoriels ne sont pas comparables — erreur fréquente après upgrade). Puis on demande au vector store les top_k chunks les plus proches.

Définition : le retrieval, c’est l’étape qui rapporte les chunks pertinents à partir de la question. Analogie infra : un cache lookup, mais par similarité au lieu d’égalité de clé. La similarité remplace le ==.

Définition : top_k, c’est le nombre de chunks que tu demandes au vector store. Cible typique : k = 5 à 20. Trop petit = pas assez de matière, trop grand = bruit qui dilue.

Comment le vector store mesure la proximité entre deux vecteurs ? Avec la similarité cosinus.

Définition (intuition) : la similarité cosinus, c’est l’angle entre deux flèches. Si elles pointent dans la même direction, l’angle est petit, le cosinus est proche de 1 → les sens sont proches. Si elles sont perpendiculaires, le cosinus est 0 → les sens n’ont aucun rapport. Si elles pointent à l’opposé, le cosinus est -1 → les sens s’opposent.

Analogie infra : c’est l’équivalent d’un score de match dans un moteur de recherche, sauf qu’on mesure une distance géométrique au lieu d’un overlap lexical.

Concept central

Pour aller plus loin (optionnel) — la formule mathématique. Pour deux vecteurs A et B : cos(θ) = (A · B) / (||A|| × ||B||) Au numérateur : le produit scalaire (somme des produits coordonnée par coordonnée). Au dénominateur : le produit des normes (longueurs des vecteurs). Le résultat est entre -1 et 1. Pas besoin de retenir ça pour utiliser un RAG — l’outil fait le calcul. Mais si tu lis du code sources, tu vas tomber dessus.

Code minimal :

# Calcule la similarité cosine entre 2 vecteurs (résultat entre -1 et 1, plus c'est proche de 1, plus les sens sont proches).
import numpy as np
def cosine(a, b):
    # np.dot = produit scalaire, np.linalg.norm = longueur du vecteur
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

Le retrieval rapporte typiquement 10 chunks candidats, déjà triés par score décroissant. Le composant 5 va décider quoi en faire.

Composant 5 — Augmentation + génération : l’envoi au LLM

Tu as les chunks pertinents. Étape finale : les coller dans le prompt avec une consigne claire au LLM, puis appeler le modèle.

Définition : l’augmentation, c’est l’opération qui prend les chunks retrouvés et les insère dans le prompt envoyé au LLM, accompagnés d’une instruction du genre « réponds en utilisant uniquement ces sources, cite chacune ».

Définition : un template de prompt, c’est une chaîne formatée avec des trous à remplir (la question, les sources). Analogie infra : un printf en C, ou un f-string Python — un format figé, des variables à injecter.

Code minimal — assemblage du prompt augmenté :

# Template figé. Les trous {sources} et {question} seront remplis à chaque requête.
PROMPT_TEMPLATE = """Tu réponds en utilisant uniquement les sources ci-dessous.

SOURCES:
{sources}

QUESTION: {question}

RÉPONSE (cite les sources entre crochets) :"""

def build_prompt(question: str, chunks: list[str]) -> str:
    # On joint les chunks avec un séparateur visible pour que le LLM voie les frontières
    sources = "\n---\n".join(chunks)
    return PROMPT_TEMPLATE.format(sources=sources, question=question)

Le prompt assemblé est envoyé à l’API du LLM (Claude, GPT, Llama). Le modèle lit la consigne, lit les sources, lit la question, et produit la génération — la réponse synthétisée, idéalement avec citations.

Récap visuel

Schéma : Documents, chunking, embedding, Vector store, Question, retrieval top_k, augmentation + template, génération (LLM), Réponse + sourcesDocumentschunkingembeddingVector storeQuestionembeddingretrievaltop_kaugmentation+ templategénération(LLM)Réponse+ sources
Le schéma en texte
  • Documents → chunking
  • chunking → embedding
  • embedding → Vector store
  • Question → embedding
  • embedding → retrieval top_k
  • Vector store → retrieval top_k
  • retrieval top_k → augmentation + template
  • augmentation + template → génération (LLM)
  • génération (LLM) → Réponse + sources

Cinq composants, deux pipelines. Indexation à gauche (chunking + embedding + store), requête à droite (embedding + retrieval + augmentation + génération). Le vector store est le pivot.

Quel composant tu changes quand le copilote te répond mal

C’est l’utilité opérationnelle de cette anatomie : quand le système échoue, savoir où regarder.

Symptôme observéComposant à challenger en premier
Réponse hors sujetRetrieval (mauvais top_k) ou Embedding (modèle pas adapté à la langue ou au domaine du corpus)
Réponse incomplète, manque le contexteChunking (chunks trop petits, le contexte argumentatif est coupé)
Réponse hallucinée malgré chunks pertinentsAugmentation (template trop permissif) ou Génération (LLM ignore les sources, prompt à durcir)
Trop lent en requêteVector store (index pas optimisé) ou top_k trop grand
Le retrieval rate des passages que tu sais présents (faible recall = % de fois où la bonne réponse est dans le top_k)Embedding — l’embedding seul rate les noms propres / identifiants. Combiner avec une recherche par mots-clés classique (BM25, traité en RAG-05, à venir) règle souvent.
Recall qui s’effondre après upgrade modèleEmbedding différent à l’indexation et à la requête → réindexer tout le corpus

Cette table est le minimum vital. La complète arrive avec la pratique.

Question

Ton copilote RAG répond à côté sur une question dont tu sais que la réponse est dans le corpus. Tu as cinq composants sous la main. Par lequel tu commences à chercher — et pourquoi celui-là plutôt qu’un autre ?

Réponse

Par le retrieval, mais en tant que mesure, pas en tant que suspect. Regarde d’abord ce que la recherche a effectivement ramené : si le bon passage est dans les chunks rapportés et que le LLM répond quand même à côté, le problème est en aval (augmentation, consigne, modèle). S’il n’y est pas, remonte : le passage est-il indexé du tout ? S’il l’est, c’est l’embedding ou le store. S’il ne l’est pas, ou s’il est coupé au mauvais endroit, c’est le chunking.

La règle générale : on ne devine pas quel composant a fauté, on regarde ce qui est sorti de chacun. Un pipeline de cinq composants sans observabilité intermédiaire ne se débogue pas, il se subit.

À toi de jouer

Prends la dernière question à laquelle ton copilote a mal répondu. Écris, en une phrase par composant, ce que tu attendais que chacun produise. Tu viens d’écrire la spécification du premier test de ton pipeline — et tu sauras la prochaine fois où regarder.

Glossaire

  • Pipeline RAG — Architecture à 5 composants qui attache une mémoire externe au LLM. Deux flux : indexation (offline) + requête (online).
  • Indexation (offline) — Phase préparatoire qui découpe les docs, les transforme en vecteurs, les stocke dans le vector store. Tournée une fois (ou périodiquement quand le corpus change).
  • Requête (online) — Phase exécutée à chaque question utilisateur : embed, search, augmente, génère.
  • Chunk — Morceau d’un document découpé pour être stocké et cherché individuellement (typiquement 200-800 tokens).
  • Chunking — L’opération de découpage des documents en chunks. Trois familles : fixed-size, recursive, semantic.
  • Embedding — Représentation d’un texte sous forme de vecteur de nombres. Deux textes proches en sens → deux vecteurs proches en géométrie. Analogie : un hash, mais sémantique.
  • Modèle d’embedding — Le réseau de neurones qui transforme texte en vecteur. Exemples open-source courants : BGE-M3, all-MiniLM-L6-v2 (détails RAG-03). Le même modèle DOIT être utilisé à l’indexation et à la requête.
  • Dimension (d’un vecteur) — Nombre de valeurs dans le vecteur (768, 1024, 1536, 3072 selon modèle). Plus c’est grand, plus c’est nuancé, plus c’est cher.
  • Vector store — Base de données spécialisée pour stocker et chercher des vecteurs par similarité. Analogie : Redis, mais indexé par similarité au lieu de clé.
  • Retrieval — Étape de recherche qui rapporte les chunks pertinents pour une question donnée.
  • top_k — Nombre de chunks ramenés par le retrieval (typique : 5 à 20).
  • Similarité cosinus — Mesure de proximité entre deux vecteurs, basée sur l’angle entre eux. Résultat entre -1 et 1, proche de 1 = sens proches.
  • Augmentation — Opération qui colle les chunks retrouvés dans le prompt avec une consigne au LLM.
  • Template de prompt — Chaîne formatée avec des trous ({sources}, {question}) à remplir à chaque requête. Analogie : un printf ou f-string.
  • Génération — Appel au LLM final qui produit la réponse à partir du prompt augmenté.
  • Recall (retrieval) — Pourcentage de fois où la bonne réponse est dans les top_k chunks ramenés par le retrieval. Indicateur clé de qualité retrieval. Analogie : taux de hit dans un cache.
  • Cutoff training (rappel RAG-00) — Date de coupure de l’entraînement du LLM, au-delà il ne sait rien.
  • Hallucination (rappel RAG-00) — Réponse inventée par le LLM, plausible mais sans fondement réel.

Vérifie-toi

5 questions, tirées de cette leçon et, pour la dernière, d’une leçon précédente.

  1. Question 1Quelle partie du pipeline tourne à chaque question posée par un utilisateur ?
    Voir le corrigé
    • Juste : L'embedding de la question, la recherche, l'augmentation du prompt et la génération.C'est le pipeline de requête. Il lit ce que l'indexation a déposé dans le vector store.
    • Non : Les cinq composants, dans l'ordre, à chaque question.Un RAG, ce sont deux pipelines distincts qui se rencontrent au vector store, pas une chaîne rejouée en entier.
    • Non : Le découpage des documents et le calcul de leurs embeddings.C'est l'indexation. Elle tourne hors ligne, une fois, et se rejoue quand le corpus change.
  2. Question 2Pourquoi utiliser un vector store plutôt qu'une simple liste de vecteurs parcourue à chaque requête ?
    Voir le corrigé
    • Juste : Parce que comparer la question à un million de vecteurs un par un prend des secondes, et qu'un store est indexé pour trouver vite les plus proches.La liste tient sur un petit corpus et s'effondre quand il grandit. Le store sert précisément la recherche par similarité.
    • Non : Parce qu'une liste ne sait pas contenir des nombres à virgule.Une liste le peut très bien. Le problème est le temps de recherche, pas le stockage.
    • Non : Parce que le vector store améliore la qualité des embeddings qu'il reçoit.Le store range et retrouve des vecteurs. Il ne les modifie pas, et ne rattrape pas un mauvais modèle d'embedding.
  3. Question 3Après une mise à jour du modèle d'embedding côté requête, le recall s'effondre du jour au lendemain. Que fais-tu ?
    Voir le corrigé
    • Juste : Tu réindexes tout le corpus avec le même modèle que celui de la requête.Le même modèle est obligatoire des deux côtés. Changer de modèle, c'est réindexer.
    • Non : Tu augmentes top_k pour ramener plus de candidats.Les vecteurs de la question et ceux du corpus vivent dans deux espaces incomparables. Ramener plus de candidats au hasard reste du hasard.
    • Non : Tu durcis la consigne du prompt.Le défaut est en amont : le modèle de génération ne reçoit plus les bons passages, aucune consigne ne les fera apparaître.
  4. Question 4Le bon passage figure bien dans les chunks ramenés, et le modèle répond pourtant à côté. Où cherches-tu ?
    Voir le corrigé
    • Non : Dans l'embedding : le modèle n'est pas adapté au corpus.La recherche a trouvé le passage. L'embedding n'est pas en cause pour cette réponse.
    • Non : Dans le chunking : le découpage a sûrement coupé le passage.Le passage est là, entier. Pour cette question, le découpage a fait son travail.
    • Juste : Dans l'augmentation et la génération : consigne trop permissive, ou modèle qui ignore les sources.Quand la matière est dans le prompt et que la réponse l'ignore, le défaut est en aval de la recherche. On le voit en regardant ce qui est sorti de chaque composant.
  5. Question 5 · rappel RAG-00Pourquoi un RAG permet-il de changer de modèle de génération sans reconstruire la base de connaissance ?
    Voir le corrigé
    • Non : Parce que le nouveau modèle réapprend le corpus au premier appel.Aucun modèle n'apprend en répondant. Il lit ce qu'on lui envoie, rien de plus.
    • Juste : Parce que la mémoire vit hors du modèle, dans un store qu'on met à jour et qu'on interroge séparément.Mémoire et génération sont découplées. Tu remplaces le composant qui rédige, le corpus indexé ne bouge pas.
    • Non : Parce que tous les modèles de génération produisent les mêmes vecteurs.Les vecteurs viennent du modèle d'embedding, pas du modèle de génération. Changer celui qui rédige ne touche pas à l'index.