RAG en temps réel sur dix millions de documents : canaux, routeurs, agents et évaluations
RAG avec dix millions de documents : un processus de récupération en moins de 200 ms grâce à des filtres SQL, une recherche hybride et un réclassement, ainsi qu’un routage des requêtes, des évaluations et un script Qdrant.
Sources: Scaling RAG to 10 Million Documents Part 2: Optimizing retrieval and generation
Intégrer dix millions de documents dans une base de données vectorielle n’est que la moitié du travail. La première partie de ce sujet portait sur l’ingestion : des routeurs de documents tels que Docling, Unstructured et Apache Tika, un découpage tenant compte de la mise en page, un indexage delta avec MinHash, ainsi qu’un design à double stockage permettant d’indexer des fichiers réels sans endommager les données ni augmenter inutilement les coûts.
La seconde partie concerne le temps de requête. Des millions de fragments intégrés ne servent à rien si la récupération prend huit secondes, si elle échoue en raison de codes d’erreur spécifiques, si elle expose des données de paie confidentielles, ou si elle est affectée par une injection de commande cachée dans un fichier obsolète. Ce qui suit décrit un processus de récupération permettant de répondre en moins de 200 ms, un tri intelligent des requêtes, une orchestration automatisée, une évaluation continue, ainsi qu’un script complet.
1. Un processus de récupération en plusieurs étapes
Un tutoriel typique conseille d’incorporer la requête de l’utilisateur et d’exécuter une recherche de similarité top_k=5 sur l’ensemble de la base de données. Avec dix millions de documents, cette approche crée deux gros problèmes.
- Les embeddings denses captent bien le sens abstrait, mais échouent avec des mots-clés exacts, des numéros de pièce alphanumériques et des codes d’erreur. Une recherche pour
Error 504 Gateway Timeoutpeut renvoyer des discussions générales sur les temps d’attente plutôt que la page de dépannage précise. - Évaluer chaque candidat à l’aide d’un modèle de pertinence gourmand en ressources est bien trop lent pour un usage interactif.
Les systèmes en production traitent donc la récupération comme un entonnoir qui réduit progressivement le nombre de candidats :
[ 10,000,000 Total Document Chunks ]
│
▼
[ Step 1: SQL Pre-Filter ] ────────► Filter by Tenant / Dept / Role / Region
│
▼
[ ~50,000 Candidates ]
│
▼
[ Step 2: Hybrid Search ] ─────────► Dense Vectors (Qdrant) + Sparse BM25
│
▼
[ Top 100 Candidates ]
│
▼
[ Step 3: Cross-Encoder ] ───────► Cohere Rerank / BGE-Reranker
│
▼
[ Final Top 5 Chunks ] ──────────► Passed to LLM Context Window
Étape 1 : pré-filtrage relationnel
Avant toute opération mathématique à haute dimension, des filtres de métadonnées basés sur le rôle de l’utilisateur, le département ou l’état du document réduisent l’espace de recherche. En passant de dix millions de fragments à environ 50 000, on diminue la latence et on assure en même temps le contrôle d’accès :
from qdrant_client.models import Filter, FieldCondition, MatchValue
# Restrict search space by user session permissions before distance scoring
user_access_filter = Filter(
must=[
FieldCondition(key="department", match=MatchValue(value="Engineering")),
FieldCondition(key="is_active", match=MatchValue(value=True))
]
)
Étape 2 : recherche hybride
Les recherches réelles existent sous deux formes complémentaires, et il est utile de comprendre les deux avant d’examiner l’algorithme de fusion.
La recherche dense ou sémantique transforme le texte en représentations vectorielles dans une base de données vectorielle, en plaçant les mots ayant des significations similaires à proximité les uns des autres. Elle gère bien les paraphrases, les synonymes et les tournures de langage conversationnelles, bien qu’elle puisse manquer un mot-clé exact et retourner des documents qui partagent simplement un concept.
La recherche sparsa ou basée sur les mots-clés, avec des algorithmes tels que BM25 ou l’ancien TF-IDF, évalue un terme en fonction de la fréquence à laquelle il apparaît dans un document par rapport à sa rareté dans l’ensemble du corpus. Elle détecte de manière fiable les mots exacts, mais ne prend pas en compte leur signification.
La réponse tentante serait d’exécuter les deux méthodes et d’additionner leurs scores, c’est là que l’approche Reciprocal Rank Fusion entre en jeu. RRF fusionne plusieurs listes classées en une seule liste unifiée sans nécessiter de normalisation des scores bruts. Un document qui se classe en tête dans les deux classements finit également en tête de la liste combinée.
Étape 3 : réclassement par encodeur croisé
La recherche hybride produit rapidement une centaine de candidats, mais un classement purement basé sur la distance vectorielle peut encore faire apparaître des résultats irrelevants. Deux méthodes d’encodage permettent de résoudre ce problème.
Un bi-encodeur est à l’origine du fonctionnement habituel de la recherche vectorielle. Les documents sont transformés en vecteurs au moment de leur ingestion, la requête de l’utilisateur devient un autre vecteur au moment de la recherche, et des calculs géométriques simples tels que la similarité cosinus ou le produit scalaire servent à les comparer. Cette méthode est rapide mais seulement modérément précise.
Un encodeur croisé lit en même temps la requête et un document, puis produit un score indiquant le degré de pertinence de ce document par rapport à la requête. Il est approfondi, mais donc lent, car évaluer un million de documents pour une seule recherche prendrait des heures. La solution consiste à les combiner. L’encodeur binaire sélectionne les 100 meilleurs candidats, et l’encodeur croisé réordonne ensuite uniquement ceux-ci pour en choisir les dix meilleurs, ce qui ne prend pas plus de 100 ms.
2. Un routeur de requêtes conditionnel
La plupart des interfaces de chat ne sont que des champs de texte, permettant aux utilisateurs de taper n’importe quoi. Un message tel que « résumer notre conversation jusqu’à présent » ou « traduire les résultats en allemand » n’a absolument pas besoin de recherche de document, et le faire passer par l’ensemble du processus est une perte de temps et d’argent.
Un routeur conditionnel se trouve à l’entrée en tant que classificateur rapide qui décide où chaque message doit être envoyé :
User Query
│
▼
[ Intent Classifier / Router ]
│
├── Simple Math / Logic ──────────► Direct Calculator / Python REPL
├── Conversational / Follow-up ───► Direct LLM Memory Context
└── Domain Knowledge Request ─────► Full Hybrid RAG Pipeline
Conceptuellement, un prompt court demande au modèle de classer la requête dans l’une de plusieurs catégories, comme la récupération dans la documentation interne ou une réponse directe :
# Conceptual Router Pattern
def route_query(user_query: str) -> str:
prompt = f"""Classify the user query into one of these routes:
- RETRIEVE: Needs internal company documentation/database lookup.
- COMPUTE: Pure math, calculation, or logic.
- DIRECT: Conversational, greetings, or basic language rewrites.
Query: {user_query}
Classification:"""
# Run a fast, lightweight classifier (or small local SLM)
decision = fast_classifier(prompt).strip()
return decision
3. Au-delà d’une simple recherche : orchestration et retour d’information
De nombreuses questions réelles nécessitent plusieurs étapes de raisonnement. Prenons l’exemple d’une demande visant à comparer les chiffres de ventes en APAC pour 2025 avec les directives de conformité de 2024 et à identifier les écarts. Cette tâche se divise en trois sous-tâches : trouver et extraire les données de ventes en APAC pour 2025, récupérer les directives de conformité de 2024, et détecter les conflits entre elles.
Un système à une seule requête échoue dans ce cas car il tente de répondre à tout en une seule demande. L’approche Agentic RAG résout ce problème grâce à des frameworks d’orchestration tels que LangGraph ou CrewAI, qui divisent le problème en tâches spécialisées coordonnées. Un planificateur peut créer plusieurs agents, chacun utilisant le même pipeline de récupération pour collecter les éléments de preuve qui lui sont nécessaires :
[ Orchestrator / Planner ]
│
┌─────────────────┴─────────────────┐
▼ ▼
[ Researcher Agent ] [ Compliance Agent ]
• Retrieves 2025 Sales Data • Retrieves 2024 Regulations
• Extracts regional tables • Parses policy constraints
│ │
└─────────────────┬─────────────────┘
▼
[ Synthesis Agent ]
• Reconciles numbers
• Validates output consistency
│
Confidence Score Check
│ │
[ Low Confidence ] [ High Confidence ]
│ │
▼ ▼
Loop back & refine Final Guardrail Validation
Auto-correction
Lorsque la récupération fournit des extraits présentant de faibles scores de similarité, ou lorsque l’agent de synthèse détecte une contradiction, celui-ci doit réagir. Il rejette le texte de mauvaise qualité, réécrit la requête en développant les abréviations ou en essayant des synonymes, puis interroge à nouveau le moteur de récupération, au lieu de fournir une réponse générique inventée. Les agents coordonnent et prennent des décisions, mais sans un pipeline RAG solide en arrière-plan, ils n’ont rien sur quoi travailler.
4. Évaluation continue
Un système de production a besoin d’une évaluation continue grâce à des frameworks automatisés tels que Ragas, TruLens ou DeepEval. Ces outils analysent plusieurs dimensions :
┌──► Faithfulness (Is the answer grounded in the retrieved chunks?)
RAG Evaluation ─┼──► Answer Relevance (Did it actually answer the user's prompt?)
├──► Context Recall (Did retrieval find all necessary reference chunks?)
└──► System Latency & Token Cost (Is it cost-effective at scale?)
En dehors des métriques opérationnelles, plusieurs indicateurs de niveau de service évaluent l’efficacité du processus de récupération :
- La fidélité correspond à la part des affirmations dans la réponse que le contexte récupéré soutient, ce qui sert à détecter les hallucinations.
- La précision du contexte est la part des fragments récupérés, parmi tous ceux inclus dans la demande, qui sont réellement pertinents.
- Le rappel du contexte vérifie si tous les faits exacts nécessaires à la réponse ont été récupérés.
- La latence, tant globale qu’au niveau de chaque étape, prend en compte le routage, le filtrage, la recherche hybride, le réordonnancement et la génération.
Chaque indicateur nécessite une valeur de référence afin de pouvoir contrôler la qualité et les performances.
5. Un script complet de récupération et de génération
Le script Python ci-dessous gère l’exécution. Il applique un pré-filtrage des métadonnées, récupère des vecteurs depuis Qdrant, assemble le contexte et appelle un serveur local LM Studio pour l’inférence :
from openai import OpenAI
from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue
# 1. Initialize Clients
# Pointing to local LM Studio running on port 8080
ai_client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="lm-studio")
qdrant_client = QdrantClient(url="http://localhost:6333")
COLLECTION_NAME = "enterprise_knowledge_base"
EMBEDDING_MODEL = "nomic-ai/nomic-embed-text-v1.5"
def retrieve_and_generate(user_query: str, user_department: str) -> str:
print(f"\n🔍 Processing query: \"{user_query}\" for department: [{user_department}]")
# 2. Vectorize the User Query
query_resp = ai_client.embeddings.create(
input=[user_query],
model=EMBEDDING_MODEL
)
query_vector = query_resp.data[0].embedding
# 3. Stage 1 & 2: SQL Pre-Filter + Vector Search
# Filter by user department and active document status
access_filter = Filter(
must=[
FieldCondition(key="department", match=MatchValue(value=user_department))
]
)
search_results = qdrant_client.search(
collection_name=COLLECTION_NAME,
query_vector=query_vector,
query_filter=access_filter,
limit=3
)
if not search_results:
return "No relevant or authorized documents found."
# 4. Context Assembly with Breadcrumbs
context_blocks = []
for hit in search_results:
breadcrumb = hit.payload.get("breadcrumb", "General")
text = hit.payload.get("text", "")
context_blocks.append(f"[{breadcrumb}]\n{text}")
full_context = "\n\n---\n\n".join(context_blocks)
# 5. Generation via Local LLM
system_prompt = (
"You are an enterprise technical assistant. "
"Answer the user query strictly using the provided context. "
"If the context does not contain the answer, explicitly state that you do not know.\n\n"
f"Context:\n{full_context}"
)
completion = ai_client.chat.completions.create(
model="local-model",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_query}
],
temperature=0.1
)
return completion.choices[0].message.content
# Example Execution
if __name__ == "__main__":
response = retrieve_and_generate(
user_query="How do I enable TLS 1.3 in config.yaml?",
user_department="Engineering"
)
print("\n🤖 Final Answer:\n", response)
La forme d’une architecture de production
Passer d’un prototype à un système qui gère des millions de documents d’entreprise signifie traiter RAG comme une discipline d’ingénierie du début à la fin, et non comme une simple appel de bibliothèque. Quatre habitudes en résument les principes :
- Ingérer de manière propre. La structure l’emporte sur l’ingéniosité ; utilisez donc des analyseurs spécialisés, conservez les informations de structure et évitez de réindexer les fichiers inchangés grâce à l’hashage delta.
- Filtrer tôt. Les contraintes de sécurité et les métadonnées relationnelles doivent être traitées dans une étape SQL exécutée avant toute computation vectorielle coûteuse.
- Mélanger les stratégies de recherche. Associez des vecteurs denses à une recherche par mots-clés BM25 ainsi qu’à un réordonneur utilisant un encodeur croisé.
- Protéger les limites. Dirigez conditionnellement les requêtes pour éviter des recherches inutiles, appliquez des règles de contrôle pour les entrées et les sorties, et impliquez des humains dans les actions à haut risque.
Lectures complémentaires
- Concevoir RAG pour 100 millions de documents : shardage, recherche hybride, coûts — Quelles sont les modifications lorsque le système RAG passe à des milliards de fragments : ingestion asynchrone, routage, shardage, récupération hybride, réclassement, autorisation, mise en cache et contrôle des coûts.
- Comment déboguer une récupération vide lorsque le système RAG ne retourne rien — Une liste de contrôle étape par étape pour les systèmes RAG qui ne retournent pas de fragments utiles : ingestion, seuils de score, filtres, réécriture des requêtes, recherche hybride, réclassement, solutions de secours et télémesure.