Changer le thème

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

Easton editorial illustration: LOGIC 路由开关, SEMANTIC 路由开关, 三座向量库, RRF 合并托盘

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ù :

  • d est un document
  • rank(d) son rang dans un retrieveur (à partir de 1)
  • k paramè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 routageTemps moyenPrécision retrievalCas d’usage
Sans routage (base unique)1,2 s72 %Scénario métier unique
Routage logique (LLM)1,8 s85 %Multi-domaines, intentions complexes
Routage sémantique0,5 s88 %Réponse rapide, types de requêtes stables
Ensemble RRF1,0 s92 %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. 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. 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. 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. 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 ?
Ça dépend du scénario. Sémantique : rapide (~100 ms), idéal si les types de requêtes sont stables ; logique : meilleure compréhension pour intentions complexes ou multi-domaines. En prod, mix recommandé : sémantique en tri rapide, repli logique si faible confiance.
Comment régler le paramètre RRF c d'EnsembleRetriever ?
c=60 est la valeur classique. c plus grand → classement plus uniforme ; c plus petit → plus de poids au top. Commencer à 60 puis affiner ; en pratique 40-80 fonctionne bien.
Comment gérer un échec de routage ?
Trois approches :

• 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 ?
Annotations structurées : category, language, date, etc. Plus les métadonnées sont riches et cohérentes, plus le filtrage est précis. Métadonnées manquantes ou incohérentes → efficacité réduite.
Le retrieval parallèle multi-retrieveurs est-il trop lent ?
Le parallèle bat le séquentiel, mais consomme plus. Limiter à 3-4 retrieveurs. En pratique : 3 en parallèle ~1,0 s, ~20 % plus lent qu'un seul, précision +20 % ou plus.
Quel impact du routage sur les performances RAG ?
Impact majeur. Mesures : sans routage 72 % / 1,2 s ; sémantique 88 % / 0,5 s ; Ensemble RRF 92 % / 1,0 s. Bon routage : -50 % latence, +20 % précision environ.

10 min de lecture · Publié le: 13 mai 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog