La plupart des démonstrations RAG échouent en production. Elles fonctionnent avec les 10 questions de la présentation de démonstration et se brisent dès que les utilisateurs posent une question nouvelle. Ce guide présente l’architecture et les modèles qui tiennent la charge : un découpage qui respecte les limites sémantiques, une récupération hybride qui prend en compte les requêtes floues et littérales, une structure de prompt qui empêche les hallucinations et une discipline des coûts qui maintient la facture stable à mesure que vous évoluez.
Les cinq décisions qui comptent
- Stratégie de découpage — influence davantage la qualité de récupération que le modèle
- Modèle d’embeddings — détermine le plafond de rappel
- Stratégie de récupération — la recherche vectorielle seule ne suffit pas
- Structure du prompt — détermine si le modèle hallucine
- Modèle de génération + mise en cache — définit l’économie unitaire
1. Découpage — le banal l’emporte sur l’ingénieux
Ne compliquez pas les choses. Utilisez un séparateur récursif de caractères avec 1000-1500 caractères par segment et un chevauchement de 100-200 caractères. Il tente de séparer d’abord sur les limites de paragraphes, puis les phrases, puis les mots. Il est stable d’un domaine à l’autre :
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answerLes solutions qui semblent plus intelligentes mais aident rarement : les modèles tenant compte des phrases, les séparateurs tenant compte du markdown et les fenêtres glissantes avec embeddings. Elles ne sont pas mauvaises — elles sont légèrement meilleures au prix d’un effort considérable. Investissez plutôt cet effort dans la récupération.
2. Modèle d’embeddings — text-embedding-3-large est le modèle par défaut
Les embeddings ne sont pas fournis par Kunavo — appelez directement un fournisseur d’embeddings pour cette étape. /v1/embeddings est un format filaire implémenté sans modèle activé derrière lui, de sorte qu’une requête vers celui-ci échoue ; GET /v1/models fait toujours autorité quant à ce qui peut être appelé. En pratique, cela ne coûte rien de plus à un pipeline RAG, hormis une seconde clé : l’appel d’embedding et l’appel de génération sont de toute façon des requêtes distinctes ; utilisez donc OpenAI, Voyage ou Cohere pour les embeddings et Kunavo pour la génération.
text-embedding-3-large offre le meilleur compromis entre multilinguisme et précision pour la plupart des cas d’utilisation en production, au tarif publié par OpenAI — vérifiez-le sur leur page de tarification plutôt qu’ici, puisque nous ne le revendons pas et ne devrions pas en citer le prix. Recréez les embeddings lorsque vous modifiez la stratégie de découpage ou le modèle lui-même, mais pas lorsque le contenu est mis à jour (ajoutez simplement les nouveaux segments).
Cas où des embeddings plus petits conviennent : récupération de textes courts en anglais uniquement (FAQ, fiches produit). Dans ces cas, text-embedding-3-small coûte 4x moins cher, avec une perte de qualité négligeable. Pour le contenu multilingue ou technique, restez sur le modèle large.
3. Récupération — le vectoriel pur est moins performant que l’hybride
La similarité vectorielle est excellente pour la correspondance sémantique floue ("how do I cancel" → "subscription cancellation policy"). Elle est très mauvaise pour les termes littéraux ("SKU-A92837" ou "order #4729"). La solution est hybride : recherche par mots-clés BM25 en parallèle, puis fusion des rangs réciproques :
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
q_embed = embed([question])[0]
vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
keyword_hits = bm25_search(question, k=k * 2)
# Reciprocal rank fusion — simple, robust
scores: dict[str, float] = {}
for rank, hit in enumerate(vector_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
for rank, hit in enumerate(keyword_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
return [chunk_by_id[i] for i in top_ids]Il s’agit du gain de qualité le plus important après le choix d’un modèle d’embeddings convenable. Suivez recall@5 sur une évaluation de 100 questions mises de côté — s’il passe de 70 % à 90 % après l’ajout de BM25, vous venez de supprimer un tiers des échecs « Je ne sais pas ».
4. Structure du prompt — trois modèles qui empêchent les hallucinations
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""
def answer(question: str) -> dict:
chunks = retrieve(question, k=5)
context = "\n\n---\n\n".join(
f"[doc:{c['id']}] {c['text']}" for c in chunks
)
resp = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[
{"role": "system", "content": [{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}]},
{"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
],
max_tokens=600,
)
return {
"text": resp.choices[0].message.content,
"sources": [c["id"] for c in chunks],
}Trois exigences incontournables dans le prompt système :
- Citer les sources — demandez au modèle d’ajouter
[doc:42]à chaque affirmation. Si l’identifiant cité n’existe pas, vous avez détecté une hallucination - Refuser explicitement — "si le contexte ne répond pas, dites que je ne dispose pas de cette information". Sans cette instruction, le modèle complète à partir de ses connaissances générales
- Sortie concise — les réponses courtes sont corrélées à des réponses exactes. Un maximum de 600 tokens est une bonne valeur par défaut en production
5. Modèle de génération + mise en cache — la couche des coûts
Claude Sonnet 4.6 est le modèle par défaut. Claude Haiku 4.5 est la solution de repli économique si vos contraintes de coût sont fortes. Encapsulez toujours le prompt système dans cache_control — le prompt système est identique à chaque appel, donc la mise en cache réduit cette partie à 10 % du tarif d’entrée.
Coût réaliste par requête à l’échelle de la production (contexte de 5K, 500 tokens de sortie, mise en cache activée) :
| Modèle | Coût par requête | À 10 000 requêtes/jour | Qualité par rapport à Sonnet |
|---|---|---|---|
claude-sonnet-4-6 | ~$0.007 | ~70 $/jour | Référence |
claude-haiku-4-5 | ~$0.001 | ~10 $/jour | ~85 %, 4× moins cher |
À 10 000 requêtes/jour → 70 $/jour sur Sonnet, 10 $/jour sur Haiku. Choisissez Sonnet pour les cas d’utilisation à forts enjeux (destinés aux clients, juridiques, médicaux) et Haiku pour le traitement interne ou en volume.
Ce qui se brise en production (et comment le détecter)
- Dérive de distribution : le corpus d’entraînement ne correspond plus aux vraies questions des utilisateurs. Échantillonnez 100 requêtes/semaine et vérifiez recall@5 manuellement
- Embeddings obsolètes : les documents sources ont été mis à jour, mais les embeddings n’ont pas été actualisés. Suivez chaque semaine la taille de l’index par rapport à celle de la source
- Fausses citations : le modèle invente des identifiants de documents qui semblent réels. Validez chaque
[doc:N]par rapport à la liste réelle des identifiants récupérés avant le rendu - Seuils de latence : la base vectorielle s’effondre au-delà d’un million de vecteurs. Utilisez un index HNSW et partitionnez par locataire dans un environnement multi-locataire
Feuille de route de production sur 4 semaines
- Semaine 1 : prototype avec 100 documents, 10 questions de test et évaluation manuelle
- Semaine 2 : passage au corpus complet, création de la récupération hybride et rédaction d’un jeu d’évaluation de 100 questions
- Semaine 3 : réglage du découpage et du prompt système en itérant sur l’évaluation, déploiement auprès des utilisateurs internes
- Semaine 4 : surveillance, budget de coûts et lancement public avec interface de citation des sources
Questions fréquentes
Quelle taille de segment un système RAG de production doit-il utiliser ?
1 000–1 500 caractères par segment, avec un chevauchement de 100–200 caractères, en utilisant un séparateur récursif de caractères qui sépare d’abord sur les limites de paragraphes, puis les phrases, puis les mots. Les modèles tenant compte des phrases, les séparateurs tenant compte du markdown et les fenêtres glissantes sont légèrement meilleurs, mais au prix d’un effort bien plus important — cet effort est mieux investi dans la récupération.
Quel modèle d’embeddings un pipeline RAG doit-il utiliser ?
text-embedding-3-large est le modèle par défaut pour le contenu multilingue et technique ; pour la récupération de textes courts en anglais uniquement, comme les FAQ et les fiches produit, text-embedding-3-small coûte environ 4× moins cher, avec une perte de qualité négligeable. Notez que les embeddings ne sont pas fournis par Kunavo : /v1/embeddings est un format filaire implémenté sans modèle activé derrière lui, de sorte que l’étape d’embedding appelle directement OpenAI, Voyage ou Cohere, tandis que l’étape de génération appelle Kunavo. Cela ne coûte rien de plus à un pipeline, hormis une seconde clé, puisque les deux opérations sont de toute façon des requêtes distinctes.
La recherche vectorielle seule suffit-elle pour la récupération RAG ?
Non. La similarité vectorielle gère bien la correspondance sémantique floue, mais échoue sur les termes littéraux comme un SKU ou un numéro de commande. Exécuter en parallèle une recherche par mots-clés BM25 et fusionner les résultats avec la fusion des rangs réciproques fait généralement passer recall@5 d’environ 70 % à environ 90 % — le gain de qualité le plus important après le choix d’un modèle d’embeddings convenable.
Comment empêcher un système RAG d’halluciner ?
Trois règles dans le prompt système : exiger une citation [doc:N] pour chaque affirmation factuelle, demander au modèle de dire qu’il ne dispose pas de l’information lorsque le contexte ne répond pas à la question, et garder les réponses courtes — un maximum de 600 tokens de sortie constitue une bonne valeur par défaut en production. Validez ensuite chaque identifiant cité par rapport à l’ensemble récupéré avant le rendu ; un identifiant inexistant est une hallucination détectée.
Combien coûte une requête RAG de production ?
Avec un contexte de 5K et 500 tokens de sortie, avec la mise en cache du prompt activée : environ 0,007 $ par requête sur Claude Sonnet 4.6 et environ 0,001 $ sur Claude Haiku 4.5 (4× moins cher, pour environ 85 % de la qualité). À 10 000 requêtes par jour, cela représente environ 70 $/jour sur Sonnet et environ 10 $/jour sur Haiku.