📅 ✍️ difTech

Le RAG (Retrieval-Augmented Generation) est devenu le pattern de référence pour construire des applications LLM qui ont besoin de connaissances spécifiques à un domaine. Mais entre un prototype qui fonctionne en démo et un système RAG robuste en production, il y a un monde. Cet article couvre l’architecture complète, les décisions techniques clés, et les pièges qui nous ont coûté du temps.

Architecture RAG : vue d’ensemble

Un pipeline RAG se décompose en deux phases distinctes :

Phase d’indexation (offline)

Documents sources → Chunking → Embedding → Vector Store

Phase de requête (online)

Question utilisateur → Embedding → Recherche vectorielle → Reranking → LLM → Réponse

La qualité du système dépend autant de chaque étape que de leur intégration. Une erreur dans le chunking va dégrader toutes les requêtes qui suivent, indépendamment de la qualité du LLM utilisé.

Chunking : la décision qui impacte tout

Le chunking, c’est la façon dont vous découpez vos documents en fragments avant de les embedder. C’est probablement la décision qui a le plus d’impact sur la qualité finale du RAG — et la moins bien documentée.

Fixed-size chunking

La méthode la plus simple : découpez en segments de N tokens avec un overlap de M tokens.

from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=64,
    length_function=len,
    separators=["\n\n", "\n", ".", " ", ""]
)

chunks = splitter.split_text(document_text)

Avantages : simple, déterministe, facile à debugger. Inconvénients : coupe souvent au milieu d’un concept, les chunks n’ont pas de cohérence sémantique propre.

Quand l’utiliser : documents homogènes avec une densité d’information uniforme (logs, données structurées converties en texte).

Semantic chunking

Au lieu de couper à taille fixe, on coupe aux points de rupture sémantique — là où le sujet change significativement.

from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings

semantic_splitter = SemanticChunker(
    embeddings=OpenAIEmbeddings(),
    breakpoint_threshold_type="percentile",
    breakpoint_threshold_amount=95
)

chunks = semantic_splitter.split_text(document_text)

Avantages : chunks cohérents sémantiquement, meilleure précision du retrieval. Inconvénients : plus lent (nécessite des embeddings à l’étape de chunking), chunks de taille variable (peut poser des problèmes avec certains vector stores).

Notre recommandation pratique

Pour la majorité des projets, on commence avec du fixed-size chunking (chunk_size=512, overlap=64) et on affine selon les métriques d’évaluation. Le semantic chunking apporte un gain réel sur des corpus hétérogènes (documentation technique mélangée à du contenu narratif), mais son coût de calcul et sa complexité de maintenance ne sont justifiés que si les métriques le demandent.

Un truc souvent ignoré : le parent-child chunking. On indexe de petits chunks (128-256 tokens) pour la précision du retrieval, mais on renvoie au LLM le chunk parent (512-1024 tokens) pour lui donner plus de contexte. LlamaIndex appelle ça le “Small-to-Big Retrieval” — c’est une des techniques qui améliore le plus la qualité de réponse.

Choix du Vector Store

Le vector store est la base de données qui stocke vos embeddings et permet la recherche par similarité. Le choix dépend de votre infrastructure existante et de votre volumétrie.

Vector StorePoints fortsPoints faiblesCas d’usage idéal
pgvectorSQL natif, pas d’infra supplémentaireMoins performant à très grande échelle< 1M vecteurs, stack Postgres existante
PineconeManaged, très performant, simpleCoûteux à grande échelle, vendor lock-inStartups, itération rapide
WeaviateHybride (vector + keyword), open-sourcePlus complexe à opérerRecherche hybride, besoin de contrôle
QdrantTrès performant, payloads riches, RustÉcosystème plus jeunePerformance critique, metadata filtering
ChromaIdéal pour le dev/POCPas production-ready à grande échellePrototypage uniquement

Notre choix le plus fréquent en production : pgvector pour les petits volumes (<500K documents), Qdrant ou Weaviate pour les volumes plus importants.

# Exemple avec pgvector via LangChain
from langchain_community.vectorstores import PGVector
from langchain_openai import OpenAIEmbeddings

vector_store = PGVector(
    connection_string="postgresql://user:pass@host:5432/db",
    embedding_function=OpenAIEmbeddings(),
    collection_name="knowledge_base",
    pre_delete_collection=False
)

# Ajout de documents avec métadonnées
vector_store.add_texts(
    texts=chunks,
    metadatas=[{"source": doc_id, "page": page_num} for chunk in chunks]
)

La clé : ne jamais négliger les métadonnées. Un vector store sans filtrage par métadonnée vous force à retriever dans l’intégralité du corpus. Avec des filtres (par date, par source, par catégorie), vous réduisez drastiquement le bruit et améliorez la précision.

Embedding Models : OpenAI vs Local

OpenAI text-embedding-3-large

Le modèle de référence pour la plupart des projets. Dimensions réglables (256 à 3072), excellent rapport qualité/prix, zéro maintenance.

from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(
    model="text-embedding-3-large",
    dimensions=1024  # Réduire les dimensions sans perte significative
)

Coût réel : ~$0.13 / million de tokens. Sur un corpus de 10M tokens, c’est $1.30 pour l’indexation initiale. Négligeable.

Modèles locaux (BGE, E5, Nomic)

Pour des cas où la confidentialité des données est critique, ou pour réduire la latence dans des systèmes à très haute fréquence de requêtes.

from langchain_community.embeddings import HuggingFaceEmbeddings

# BGE-M3 : excellent multilingual, open-source
embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-m3",
    model_kwargs={"device": "cuda"},  # ou "cpu"
    encode_kwargs={"normalize_embeddings": True}
)

BGE-M3 est notre choix par défaut pour les modèles locaux : multilingual, performant sur les benchmarks MTEB, et disponible sous licence permissive.

Attention : le choix du modèle d’embedding est irréversible une fois l’indexation faite (sans re-indexer). Documentez-le soigneusement et versionner votre index si vous en changez.

Reranking : l’étape qui transforme un bon RAG en excellent RAG

Le retrieval vectoriel récupère les K chunks les plus proches en termes de similarité cosinus. Mais la similarité cosinus n’est pas parfaite — elle favorise les textes qui partagent des termes avec la question, pas nécessairement ceux qui y répondent le mieux.

Le reranking consiste à prendre les K résultats du retrieval (typiquement 20-50) et à les classer à nouveau avec un modèle plus précis (cross-encoder), pour n’en garder que les N meilleurs (typiquement 3-5).

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder

# Cross-encoder reranker
reranker_model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-v2-m3")
compressor = CrossEncoderReranker(model=reranker_model, top_n=5)

# Retriever avec reranking
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=vector_store.as_retriever(search_kwargs={"k": 25})
)

Dans nos benchmarks internes, le reranking améliore la précision du retrieval de 15 à 30% selon les corpus. C’est souvent la modification à plus fort ROI sur un pipeline RAG existant.

Alternative managée : Cohere Rerank (cohere.rerank-v3-5) — plus simple à intégrer, mais payant (~$1 / 1000 requêtes).

Évaluation avec RAGAS

On ne peut pas améliorer ce qu’on ne mesure pas. RAGAS est le framework de référence pour évaluer un pipeline RAG sans avoir besoin d’un jeu de test annoté manuellement.

from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall,
)
from datasets import Dataset

# Préparer un dataset d'évaluation
eval_data = {
    "question": ["Quel est le délai de livraison ?", ...],
    "answer": [rag_pipeline.run(q) for q in questions],
    "contexts": [[ctx.page_content for ctx in retrieve(q)] for q in questions],
    "ground_truth": ["Le délai de livraison est de 3 à 5 jours ouvrés.", ...]
}

dataset = Dataset.from_dict(eval_data)
results = evaluate(dataset, metrics=[faithfulness, answer_relevancy, context_precision, context_recall])
print(results)

Les 4 métriques clés :

Mettez en place une évaluation RAGAS avant de mettre en production, et relancez-la à chaque changement significatif (nouveau modèle d’embedding, modification du chunking, ajout de données).

Monitoring en production

Un pipeline RAG en production se dégrade silencieusement si on ne le surveille pas. Les points à monitorer :

Latence par composant : retrieval, reranking, appel LLM, total. Une dégradation de la latence de retrieval signale souvent un problème côté vector store (index à reconstruire, volumétrie trop importante).

Taux de “no answer” : si votre système est configuré pour ne pas répondre quand il n’a pas assez de contexte, monitorez ce taux. Une hausse indique que vos documents ne couvrent plus les questions posées.

Feedback utilisateur : si l’interface le permet, capturez les pouces haut/bas. Ce signal est invaluable pour identifier les cas d’usage dégradés.

import langfuse

# Tracer chaque requête RAG pour le monitoring
with langfuse.trace(name="rag-query", input={"question": user_question}) as trace:
    retrieved_docs = retriever.get_relevant_documents(user_question)
    trace.span(name="retrieval", output={"num_docs": len(retrieved_docs)})

    answer = llm_chain.run(question=user_question, context=retrieved_docs)
    trace.span(name="generation", output={"answer": answer})

Langfuse est notre outil de monitoring LLM de référence — open-source, self-hostable, et intégré avec LangChain et LlamaIndex.

Coûts réels : ce qu’on ne vous dit pas

Sur un projet récent (corpus de 50K documents, 500 requêtes/jour) :

ComposantCoût mensuel
Embeddings (indexation initiale)~$15 (one-time)
Embeddings (requêtes, 500/jour)~$8/mois
LLM (GPT-4o, 500 req/jour, ~800 tokens output)~$180/mois
Vector store (Qdrant Cloud)~$70/mois
Reranking (Cohere)~$15/mois
Total~$273/mois

Le LLM représente la grande majorité du coût. Deux leviers principaux pour le réduire : passer à un modèle moins puissant pour les requêtes simples (GPT-4o-mini ou Claude Haiku), et optimiser le prompt pour réduire les tokens de contexte envoyés.

Ce que nous retenons

Un pipeline RAG robuste en production, c’est 20% de code LLM et 80% d’infrastructure, d’évaluation et de monitoring. Les équipes qui l’oublient livrent des démos impressionnantes et des systèmes fragiles.

Les points non-négociables : chunking réfléchi dès le départ, reranking activé, évaluation RAGAS avant le lancement, et un outil de monitoring dès la première semaine. Le reste s’affine avec le temps.

Besoin d'un expert ?

difTech vous accompagne dans vos projets data — de l'architecture à la mise en production.

📅 Réserver un créneau gratuit