Note : le routage étant l’articulation des traitements, plusieurs des articles précédents vont être ré-écrits pour s’y rattacher.
Objectif et principe du routage
Le routage intervient en amont de ReAct, le but étant d’éviter d’utiliser un marteau-pilon pour écraser une mouche. Avec sa conséquence pour l’environnement :
Nous décrirons :
– la classification de la requête,
– la logique du routage,
– l’intégration des outils.
Classification de la requête
On commence par classer la requête avec un LLM router (ou Query Classifier). Nous distinguons trois classifications : ’direct’, ’tool’, ’react’.
- class ComplexityRouter:
- def __init__(self, tools: list[FunctionTool]):
- self.tools = tools
- def route(self, query: str) -> list[FunctionTool]:
- # Heuristique simple : route selon mots-clés
- if any(word in query.lower() for word in ["moyenne", "écart", "tendance"]):
- return [t for t in self.tools if getattr(t, "complexity", None) == "tool"]
- elif any(word in query.lower() for word in ["tva", "prix", "ht", "ttc"]):
- return [t for t in self.tools if getattr(t, "complexity", None) == "simple"]
- elif any(word in query.lower() for word in ["croiser", "relier", "jointure"]):
- return [t for t in self.tools if getattr(t, "complexity", None) == "react"]
- else:
- # Fallback : tout proposer
- return self.tools
Ce code est simpliste : dans un scénario réel, on développera le routage en s’appuyant de façon dynamique, toujours avec un appel au LLM, sur la description des outils sélectionnés ou créés pour les besoins du scénario.
On comprend que la classification va dépendre étroitement de l’application, raison pour laquelle nous avons défini des thèmes permettant des configurations particularisées dans une approche multi-utilisateurs.
- def classify_query(query: str) -> str:
- prompt = f"""Classify the following query:
- - If it's factual/simple, return 'direct'
- - If it needs a tool (search, calculator, etc.), return 'tool'
- - If it needs reasoning and tool use, return 'react' :
- Query: {query}
- Classification:"""
- return llm.predict(prompt).strip().lower()
Pourquoi un LLM est pertinent ici ?
– Il comprend le langage naturel dans sa richesse.
– Il peut détecter des requêtes multi-étapes, même si elles sont formulées subtilement.
– Il peut juger si une requête nécessite du raisonnement, de la planification, ou une coordination d’outils.
Logique du routage
- def route_query(query: str):
- category = classify_query(query)
- if category == "direct":
- return direct_agent.query(query)
- elif category == "tool":
- return tool_agent.query(query)
- else category == "react":
- return react_agent.query(query)
On observera que ReAct devra traiter non seulement les questions complexes, mais également, en dernier ressort, celles qui n’auront pas été classifiées comme ’simple’ ou ’tool’.
Il est important de noter que, si ReAct est mis en jeu, tous les outils pourront être sélectionnés aux étapes du raisonnement.
intégration des outils
Une de ces trois classes d’agent sera utilisée en fonction de la classification :
Direct Agent
On retrouvera dans cette classe notre agent RAG : un moteur de requête sur un VectorStoreIndex.
Tool Agent
Agent outil : utilise ToolNode ou QueryTool avec des API externes, en particulier fournies par Model Context Protocol (MCP) .
ReAct Agent
– Agent ReAct : utilise LLMRouter et ReActAgent du framework d’agents LlamaIndex.
Tous les outils seront mis à disposition de la boucle ReAct.
La classification "react" est spéciale : elle ne décrit pas un outil en soi, mais plutôt une stratégie de raisonnement. Autrement dit :
– Un outil "simple" : répond directement (recherche, calcul, transformation),
– Un outil "tool" : interagit avec une source externe ou fait un traitement structuré,
– Un outil "react" : n’est pas un outil, mais une combinaison d’outils orchestrée par un agent ReAct.
Le processus de sélection et d’exécution de ReAct se déroule comme ceci :