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.
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)
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.
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)]
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.
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-M3etall-MiniLM-L6-v2sont 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 key123on faitSEARCH "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.
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.
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
printfen 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
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 sujet | Retrieval (mauvais top_k) ou Embedding (modèle pas adapté à la langue ou au domaine du corpus) |
| Réponse incomplète, manque le contexte | Chunking (chunks trop petits, le contexte argumentatif est coupé) |
| Réponse hallucinée malgré chunks pertinents | Augmentation (template trop permissif) ou Génération (LLM ignore les sources, prompt à durcir) |
| Trop lent en requête | Vector 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èle | Embedding 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.
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 : unprintfou 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.