Le probleme : les LLMs ne savent pas tout
Au Module 1, on a vu que le LLM a une connaissance fige — il ne connait que ce qu'il a vu pendant son entrainement. Il ne connait pas les tarifs Selectra d'aujourd'hui, ni les documents internes de ta boite, ni le contenu d'un PDF que tu viens de recevoir.
RAG = Retrieval-Augmented Generation. En francais : generation augmentee par la recuperation. Le principe est simple :
Pipeline RAG
Question de l'utilisateur
|
[ 1. Retrieval ]
Chercher les documents pertinents
dans une base de connaissances
|
[ 2. Augmentation ]
Injecter ces documents dans le
prompt du LLM comme contexte
|
[ 3. Generation ]
Le LLM repond en se basant sur
les documents fournis
|
Reponse sourcee et fiable
| Approche | Principe | Quand l'utiliser |
|---|---|---|
| Sans RAG | Le LLM repond de memoire | Questions de culture generale, code, raisonnement |
| RAG | On donne des documents au LLM avant qu'il reponde | Donnees privees, info a jour, besoin de sources |
| Fine-tuning | On re-entraine le modele sur tes donnees | Changer le style/comportement du modele, pas pour des faits |
| Long context | On met tout dans le prompt (si ca rentre) | Petit corpus (<100 pages), pas besoin de systeme complexe |
Les embeddings — la base du RAG
Au Module 1, on a brievement vu les embeddings. Ici on va plus loin car c'est le moteur du RAG.
embed("Comment reduire ma facture d'electricite ?")
→ [0.23, -0.41, 0.87, 0.12, ...] (1536 dimensions)
embed("Je paye trop cher mon courant, que faire ?")
→ [0.21, -0.39, 0.85, 0.14, ...] (tres proche ! meme sens)
embed("Quelle est la capitale de la France ?")
→ [0.78, 0.33, -0.21, 0.67, ...] (tres loin — sujet different)
distance("facture", "courant") = 0.05 → tres similaire
distance("facture", "capitale") = 1.42 → tres differentLa entre deux embeddings mesure la similarite semantique. C'est ca qui permet de trouver "les documents qui parlent du meme sujet que la question".
import { embed, embedMany } from "ai";
// Embedder un seul texte
const { embedding } = await embed({
model: "openai/text-embedding-3-small",
value: "Comment changer de fournisseur d'electricite ?",
});
// embedding = number[] (1536 dimensions)
// Embedder plusieurs textes d'un coup (plus efficace)
const { embeddings } = await embedMany({
model: "openai/text-embedding-3-small",
values: [
"Guide pour changer de fournisseur",
"Tarifs electricite 2026",
"Comment lire son compteur Linky",
],
});
// embeddings = number[][] (3 vecteurs)| Modele d'embedding | Dimensions | Usage |
|---|---|---|
| text-embedding-3-small (OpenAI) | 1536 | Bon rapport qualite/prix, usage general |
| text-embedding-3-large (OpenAI) | 3072 | Plus precis, plus cher |
| voyage-3 (Voyage AI) | 1024 | Excellent pour le francais |
| Mistral Embed | 1024 | Francais natif, bon marche |
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# Simulons des embeddings (en vrai, ils viennent d'un modele)
# Ici on utilise sentence-transformers (pip install sentence-transformers)
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2") # leger, 384 dims
phrases = [
"Comment reduire ma facture d'electricite ?",
"Je paye trop cher mon courant, que faire ?",
"Quelle est la capitale de la France ?",
]
embeddings = model.encode(phrases) # → np.array shape (3, 384)
# Matrice de similarite
sim_matrix = cosine_similarity(embeddings)
print(f"facture ↔ courant : {sim_matrix[0][1]:.3f}") # → 0.82 (tres proche !)
print(f"facture ↔ capitale : {sim_matrix[0][2]:.3f}") # → 0.13 (rien a voir)
# Trouver le document le plus proche d'une question
question = model.encode(["Je veux payer moins cher"])
scores = cosine_similarity(question, embeddings)[0]
best = np.argmax(scores)
print(f"Meilleur match : '{phrases[best]}' (score: {scores[best]:.3f})")Vector stores — la memoire du RAG
OK, on sait convertir du texte en vecteurs. Mais ou les stocker, et comment chercher dedans rapidement ?
Indexation (une seule fois)
Tu prends tes documents, tu les decoupes en chunks, tu calcules l'embedding de chaque chunk, et tu les stockes dans le vector store avec les metadonnees (source, page, date...).
Recherche (a chaque requete)
La question de l'utilisateur est embedee, puis le vector store trouve les K chunks les plus proches (Approximate Nearest Neighbors — ANN).
Generation
Les chunks recuperes sont injectes dans le prompt du LLM, qui genere sa reponse en se basant dessus.
Architecture RAG complete
INDEXATION (offline, une fois)
─────────────────────────────
Documents PDF/HTML/TXT
|
[ Chunking ] → morceaux de ~500 tokens
|
[ Embedding model ] → vecteurs
|
[ Vector Store ] → stockage indexe
REQUETE (a chaque question)
──────────────────────────
"Comment changer de fournisseur ?"
|
[ Embedding model ] → vecteur de la question
|
[ Vector Store: top-K search ]
|
3-5 chunks pertinents retrouves
|
[ Prompt = system + chunks + question ]
|
[ LLM ]
|
"Pour changer de fournisseur, voici les etapes..."
(avec citations des sources)
| Vector store | Type | Avantage | Usage |
|---|---|---|---|
| Pinecone | Cloud (managed) | Simple, scalable, pas de maintenance | Production, SaaS |
| Chroma | Local / embedde | Gratuit, tourne en local, parfait pour prototyper | Dev, petits projets |
| Weaviate | Cloud ou self-hosted | Hybrid search (vecteurs + mots-cles) | Production avancee |
| pgvector | Extension PostgreSQL | Pas de nouvelle DB a gerer si tu as deja Postgres | Si tu utilises deja Neon/Supabase |
| En memoire (array) | Juste du JS | Zero dependance, quelques lignes de code | Prototypage rapide, <1000 docs |
Le chunking — decouper intelligemment
Tu ne peux pas embedder un PDF de 200 pages d'un coup — le modele d'embedding a une limite de tokens, et un vecteur de 200 pages serait trop generique pour etre utile. Il faut decouper.
| Strategie | Principe | Taille typique | Quand l'utiliser |
|---|---|---|---|
| Fixed-size | Couper tous les N tokens | 200-500 tokens | Simple, rapide, bon par defaut |
| Par paragraphe | Couper aux sauts de ligne doubles | Variable | Documents bien structures (articles, docs) |
| Recursive | Couper en hierarchie (section → paragraphe → phrase) | 200-1000 tokens | Le plus polyvalent, recommande |
| Semantique | Couper quand le sujet change (via embeddings) | Variable | Documents longs sans structure claire |
Document original (2000 tokens):
"## Changer de fournisseur
Pour changer de fournisseur d'electricite, il suffit de souscrire
une nouvelle offre. L'ancien fournisseur sera resilie automatiquement...
[500 tokens]
## Les tarifs reglementes
Le tarif reglemente de vente (TRV) est fixe par les pouvoirs publics...
[600 tokens]
## Le compteur Linky
Le compteur Linky permet de relever votre consommation a distance...
[900 tokens]"
Apres chunking recursif (max 500 tokens, overlap 50):
→ Chunk 1: "## Changer de fournisseur
Pour changer..." (480 tokens)
→ Chunk 2: "## Les tarifs reglementes
Le tarif..." (520 tokens → re-decoupe)
→ Chunk 2a: "## Les tarifs reglementes
Le tarif..." (300 tokens)
→ Chunk 2b: "...fixe par les pouvoirs publics..." (270 tokens)
→ Chunk 3: "## Le compteur Linky
Le compteur..." (450 tokens)
→ Chunk 4: "...relever votre consommation a distance..." (500 tokens)from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # max 500 caracteres par chunk
chunk_overlap=50, # 50 caracteres de chevauchement
separators=["\n## ", "\n\n", "\n", ". ", " "], # priorite de coupure
)
texte = """## Changer de fournisseur
Pour changer de fournisseur d'electricite, il suffit de souscrire
une nouvelle offre. La resiliation est automatique et gratuite.
## Les tarifs reglementes
Le tarif reglemente de vente (TRV) est fixe par les pouvoirs publics.
Il change deux fois par an, en fevrier et en aout."""
chunks = splitter.split_text(texte)
for i, chunk in enumerate(chunks):
print(f"Chunk {i} ({len(chunk)} chars): {chunk[:60]}...")Similarity search — trouver les bons documents
Quand l'utilisateur pose une question, comment trouver les chunks les plus pertinents parmi des milliers ?
// Cosine similarity entre deux vecteurs
function cosineSimilarity(a: number[], b: number[]): number {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
// Chercher les K chunks les plus proches
function search(query: number[], chunks: { text: string; embedding: number[] }[], k = 3) {
return chunks
.map(chunk => ({
text: chunk.text,
score: cosineSimilarity(query, chunk.embedding),
}))
.sort((a, b) => b.score - a.score)
.slice(0, k);
}
// Utilisation
const queryEmbedding = await embed({ model, value: "Comment changer de fournisseur ?" });
const results = search(queryEmbedding.embedding, indexedChunks, 3);
// → [
// { text: "Pour changer de fournisseur...", score: 0.89 },
// { text: "La procedure de changement...", score: 0.82 },
// { text: "Les delais de changement...", score: 0.76 },
// ]C'est simple et ca marche. Pour des milliers de documents, les vector stores utilisent des algorithmes plus rapides (HNSW, IVF) mais le principe est le meme.
| Recherche | Force | Faiblesse | Exemple |
|---|---|---|---|
| Vectorielle | Comprend le sens | Rate les mots exacts (codes, noms) | "facture elevee" trouve "je paye trop cher" |
| BM25 (mots-cles) | Trouve les mots exacts | Ne comprend pas les synonymes | "PDL 09234567" trouve le bon compteur |
| Hybrid | Les deux | Plus complexe a mettre en place | Le meilleur choix en production |
Pipeline RAG complet en code
Assemblons tout en un pipeline fonctionnel. Voici un RAG complet sur une FAQ Selectra.
import { embed, embedMany, generateText } from "ai";
// ── 1. Donnees (en prod, ca viendrait d'une DB ou de fichiers) ──
const faq = [
{
question: "Comment changer de fournisseur d'electricite ?",
answer: "Il suffit de souscrire une offre chez un nouveau fournisseur. La resiliation de l'ancien contrat est automatique et gratuite. Le changement prend environ 3 semaines. Vous n'aurez aucune coupure d'electricite pendant la transition.",
},
{
question: "Qu'est-ce que le numero PDL ?",
answer: "Le PDL (Point de Livraison) est un numero a 14 chiffres qui identifie votre compteur electrique. Vous le trouvez sur votre facture d'electricite, sur votre compteur Linky, ou en appelant Enedis au 09 72 67 50 XX (XX = votre departement).",
},
{
question: "Quels documents faut-il pour souscrire ?",
answer: "Pour souscrire un contrat d'electricite, vous avez besoin de : votre adresse exacte, votre numero PDL (ou PCE pour le gaz), un RIB pour le prelevement, et une estimation de votre consommation annuelle en kWh.",
},
// ... 200 autres Q/R
];
// ── 2. Indexation (une seule fois au demarrage) ──
const texts = faq.map(f => f.question + " " + f.answer);
const { embeddings } = await embedMany({
model: "openai/text-embedding-3-small",
values: texts,
});
const index = faq.map((f, i) => ({
text: f.question + "\n" + f.answer,
embedding: embeddings[i],
}));
// ── 3. Recherche (a chaque question utilisateur) ──
const userQuestion = "J'ai besoin de quoi pour ouvrir un contrat ?";
const { embedding: queryVec } = await embed({
model: "openai/text-embedding-3-small",
value: userQuestion,
});
// Trouver les 3 Q/R les plus pertinentes
const results = index
.map(item => ({ ...item, score: cosineSimilarity(queryVec, item.embedding) }))
.sort((a, b) => b.score - a.score)
.slice(0, 3);
// ── 4. Generation avec contexte ──
const response = await generateText({
model: "anthropic/claude-sonnet-4.6",
system: "Tu es l'assistant Selectra. Reponds en te basant UNIQUEMENT sur les documents fournis. Si l'info n'est pas dans les documents, dis-le.",
prompt: `Documents pertinents :
---
${results.map(r => r.text).join("\n---\n")}
---
Question du client : ${userQuestion}
Reponds en citant les sources.`
});Application Selectra : RAG pour l'agent vocal
Comment le RAG s'integre dans l'agent vocal Selectra ?
Agent vocal + RAG
Client : "C'est quoi les tarifs chez TotalEnergies ?"
|
[ Agent vocal (LLM) ]
|
Besoin d'info factuelle → RAG
|
[ Vector search: "tarifs TotalEnergies" ]
|
Documents retrouves :
- "TotalEnergies Essentielle : 0.19€/kWh, abo 12€/mois"
- "TotalEnergies Verte : 0.21€/kWh, abo 13€/mois"
|
[ LLM + contexte RAG ]
|
Agent : "TotalEnergies propose deux offres principales :
l'offre Essentielle a 0.19€ le kilowatt-heure
et l'offre Verte a 0.21€. Voulez-vous que je
compare avec d'autres fournisseurs ?"
En pratique, l'agent vocal Selectra utiliserait le RAG pour :
| Cas d'usage RAG | Base de connaissances | Pourquoi RAG et pas de memoire |
|---|---|---|
| Tarifs des offres | Base Selectra mise a jour quotidiennement | Les prix changent tous les mois — le LLM ne peut pas les connaitre |
| FAQ client | 200+ questions/reponses Selectra | Reponses precises et validees juridiquement |
| Procedures internes | Scripts de qualification, regles metier | L'agent doit suivre la procedure exacte, pas improviser |
| Info compteur | Guide Linky, numeros Enedis par departement | Infos techniques precises (numeros de tel, etapes) |
Quand NE PAS utiliser le RAG
| Situation | RAG ? | Alternative |
|---|---|---|
| Petit corpus (<50 pages) | Non | Met tout dans le context window directement. Plus simple, plus fiable. |
| Le LLM connait deja la reponse | Non | Pour du code Python, de la culture generale, du raisonnement pur → pas besoin de RAG. |
| Tu veux changer le style du modele | Non | Fine-tuning ou system prompt elabore. Le RAG donne des faits, pas un style. |
| Donnees privees + mise a jour frequente | Oui | C'est LE cas d'usage ideal du RAG. |
Quiz final
Module 03 termine
Tu sais maintenant comment fonctionne le RAG de bout en bout : embeddings, chunking, vector stores, similarity search, et comment l'integrer dans un agent vocal. Prochain module : Tool Calling & Agents — quand le LLM agit dans le monde reel.