Routage de requêtes RAG en pratique : bases vectorielles multiples et distribution intelligente

Trois heures du matin, les alertes de production se rallument encore.
Je fixe le tableau de bord : la requête utilisateur « ventes Q3 région Est-Chine » met 12 secondes à répondre. En démo, tout tournait parfaitement — pourquoi ça casse en prod ? Pire encore, les logs montrent que le système a enclenché un flux complet de raisonnement multi-sauts pour une simple requête factuelle.
« C’est comme utiliser une ogive nucléaire pour tuer un moustique », a commenté un collègue en jetant un œil aux logs.
Ce jour-là, j’ai compris : le problème n’était pas la précision de la recherche, mais la stratégie de retrieval. Le RAG traditionnel se comporte comme un « retrieveur aveugle » — quelle que soit la question, même pipeline : recherche vectorielle + génération LLM. Les requêtes simples sont sur-traitées, les requêtes complexes sous-alimentées.
Cet article partage comment doter un système RAG d’un « contrôleur de trafic » — un routeur de requêtes. Il analyse les caractéristiques de chaque requête et la dirige vers le chemin de retrieval le plus adapté : voie rapide pour les faits simples, voie profonde pour le raisonnement complexe, retrieval multi-sources pour des réponses complètes.
Franchement, cette approche a sauvé mon projet.
1. Pourquoi le routage de requêtes ? — Du « retrieveur aveugle » à la distribution intelligente
Voici d’abord les pièges que j’ai rencontrés.
L’an dernier, nous avons construit un RAG SAV pour une entreprise e-commerce. La base de connaissances mélangeait infos produits, politique SAV, règles logistiques et campagnes marketing. Les tests passaient ; en prod, les plaintes ont doublé.
Les logs ont révélé un problème classique : pour « combien de temps pour un remboursement », le système renvoyait des délais logistiques et des règles promotionnelles. Pas que la réponse soit fausse — elle manquait de focus, noyée d’informations hors sujet.
Premier point douloureux du RAG traditionnel : l’interférence de connaissances. Quand toutes les données métier vivent dans une seule base vectorielle, les résultats deviennent un pot-pourri. Question sur le scénario A, réponse « similaire » du scénario B.
Deuxième point, plus discret : l’efficacité de réponse.
Prenons « quel est le chiffre d’affaires Q3 région Est-Chine ? » — une requête factuelle simple, une requête SQL ou BM25 suffirait. Le RAG classique enchaîne encodage vectoriel, similarité cosinus, Top-K, génération LLM… 1 à 2 secondes, forte consommation de ressources.
Troisième point : la mauvaise interprétation de l’intention.
« Trouve la vidéo la plus courte » — SelfQueryRetriever peut ne pas comprendre que « durée de lecture » est un filtre métadonnée. « La grève a-t-elle affecté le cours de l’action ? » — raisonnement multi-sauts (grève → entreprise → cours) : un seul retrieveur vectoriel ne suffit pas.
Il nous faut un routeur qui « jauge la situation » : reconnaître le type de requête et choisir le bon chemin.
Comme un système de commande au restaurant — comptoir rapide pour les plats simples, cuisine pour les plats élaborés, fenêtre livraison pour l’externe. Chacun son rôle, efficacité maximale.
2. Architecture cœur du routage — Modèle de distribution à trois niveaux
L’idée est simple : traiter par couches, chacune son métier.
J’ai conçu le système en trois niveaux :
Requête utilisateur
↓
┌─────────────────────────────────┐
│ Couche routage : classification │
│ (LLM / Semantic Router) │
│ → python_docs / js_docs / go_docs│
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Couche retrieval : bases dédiées│
│ Chroma(python) / Chroma(js)... │
│ → top-k chunks │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ Couche fusion : algorithme RRF │
│ RRF(d) = Σ 1/(k + rank(d)) │
│ → réponse finale │
└─────────────────────────────────┘
↓
Génération de la réponse
Couche routage — le « cerveau ». Analyser la requête et décider quel chemin emprunter. Deux approches dominantes : analyse logique LLM et correspondance sémantique Semantic Router. Détails plus bas.
Couche retrieval — les « mains ». Chaque scénage métier a son index vectoriel (Python, JavaScript, Go…). Le routeur choisit la base ; le retrieveur exécute la recherche.
Couche fusion — l’« arbitre ». Requêtes multi-scénarios : fusion et tri des résultats via RRF (Reciprocal Rank Fusion) — simple et efficace.
Voici comment LangChain EnsembleRetriever matérialise cette architecture :
from langchain.retrievers import EnsembleRetriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
# Initialiser la base vectorielle Python
python_store = Chroma(
persist_directory="./chroma_python",
embedding_function=OpenAIEmbeddings()
)
python_retriever = python_store.as_retriever(
search_kwargs={"k": 5}
)
# Initialiser la base vectorielle JavaScript
js_store = Chroma(
persist_directory="./chroma_js",
embedding_function=OpenAIEmbeddings()
)
js_retriever = js_store.as_retriever(
search_kwargs={"k": 5}
)
# Fusion RRF de plusieurs retrieveurs
ensemble_retriever = EnsembleRetriever(
retrievers=[python_retriever, js_retriever],
c=60 # Paramètre RRF, valeur classique
)
# Exécuter la recherche
docs = ensemble_retriever.invoke("如何处理异步回调?")
print(f"检索到 {len(docs)} 个文档片段")
Le cœur est EnsembleRetriever : appelle plusieurs retrieveurs en parallèle, fusionne via RRF. c=60 est une valeur empirique — trop grand lisse le classement, trop petit surpondère le top.
En pratique, la précision est passée de 72 % à 92 %. Contrepartie : temps de réponse plus long — plusieurs retrieveurs en parallèle consomment plus de ressources.
3. Trois stratégies de routage — Logique, sémantique, métadonnées
Comment le routeur choisit-il le chemin ? Trois approches courantes, chacune avec son domaine.
3.1 Routage logique : le LLM comme dispatcher
Idée directe : le LLM analyse l’intention, puis choisit la source de données.
Implémentation LangChain :
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_deepseek import ChatDeepSeek
# Prompt de routage
system_prompt = """你是一个编程查询路由专家。
根据用户问题涉及的编程语言,路由到相应数据源:
- Python 相关问题 → python_docs
- JavaScript 相关问题 → js_docs
- Go 相关问题 → golang_docs
- 无法判断 → general_docs
只返回数据源名称,不要包含其他内容。"""
# Construire la chaîne de routage
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{query}")
])
llm = ChatDeepSeek(model="deepseek-chat", temperature=0)
router_chain = prompt | llm | StrOutputParser()
# Exécuter le routage
query = "如何在 Python 中实现异步爬虫?"
datasource = router_chain.invoke({"query": query})
print(f"路由结果: {datasource}") # 输出: python_docs
Avantage : flexibilité. Le LLM gère les intentions complexes — « comparer les modèles de concurrence Python et Go » peut renvoyer plusieurs sources puis passer par Ensemble.
Inconvénient : lent. Chaque routage = un appel LLM, +0,5 à 1 s de latence, coûts API qui s’accumulent.
3.2 Routage sémantique : remplacer l’appel LLM par la similarité vectorielle
Pour la vitesse, Semantic Router est préférable.
Principe proche d’un « if/else flou » : règles prédéfinies (exemples par route), similarité vectorielle entre requête et exemples — la route la plus proche l’emporte.
Avec la bibliothèque semantic-router :
from semantic_router import Route, RouteLayer
from semantic_router.encoders import OpenAIEncoder
# Définir les règles de routage
python_route = Route(
name="python_docs",
utterances=[
"如何在 Python 中读取文件",
"Python 装饰器的用法",
"怎么用 Python 实现异步编程",
"Python 列表推导式语法",
]
)
js_route = Route(
name="js_docs",
utterances=[
"JavaScript 异步回调怎么处理",
"怎么在 JS 中操作 DOM",
"Node.js 事件循环机制",
"JS Promise 和 async/await 区别",
]
)
# Créer la couche de routage
route_layer = RouteLayer(
encoder=OpenAIEncoder(),
routes=[python_route, js_route]
)
# Exécuter le routage (sans appel LLM)
query = "Python 的生成器怎么用?"
result = route_layer(query)
print(f"路由结果: {result.name}") # 输出: python_docs
Le routage sémantique est 3 à 5 fois plus rapide que le routage LLM. Embedding OpenAI ~100 ms, LLM 500 ms+.
Limite : règles à prédéfinir. Requête hors périmètre → échec (None). Idéal quand les types de requêtes sont relativement stables.
3.3 Routage par métadonnées : filtrage sur champs structurés
Base de connaissances riche en métadonnées (catégorie, langue, date) ? SelfQueryRetriever permet un filtrage précis.
from langchain.retrievers.self_query.base import SelfQueryRetriever
from langchain.chains.query_constructor.base import AttributeInfo
from langchain_openai import ChatOpenAI
# Définir les champs métadonnées
metadata_field_info = [
AttributeInfo(
name="category",
description="文档分类:tutorial, api, guide, troubleshooting",
type="string"
),
AttributeInfo(
name="language",
description="编程语言:python, javascript, golang",
type="string"
),
AttributeInfo(
name="date",
description="文档发布日期",
type="date"
)
]
# Créer le retrieveur
llm = ChatOpenAI(model="gpt-4", temperature=0)
retriever = SelfQueryRetriever.from_llm(
llm=llm,
vectorstore=vectorstore,
document_contents="编程技术文档",
metadata_field_info=metadata_field_info,
verbose=True
)
# La requête est convertie en filtres métadonnées
query = "Python 教程类文档,最近的"
docs = retriever.invoke(query)
# Filtres générés automatiquement :
# category == "tutorial" AND language == "python"
# tri par date décroissante
Avantage : précision. Le LLM transforme le langage naturel en filtres structurés avant la recherche vectorielle. Exige des métadonnées de qualité — sans catégories ou tags langue, inutilisable.
4. Collaboration multi-bases vectorielles — EnsembleRetriever en profondeur
Les stratégies de routage répondent à « quelle base interroger ». Certaines requêtes touchent plusieurs scénarios — « comparer l’async Python et JavaScript » — il faut chercher partout et fusionner.
Le cœur d’EnsembleRetriever : l’algorithme RRF.
Principe de l’algorithme RRF
Formule simple :
RRF(d) = Σ 1/(k + rank(d))
Où :
dest un documentrank(d)son rang dans un retrieveur (à partir de 1)kparamètre de lissage, typiquement 60
Exemple : document X classé 2e par A → score 1/(60+2) = 0,0156 ; 5e par B → 1/(60+5) = 0,0154 ; total = 0,031.
Tous les documents sont scorés ainsi, puis triés.
Pourquoi RRF bat une moyenne pondérée simple ? Il tient compte du rang, pas du score brut. Les échelles diffèrent entre retrieveurs (cosinus 0-1 vs scores BM25) — RRF contourne le problème.
Retrieval hybride dense + sparse
En production, on combine souvent recherche dense (vecteurs) et sparse (BM25).
Vecteurs pour le sens, BM25 pour les mots-clés — complémentaires.
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import Chroma
# Sparse : BM25
bm25_retriever = BM25Retriever.from_texts(
documents_text_list,
k=5
)
# Dense : vecteurs
vector_retriever = Chroma.from_texts(
documents_text_list,
embedding=OpenAIEmbeddings()
).as_retriever(search_kwargs={"k": 5})
# Hybride
ensemble = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6], # BM25 0.4, vecteurs 0.6
c=60
)
# Exécuter
query = "LangChain Agent 工具调用"
docs = ensemble.invoke(query)
Astuce de réglage des poids :
- Requêtes sémantiques (« comment implémenter un Q&A intelligent ») : vecteurs 0,6-0,7
- Correspondance mots-clés (« nouveautés Python 3.11 ») : BM25 0,5-0,6
- Usage général : 0,5/0,5
Évaluation de la qualité de retrieval
TruLens permet de mesurer l’efficacité d’EnsembleRetriever :
from trulens_eval import Feedback, TruChain
from trulens_eval.feedback.provider.openai import OpenAI
provider = OpenAI()
# Définir les métriques
relevance_feedback = Feedback(
provider.relevance,
name="Answer Relevance"
).on_input_output()
context_relevance_feedback = Feedback(
provider.context_relevance,
name="Context Relevance"
).on_input().on(context)
# Enregistrer la chaîne d'évaluation
tru_recorder = TruChain(
chain=ensemble_retriever_chain,
feedbacks=[relevance_feedback, context_relevance_feedback],
feedback_mode="with_chain"
)
# Exécuter l'évaluation
with tru_recorder as recording:
response = ensemble_retriever_chain.invoke({"query": test_query})
TruLens fournit « Answer Relevance » et « Context Relevance ». En pratique : contexte moyen 0,72 en vecteur seul, 0,91 avec Ensemble.
5. Comparaison de performances et bonnes pratiques
Théorie mise de côté — quels résultats concrets ?
Même jeu de test (500 requêtes, 4 scénarios métier), quatre approches :
| Stratégie de routage | Temps moyen | Précision retrieval | Cas d’usage |
|---|---|---|---|
| Sans routage (base unique) | 1,2 s | 72 % | Scénario métier unique |
| Routage logique (LLM) | 1,8 s | 85 % | Multi-domaines, intentions complexes |
| Routage sémantique | 0,5 s | 88 % | Réponse rapide, types de requêtes stables |
| Ensemble RRF | 1,0 s | 92 % | Scénarios mixtes, retrieval multi-sources |
Lecture des chiffres clés
Le routage sémantique est 3 à 4 fois plus rapide que le logique — Embedding ~100 ms vs LLM ~800 ms. Types de requêtes stables → sémantique en premier choix.
Ensemble RRF a la meilleure précision — plusieurs retrieveurs couvrent des espaces sémantiques différents, RRF équilibre leurs forces. Coût : latence légèrement supérieure au retrieveur unique.
Sans routage, paradoxalement plus lent — une seule base renvoie beaucoup de bruit ; le LLM extrait la réponse dans plus de bruit, ralentissant la génération.
Synthèse des bonnes pratiques
Quelques recommandations tirées de l’expérience terrain :
1. Scénarios simples → routage sémantique
Scénario clair (Q&A doc Python), types stables (FAQ, exemples code, dépannage) → Semantic Router direct. Rapide, peu coûteux.
2. Raisonnement complexe → routage logique
Multi-sauts ou comparaisons inter-domaines — le LLM comprend mieux. Ex. « différence concurrence Python et Go » → deux bases.
3. Retrieval multi-sources → Ensemble
Intention incertaine : interroger plusieurs bases, fusionner par RRF. Plus sûr qu’un seul routeur — limiter à 3-4 retrieveurs max, au-delà la latence explose.
4. Métadonnées riches → SelfQuery
Documents bien annotés (catégorie, langue, date, auteur) → SelfQueryRetriever parse l’intention et filtre précisément.
5. Stratégie dynamique
En prod : hybride — routage sémantique en premier (~100 ms), si score < seuil (ex. 0,6), repli sur routage logique. La majorité des requêtes simples reste rapide ; les cas complexes sont bien traités.
Conclusion
Beaucoup investissent dans le choix d’embeddings et le prompt engineering en oubliant le routage de requêtes.
Une décision simple — voie rapide ou profonde ? — peut faire passer le temps de réponse de 1,8 s à 0,5 s et la précision de 72 % à 92 %.
Principes à retenir :
- Requêtes simples → routage sémantique : rapide, économique
- Raisonnement complexe → routage logique : meilleure compréhension LLM
- Multi-sources → Ensemble : RRF pour des réponses complètes
- Métadonnées riches → SelfQuery : filtrage précis, moins de bruit
Notre équipe tourne en prod sur un mix : sémantique en première couche, repli logique si faible confiance, Ensemble automatique en multi-sources. Taux de résolution du chatbot SAV : 68 % → 89 %.
Code d’exemple complet sur le dépôt GitHub — clonez et testez. Questions bienvenues en commentaires.
Implémenter un système de routage de requêtes RAG
Construire une architecture RAG intelligente avec routage sémantique, routage logique et retrieval multi-sources
⏱️ Estimated time: 45 min
- 1
Step 1: Choisir la stratégie de routage
Selon le scénario métier :
• Types de requêtes stables → routage sémantique (rapide, ~100 ms)
• Multi-domaines → routage logique (précision élevée, ~800 ms)
• Scénarios mixtes → Ensemble RRF (précision max 92 %) - 2
Step 2: Implémenter le routage sémantique
Avec la bibliothèque semantic-router :
```python
from semantic_router import Route, RouteLayer
from semantic_router.encoders import OpenAIEncoder
python_route = Route(
name="python_docs",
utterances=["Python 异步编程", "Python 装饰器"]
)
route_layer = RouteLayer(
encoder=OpenAIEncoder(),
routes=[python_route]
)
``` - 3
Step 3: Configurer EnsembleRetriever
Fusionner les résultats de plusieurs retrieveurs :
```python
from langchain.retrievers import EnsembleRetriever
ensemble = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.4, 0.6],
c=60
)
```
c=60 est une valeur empirique ; ajuster les poids selon le type de requête. - 4
Step 4: Évaluer la qualité de retrieval
Métriques TruLens :
• Pertinence de la réponse (Answer Relevance)
• Pertinence du contexte (Context Relevance)
• Comparer avant/après optimisation
En pratique : pertinence contextuelle de 0,72 à 0,91 avec Ensemble.
FAQ
Routage sémantique ou logique — lequel choisir ?
Comment régler le paramètre RRF c d'EnsembleRetriever ?
Comment gérer un échec de routage ?
• Route par défaut : si score sémantique sous le seuil, retrieveur par défaut
• Repli logique : après échec sémantique, appel LLM pour décision précise
• Multi-sources : en cas de doute, Ensemble sur plusieurs bases + fusion RRF
Quelles exigences métadonnées pour SelfQueryRetriever ?
Le retrieval parallèle multi-retrieveurs est-il trop lent ?
Quel impact du routage sur les performances RAG ?
10 min de lecture · Publié le: 13 mai 2026 · Mis à jour le: 27 juil. 2026
Guide d'ingénierie RAG
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Routage de requêtes RAG en pratique : collaboration multi-bases vectorielles et distribution intelligente
Guide pratique du routage de requêtes RAG : comparaison systématique du routage logique, du routage sémantique et d'EnsembleRetriever, avec implémentation LangChain complète, Semantic Caching et stratégies d'optimisation des coûts via Tiered Retrieval.
Partie 4 sur 5
Suivant
C’est le dernier article publié dans cette série pour le moment.



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire