Faire répondre un modèle sur tes documents, sans le réentraîner.
Ce cours suppose que tu sais ce qu'est un token, un embedding et une fenêtre de contexte. Si ces mots ne te disent rien, commence par comment fonctionne un LLM, puis reviens ici.
Imagine la situation. Tu as 300 PDF : des procédures internes, de la documentation technique, des comptes-rendus. Tu voudrais poser une question en français, du genre « quelle est la procédure si le capteur X sature ? », et obtenir une réponse exacte tirée de ces documents, avec la page d'où elle vient.
Tu ouvres ChatGPT. Premier problème : il ne connaît pas tes documents, il a appris sur du texte public jusqu'à une certaine date. Second problème, plus vicieux : il ne te dit pas qu'il ne sait pas. Un modèle de langage est entraîné à produire une suite de mots plausible, pas à vérifier. Face à une question dont il ignore la réponse, il en fabrique une qui ressemble à une bonne réponse. C'est ce qu'on appelle une hallucination.
Il existe trois réponses possibles. Les deux premières ne conviennent pas ici, et voir où elles échouent aide à comprendre la troisième.
C'est le fine-tuning. On prolonge l'entraînement du modèle avec tes données. Ça semble logique, et c'est presque toujours le mauvais outil ici :
Puisque le modèle lit ce qu'on lui écrit, pourquoi ne pas lui coller les 300 PDF avant la question ? Parce qu'il existe une fenêtre de contexte : la quantité de texte qu'un modèle peut lire d'un coup. Elle est limitée, et elle est facturée. 300 PDF n'y rentrent pas. Et même si c'était le cas, tu paierais l'intégralité de ta documentation à chaque question, pour une précision qui baisse à mesure qu'on noie l'information utile dans le bruit.
L'idée est simple une fois posée : on ne donne pas tout, on donne juste ce qu'il faut. Avant de poser la question au modèle, on va chercher automatiquement les 3 à 5 passages pertinents dans les 300 PDF, et on les colle dans le prompt avec la question. Le modèle n'a plus qu'à rédiger à partir de ce qu'il a sous les yeux.
C'est un examen à livre ouvert.
Le fine-tuning, c'est apprendre le livre par cœur (long, imprécis, à refaire à chaque édition).
Tout coller dans le prompt, c'est relire la bibliothèque entière à chaque question.
Le RAG, c'est ouvrir le livre à la bonne page, puis répondre avec.
D'où le nom, RAG, pour Retrieval-Augmented Generation :
Le modèle ne fouille pas ta base de documents. Il n'y a aucun accès. C'est ton code qui cherche, trouve les passages, et les écrit dans le prompt. Le modèle, lui, ne voit qu'un texte un peu plus long que d'habitude. Tout le travail intéressant du RAG est avant l'appel au modèle.
Ce que ça change en pratique : la réponse devient vérifiable, puisqu'on sait de quel document et de quelle page elle vient. Mettre à jour la connaissance prend quelques secondes, le temps de réindexer un fichier. Et le modèle reste générique, donc remplaçable par un autre.
Le RAG en une image : deux temps bien distincts
Reste la partie difficile : comment une machine fait-elle pour trouver les bons passages ? Chercher les mots de la question ne suffit pas. Si le document dit « le capteur est en saturation » et que tu demandes « que faire quand X déborde ? », aucun mot ne correspond. Il faut chercher par le sens. C'est le rôle des embeddings, expliqués dans le cours sur le fonctionnement d'un LLM.
Tu as maintenant les deux briques. Assemblons la machine. Elle fonctionne en deux temps qu'il ne faut jamais confondre : une préparation faite une seule fois, et une boucle rejouée à chaque question.
Le découpage et son chevauchement
On cherche sur les vecteurs, on répond avec le texte. Le vecteur sert à trouver, jamais à rédiger. C'est pour ça qu'on stocke les deux.
Pour comparer des textes entiers, on ne veut pas un vecteur par mot : on veut un seul vecteur pour un paragraphe entier. Le modèle d'embedding lit tout le passage, contextualise chaque mot, puis fait la moyenne. Il en sort un seul point qui résume l'ensemble.
La taille de ce vecteur est fixée par le modèle (384, 768, 1024, 3072 nombres), jamais par la longueur du texte. Une phrase de 5 mots et un paragraphe de 300 mots donnent tous deux un vecteur de la même taille.
Il existe deux familles de modèles bien distinctes : les modèles d'embedding, petits et rapides, qui transforment du texte en vecteurs pour le comparer, et les LLM, bien plus gros, qui rédigent. Même socle technique, deux métiers, et on ne les confond jamais.
Un RAG manipule deux tokenizers différents. C'est le piège qui surprend tout le monde.
Deux voies, deux tokenizers
① Chunker par tokens, pas par caractères → respecter la fenêtre du modèle d'embedding.
② Compter les tokens (tiktoken) → ne pas dépasser le contexte du LLM.
③ Se souvenir de la taxe : un corpus français remplit les chunks plus vite, donc on en case moins.
C'est l'embedding contextuel qui compte, lui seul sait distinguer les sens. Il est calculé une fois par chunk à l'indexation, puis rangé dans ta base.
Par l'angle entre deux vecteurs, ce qu'on appelle la similarité cosinus. L'intuition : dans cet espace, c'est la direction qui porte le sens, pas la longueur de la flèche. Deux textes qui pointent dans la même direction parlent de la même chose.
Petit angle = même sujet
Tu n'implémentes rien de tout ça. La base vectorielle fait le calcul et l'optimisation : tu choisis la métrique (cosinus) et le k, tu appelles search(vecteur, k), tu reçois les passages triés.
→ Le détail complet (formule du cosinus, exemple chiffré, force brute vs HNSW, late chunking) est dans la fiche de cette page, onglet « Fiches ».
Un RAG qui répond mal, c'est presque toujours l'un de ces six points.
| taille de chunk | 200 à 500 tokens. Trop petit : le passage perd son contexte. Trop grand : le vecteur devient vague et retrouve mal. |
| chevauchement | 10 à 15 % de la taille du chunk. Assez pour ne pas couper une idée, pas assez pour dupliquer toute la base. |
| k | 3 à 10 passages. Trop peu : il manque l'info. Trop : on noie le modèle et on paie des tokens pour rien. |
| seuil de score | Ignorer les chunks sous ~0,8 de cosinus, plutôt que d'envoyer du hors-sujet au LLM « parce qu'il fallait bien k résultats ». |
| même modèle | Le même modèle d'embedding pour les documents et pour les questions, sans exception. |
| compter en tokens | Découper par tokens, pas par caractères, et se souvenir qu'un texte français en consomme environ 1,5× plus. |
Tu stockes tes chunks en vecteurs. Pour une question : tu la transformes en vecteur, tu récupères les vecteurs les plus proches.
① Deux modèles, pas un : le modèle d'embedding (chercher) ≠ le LLM (répondre).
② Fenêtre du modèle d'embedding souvent 512 tokens → d'où le chunking par tokens.
③ Taxe linguistique : un chunk français remplit la fenêtre plus vite → moins de mots par vecteur.
Les vecteurs vivent dans une base vectorielle. Trouver les plus proches parmi des millions se fait par recherche approximative (ANN).
Le RAG n'est pas magique. Savoir où il casse évite de perdre des semaines.
Le RAG réduit les hallucinations, il ne les supprime pas. Si les passages fournis sont hors sujet, le modèle rédigera quand même quelque chose. D'où l'importance d'afficher les sources : c'est ce qui rend l'erreur visible.
Une fois le RAG de base en place, voici l'ordre dans lequel il vaut le coup de l'améliorer.
Tu as maintenant le modèle mental complet : découper → vectoriser → ranger → retrouver par proximité → coller dans le prompt → rédiger → citer.
Bascule sur l'onglet Fiches pour la version condensée, avec les détails techniques, les formules et les schémas de référence.
De tes documents bruts à une réponse sourcée. Étape par étape.
Vue d'ensemble : deux phases
Le LLM ne fouille pas ta base. C'est toi qui cherches, puis tu lui passes le texte trouvé dans le prompt. Il n'a aucun accès à ta base.
PDF, Word (.docx), pages web, code source… la connaissance que tu veux rendre interrogeable. À ce stade, rien n'est fait : ce sont des fichiers sur un disque.
Un modèle d'embedding ne lit que du texte. On jette la mise en forme (polices, colonnes, images), on garde le contenu.
① Un document entier dépasse la fenêtre du modèle d'embedding (souvent 512 tokens).
② Un seul vecteur pour 50 pages serait trop vague pour retrouver une info précise.
Taille typique : 200 à 500 tokens. On découpe par tokens, pas par caractères.
Chunks + chevauchement (overlap)
Le vecteur du chunk, c'est son adresse dans l'espace du sens. Deux chunks au sens proche = vecteurs proches, même s'ils n'emploient pas les mêmes mots.
Le mécanisme interne
L'attention ne relie que les tokens du chunk lui-même. Chaque chunk est encodé isolément : le vecteur du chunk 2 ne sait rien du chunk 1 ni du 3.
L'attention ne traverse pas les frontières
Late chunking : on inverse l'ordre, on embed tout le document d'abord (chaque token voit ses voisins), et on ne découpe les vecteurs qu'ensuite, juste avant le pooling. Chaque vecteur de chunk a donc « vu » ses voisins. Coût : un modèle à long contexte et plus de calcul.
Contextual retrieval : coller un court résumé du document en tête de chaque chunk avant de l'encoder.
Sentence-window : encoder de petits chunks, mais renvoyer au LLM le chunk plus ses voisins au moment de répondre.
Le chunking isolé reste le défaut : plus simple, et un vecteur trop chargé de contexte retrouve moins nettement.
① le vecteur (la clé de recherche) · ② le texte original (c'est lui qu'on enverra au LLM) · ③ les métadonnées (source, page → pour citer).
Ce que contient la base
| ID | Vecteur (clé) | Texte du chunk | Source |
|---|---|---|---|
| 1 | [0.07, 0.5, …] | « La cuisson basse… » | guide.pdf · p.3 |
| 2 | [−0.2, 0.1, …] | « Préchauffer le four… » | guide.pdf · p.4 |
| 3 | [0.4, −0.3, …] | « def cuire(temp): … » | script.py |
Le vecteur sert à trouver. Le texte sert à répondre. Toute cette phase (§1→§5) est faite une seule fois.
La question est encodée par exactement le même modèle d'embedding que les chunks. Sinon les vecteurs vivent dans des espaces différents et ne sont pas comparables.
Le tableau donne les coordonnées d'un seul point. Chaque case = un axe. 768 nombres = 1 point dans un espace à 768 axes, et non 768 points.
Le tableau = les coordonnées d'un point
Le sens est porté par la direction. Donc pour comparer deux vecteurs, on mesure l'angle entre eux.
Petit angle = sens proche
cos(θ) = (A·B) / (‖A‖ × ‖B‖)
Le produit scalaire divisé par les longueurs, ce qui annule l'effet de taille : seule la direction compte.
Si les vecteurs sont normalisés (longueur 1) : cosinus = simple produit scalaire. D'où sa rapidité.
1. Produit scalaire : A·B = a₁b₁ + a₂b₂ + … + aₙbₙ (multiplier terme à terme, additionner).
2. Longueur (Pythagore) : ‖A‖ = √(a₁² + a₂² + … + aₙ²).
3. Diviser. (Si on veut l'angle : θ = arccos(cos θ), mais pour la recherche on s'arrête au cosinus, qui est le score.)
Exemple. A = [2, 1], B = [1, 3]
A·B = 2×1 + 1×3 = 5
‖A‖ = √5 ≈ 2,24 · ‖B‖ = √10 ≈ 3,16
cos θ = 5 / (2,24 × 3,16) ≈ 0,71 → environ 45°.
0,71 c'est « assez proche ». En pratique on retient surtout les scores > ~0,8.
Le cosinus d'un angle = la coordonnée horizontale du point sur le cercle. Plus l'angle est petit, plus on est à droite, plus le cosinus est proche de 1.
Pour le RAG : on vise cos ≈ 1 → angle ≈ 0 → même sens.
Force brute = calculer le cosinus avec chaque vecteur, trier, garder le top-k. Exact, mais N calculs par question.
ANN (approximatif) = un index ne compare qu'une petite fraction. Chance infime de rater le tout-à-fait-plus-proche.
Force brute vs HNSW
C'est la base vectorielle qui fait tout. Tu choisis la métrique (cosinus) et le k, tu appelles search(vecteur, k), tu reçois les k chunks triés. Tu n'implémentes ni le cosinus ni HNSW.
Un prompt en 3 parties : consigne + contexte (le texte des k chunks) + question.
Assemblage du prompt
La réponse est fondée sur des passages précis et identifiés → moins d'hallucinations. Le modèle a la matière sous les yeux au lieu de répondre de mémoire.
Grâce aux métadonnées gardées au stockage (§5), on affiche les sources : quels documents, quelles pages ont servi. La réponse devient vérifiable.
Seules les étapes §6 → §10 sont rejouées à chaque question.
L'indexation (§1 → §5) ne se refait que si tes documents changent.