Un client fintech nous appelle un mardi matin. Leur agent de support, branché sur leur base de connaissances avec un RAG « state-of-the-art », vient de coûter 4 200 € de transactions perdues. Un développeur junior a appliqué une suggestion de l'agent : un snippet d'intégration Stripe, sûr de lui, parfaitement formaté. Sauf qu'ils utilisent Adyen depuis trois ans. Le LLM n'a jamais vu leur code — il a juste halluciné ce qu'il avait vu le plus souvent sur GitHub. Le RAG était mal configuré : retrieval naïf, pas de reranking, embeddings génériques. Voici les trois erreurs qu'on retrouve dans 80% des audits RAG, et l'architecture qui marche.
Pourquoi un RAG « qui marche en démo » se casse en production
En démo, le RAG semble magique. Vous chargez 50 PDF, vous posez trois questions, vous obtenez trois réponses correctes, le client signe. Six semaines plus tard, le support vous remonte que l'agent répond à côté une fois sur quatre. Pourquoi ?
Parce qu'un RAG enchaîne quatre maillons fragiles : chunking, embedding, retrieval, augmentation. Si un seul lâche, l'agent hallucine ou rate la bonne source. Et un retrieval correct sur 100 documents ne garantit rien sur 100 000.
Les chiffres que nous mesurons en audit chez nos clients :
| Métrique | Démo (50 docs) | Production (50 000 docs) |
|---|---|---|
| Précision @5 | 80–90 % | 45–65 % |
| Hallucinations | < 5 % | 18–32 % |
| Latence p95 | 800 ms | 3 200 ms |
| Coût mensuel embeddings | 2 € | 480 € |
Le projet ne « rate » pas brutalement. Il dérive lentement, jusqu'au jour où un utilisateur applique une suggestion fausse et que ça coûte de l'argent réel.
La stack minimale qui passe en production
Voici le pipeline que nous déployons par défaut chez nos clients. Pas de framework lourd, pas de LangChain — quatre composants, 200 lignes de TypeScript ou Python.
// 1. Indexation (offline, à chaque déploiement)
const chunks = chunkByAST(file); // Découpe par fonctions/classes
const embeddings = await localEmbedder.embedBatch(chunks); // MiniLM local
await vectorDb.upsert(chunks, embeddings); // Qdrant ou pgvector
// 2. Runtime (à chaque requête)
const queryEmb = await localEmbedder.embed(question);
const semantic = await vectorDb.search(queryEmb, 20); // Sémantique
const keyword = await vectorDb.bm25(question, 20); // BM25
const fused = reciprocalRankFusion([semantic, keyword]); // RRF
const reranked = await crossEncoderRerank(question, fused).slice(0, 5);
// 3. Augmentation
const answer = await llm.chat(buildPrompt(question, reranked));Quatre composants, dans cet ordre — et chaque mot compte.
| Composant | Choix par défaut | Coût mensuel | Quand changer |
|---|---|---|---|
| Chunking | AST (tree-sitter) | 0 € | Jamais sauf langage exotique |
| Embeddings | Xenova/all-MiniLM-L6-v2 local | 0 € | Multilingue → BGE-M3 |
| Vector DB | pgvector si déjà Postgres, Qdrant sinon | 20–80 € | > 1 M chunks → Qdrant cluster |
| Reranker | bge-reranker-v2-m3 | 0 € (CPU) | Latence < 200 ms → API Cohere |
Choisir sa vector DB en 2026 : la décision en 3 questions
La question « quelle vector DB ? » occupe trop de réunions pour ce qu'elle apporte. Trois questions règlent 95 % des cas :
- Avez-vous déjà Postgres en production ? Oui → pgvector. Pas de débat. L'extension est mature depuis 2023, l'index HNSW est natif depuis 0.5, vous évitez une dépendance.
- Dépassez-vous 5 millions de chunks ? Oui → Qdrant ou Weaviate cluster. Postgres + pgvector tient jusqu'à 2–3 M chunks confortablement, au-delà la latence p95 se dégrade.
- Avez-vous des contraintes de souveraineté (santé, défense, banque) ? Oui → Qdrant self-hosted ou Milvus. Évitez Pinecone et les SaaS US par défaut.
Tout le reste — Pinecone, Vespa, Chroma, Marqo — est de l'optimisation prématurée pour un projet B2B classique.
Pourquoi 80% des projets échouent : les trois erreurs qu'on retrouve à chaque audit
Erreur 1 — Chunking par caractères ou par lignes fixes
Le piège classique. Vous découpez vos documents tous les 500 caractères ou toutes les 100 lignes. C'est rapide, c'est trivial, c'est faux.
Une fonction de 80 lignes coupée en deux donne deux chunks dont aucun n'est utilisable seul. Le LLM voit la moitié haute (signature, début de logique) ou la moitié basse (return, gestion d'erreur) — jamais les deux. Il extrapole le reste, donc il hallucine.
Le bon découpage respecte les frontières sémantiques : fonctions, classes, sections de doc. Notre benchmark interne sur 12 codebases clients :
| Méthode de chunking | Précision @5 | Effort d'implémentation |
|---|---|---|
| Caractères fixes (500) | 60 % | Trivial |
| Lignes fixes (100) | 65 % | Trivial |
Split sur regex function | 75 % | Faible |
| AST (tree-sitter) | 85 % | Moyen |
| AST + imports propagés | 92 % | Élevé |
Le saut de 65 % à 85 % de précision pour deux jours de travail est l'investissement RAG le plus rentable que vous puissiez faire.
L'outil qu'on utilise par défaut : tree-sitter. Il couvre 40+ langages, génère un AST en quelques millisecondes, et permet d'extraire les frontières sémantiques sans parser maison. Pour du Markdown ou de la documentation, on coupe sur les H2/H3 — un H3 == un chunk, avec le H2 parent injecté en préfixe pour préserver le contexte.
Erreur 2 — Recherche sémantique seule
C'est l'erreur la plus contre-intuitive. La recherche par embeddings est censée « comprendre le sens » — donc on l'utilise seule, et on rate systématiquement les noms exacts.
Demandez à un RAG sémantique de trouver la classe PaymentProcessor. Il va vous remonter TransactionHandler, BillingService, OrderManager — tous sémantiquement proches. Mais PaymentProcessor ? Probablement pas dans le top-5, parce que son embedding ressemble à 200 autres classes dans votre code.
La correction tient en trois lignes : ajoutez BM25 en parallèle, fusionnez avec Reciprocal Rank Fusion. C'est dans toutes les vector DB modernes (pgvector 0.7+, Qdrant, Weaviate). L'impact mesuré chez un client SaaS :
| Stratégie | Precision@5 | Recall@5 |
|---|---|---|
| Sémantique seule | 72 % | 65 % |
| BM25 seul | 68 % | 58 % |
| Hybride + RRF | 85 % | 78 % |
Erreur 3 — Pas de reranking, top-20 envoyé brut au LLM
L'argument économique a la peau dure : « le reranker coûte 50 ms supplémentaires, donc on le saute ». Sauf que sans reranker, vous envoyez 20 chunks au LLM dont 12 ne sont pas pertinents. Ces 12 chunks parasites :
- Diluent l'attention du modèle sur les bonnes sources.
- Coûtent en tokens : 20 chunks × 500 tokens = 10 000 tokens d'input à chaque requête.
- Augmentent le risque d'hallucination : le LLM mélange des fragments hors-sujet avec les bons.
Un cross-encoder local (bge-reranker-v2-m3) tourne en 80 ms sur CPU pour 20 candidats. L'économie de tokens compense largement la latence : nous mesurons -40 % de tokens consommés et -25 % d'hallucinations sur les déploiements clients.
L'erreur bonus que personne n'évoque : pas de citation des sources
Même avec un pipeline parfait, un agent qui ne cite pas ses sources reste un agent qui hallucine de manière indétectable. Le pattern qu'on impose à chaque déploiement : forcer le LLM à inclure dans sa réponse les identifiants des chunks qu'il a utilisés, avec un format strict ([doc-42:fonction-submitTransaction]). En sortie, on parse ces tags et on les transforme en liens cliquables.
Bénéfice double : (1) l'utilisateur peut vérifier la source en un clic, (2) on détecte les hallucinations en regardant les réponses sans tags ou avec des tags inventés. Sur le projet fintech évoqué en intro, ce simple ajout a baissé de 40 % les remontées « l'agent dit n'importe quoi » — non pas parce que l'agent s'améliorait, mais parce que les utilisateurs validaient eux-mêmes en cliquant sur la source.
Edge cases : où le pattern « marche » casse
Cas 1 : codebases legacy sans structure exploitable
Un client avait 1,2 million de lignes de COBOL et de PL/SQL générées par des outils des années 90. Pas d'AST tree-sitter pour ça. Solution : chunking par procédure stockée pour PL/SQL (split sur CREATE OR REPLACE), chunking par paragraphe nommé pour COBOL. Précision tombée à 78 % — acceptable pour le cas d'usage (refactoring assisté), pas pour du Q&A client.
Cas 2 : documents très longs (> 10k tokens) à indexer entiers
Un éditeur juridique nous a confié 40 000 contrats de 30 à 200 pages. Découper par paragraphe perd le contexte (« comme stipulé à l'article 3.2 »). Solution : indexation hiérarchique — un embedding par section + un embedding par document complet. Le retrieval cherche d'abord le bon contrat, puis la bonne section. +15 points de précision vs flat indexing.
Cas 3 : multilingue avec langues rares
Embeddings MiniLM couvre l'anglais. Pour du français, c'est OK. Pour de l'arabe, du japonais ou du polonais en mélange, il faut BGE-M3 ou multilingual-e5-large. Coût : 2× plus de RAM (1,5 Go vs 90 Mo), mais c'est la seule option viable. Tester avant : générer 200 paires question-réponse, mesurer recall@5 sur chaque langue.
| Situation | Stack standard ? | Adaptation |
|---|---|---|
| Codebase TypeScript / Python / Go | ✅ | Aucune |
| Codebase legacy (COBOL, PL/SQL) | ⚠️ | Chunking custom par procédure |
| Documents > 50 pages | ⚠️ | Indexation hiérarchique |
| Multilingue (>3 langues) | ❌ | Embeddings multilingues, RAM 2× |
| Codebase > 5 M lignes | ❌ | Sharding par module + retrieval en cascade |
Cas 4 : RAG sur des données qui se contredisent
Un éditeur médical avait dans son corpus deux versions d'un même protocole, l'ancienne et la nouvelle, sans marquage clair. Le RAG remontait les deux, le LLM choisissait au hasard. Solution : enrichir chaque chunk avec des métadonnées de fraîcheur (date de validation, version, statut « actif »/« déprécié ») et filtrer au retrieval par status = active AND date >= '2025-01-01'. Précision passée de 71 % à 93 % en deux jours, sans toucher aux embeddings.
Cette approche — chunks + métadonnées filtrables — est ce que la littérature appelle parfois le « RAG hybride » ou le « metadata-aware RAG ». C'est rarement abordé dans les tutos parce que ce n'est pas sexy, mais c'est ce qui distingue un RAG qui marche d'un RAG qui devine.
Ce qu'il faut retenir
Trois règles, dans cet ordre :
- Le chunking est plus important que le modèle d'embedding. Un AST chunking avec MiniLM bat un chunking naïf avec OpenAI
text-embedding-3-largeà tous les coups. - La recherche hybride n'est pas optionnelle. Sémantique + BM25 + RRF ajoute 50 lignes de code et +13 points de précision.
- Le reranker se rentabilise en tokens économisés, pas seulement en qualité. Calculez l'économie avant de l'écarter pour cause de latence.
Pour aller plus loin :
- Documentation pgvector : index HNSW vs IVFFlat selon la taille
- Papier RRF original (Cormack et al., 2009) — la formule est triviale, l'effet ne l'est pas
- Benchmark MTEB pour comparer les modèles d'embeddings sur votre langue
Conclusion
Un RAG en production qui dépasse 85 % de précision tient sur quatre composants choisis avec soin, pas sur un framework miracle. Le client fintech évoqué en intro est passé de 18 % d'hallucinations à 4 % en deux semaines, en changeant uniquement le chunking et en ajoutant un reranker. Aucun changement de modèle, aucune migration de vector DB.
