Agentic IA (2/3): From Router to React Agent¶
Dans le premier article de cette série, nous avons construit un Router Agent avec une logique if/else rigide. Aujourd'hui, nous passons au React Agent : un agent autonome qui décide lui-même de ses actions.
Contrairement aux Router Agents qui suivent un chemin fixe, les React Agents s'adaptent dynamiquement à chaque situation. Cette flexibilité résout un problème majeur : les orchestrateurs complexes deviennent rapidement ingérables car ils nécessitent de maintenir des prompts avec exemples de plus en plus lourds.
Nous explorerons les concepts clés, comparerons les frameworks disponibles, et implémenterons notre propre solution.
Core Concepts d'agents React¶
Le pattern React = Reason-Act-Observe. L'agent raisonne → agit → observe → répète.
- Raisonne (Reason) : Analyse la situation et détermine la prochaine action à entreprendre
- Agit (Act) : Exécute l'action choisie en utilisant les tools disponibles
- Observe : Analyse le résultat de l'action pour décider de la suite
Ce cycle se répète jusqu'à ce que l'agent estime avoir accompli la tâche demandée.
| Aspect | Router Agent | React Agent |
|---|---|---|
Orchestration |
Logique if/else explicite | LLM décide dynamiquement |
Flexibilité |
Limitée au workflow prédéfini | Adaptable à tous scénarios |
Prédictibilité |
Totale | Variable selon le LLM |
Débogage |
Simple (flux linéaire) | Plus complexe (flux dynamique) |
Performance |
Optimisée (pas de réflexion) | Dépend du LLM |
Évolutivité |
Modification du code nécessaire | Ajout de tools suffit |
Nous approfondirons ces distinctions dans l'article précédent qui traite de cette comparaison.
Framework Landscape¶
Trop de frameworks sont disponibles sur le marché. Pour la production, je recommande : Pydantic AI et LangGraph. Les autres (Swarm, SmolAgent) sont plus adaptés à l'apprentissage et au prototypage.
| Framework | Atouts | Inconvénients | Use Case Idéal |
|---|---|---|---|
| LangChain | Écosystème riche, nombreux connecteurs | Complexité élevée, abstractions opaques | Prototypage rapide, intégrations multiples |
| LangGraph | Contrôle fin du workflow, Human in the loop, graph-based, state management | Courbe d'apprentissage élevée | Workflows complexes, debugging avancé |
| SmolAgent | Simplicité, transparence, performance | Écosystème plus limité | Prototypage rapide, React loop, démo |
| CrewAI | Multi-agents, orchestration avancée | Overhead pour cas simples | Équipes d'agents, tâches complexes |
| Pydantic AI | Type safety, AG-UI, intégration MCP, async, interruption et communauté active | Écosystème récent, documentation en cours | Applications type-safe et prêtes pour la production |
Les frameworks gèrent efficacement les machines d'état et l'orchestration de prompts, mais chacun a ses spécificités.
Mes recommandations sont:
- Débutant : SmolAgent
- Prototype : LangChain
- Production : Pydantic AI
- Workflows complexes : LangGraph
- Multi-agents : CrewAI
Les Tools¶
Pourquoi les LLM ont besoin de tools ?
Les LLM prédisent des tokens, ils ne calculent pas. D'où les erreurs fréquentes sur des questions simples comme "combien de 'r' dans strawberry".
Les grands modèles de langage comme GPT-4 ou Mistral sont extraordinaires pour comprendre et générer du texte, mais ils ont une limitation fondamentale : leur tâche principale est de prédire le prochain token dans une séquence, pas d'exécuter des calculs précis ou d'accéder à des informations en temps réel.
Cette limitation devient évidente avec des questions simples comme "Combien de 'r' dans le mot 'strawberry' ?" où même les meilleurs LLM peuvent se tromper. Ils "devinent" la réponse basée sur leurs données d'entraînement plutôt que de compter réellement les lettres.
C'est pourquoi ChatGPT et Claude intègrent maintenant des tools comme des calculatrices et des interpréteurs de code - pour dépasser leurs propres limites et fournir des réponses fiables.
Concept Fondamental¶
Les tools étendent les capacités des agents : calculs, bases de données, web, etc.
Prenons un exemple concret qui démontre cette différence (exemple très connu) :
# Sans tools - Le LLM hallucine souvent
question = "Combien de 'r' y a-t-il dans le mot 'strawberry' ?"
agent.run(question)
# Réponse LLM typique : "Il y a 2 'r' dans strawberry" ❌ (Incorrect, il y en a 3)
# Avec tools - Résultat précis
@tool
def count_letters(word: str, letter: str) -> str:
"""Compte le nombre d'occurrences d'une lettre dans un mot."""
count = word.lower().count(letter.lower())
return f"Le mot '{word}' contient {count} occurrence(s) de la lettre '{letter}'."
agent.add_tools(count_letters)
agent.run(question)
# Résultat : "Le mot 'strawberry' contient 3 occurrence(s) de la lettre 'r'." ✅
Les tools transforment un LLM qui "devine" en agent qui "sait". Ils permettent d'avoir des réponses factuelles plutôt que des approximations.
ChatGPT et Claude utilisent des tools : calculatrice, code Python, recherche web, analyse d'images, accès API météo, calendrier.
D'ailleurs, rappelez-vous qu'au début de ChatGPT, si vous demandiez la date actuelle, c'était une date comme 2021 qui était retournée. Aujourd'hui, le LLM appelle ses tools calendar quand il s'agit de la date actuelle.
Ce qu'il faut pour avoir un tool¶
3 éléments clés pour un bon tool : docstring détaillée, types annotés, retour consistant.
@tool
def example_tool(param1: str, param2: int) -> str:
"""Description claire de ce que fait le tool.
Args:
param1: Description du premier paramètre
param2: Description du second paramètre
Returns:
Description du résultat attendu
"""
# Implémentation de la logique
result = perform_action(param1, param2)
return f"Résultat formaté : {result}"
Démonstration Pratique¶
Faisons une démonstration pratique avec le framework SmolAgent. À la fin, vous verrez que SmolAgent sans tools = erreurs de calcul. Avec tools = précision.
SmolAgent propose :
CodeAgent(écrire du code Python et l'exécuter)ToolCallingAgent(appels JSON)
Selon vos besoins, l'une ou l'autre approche peut être utilisée. Par exemple, la navigation web nécessite souvent d'attendre après chaque interaction de page, donc les appels de tools JSON peuvent bien convenir. D'après mes discussions avec d'autres développeurs IA et data scientists, personne n'utilise CodeAgent dans un projet sérieux, surtout que la génération de code peut partir dans tous les sens.
Étape 1 : Agent sans Tools
model = LiteLLMModel(
model_id="mistral/mistral-tiny",
api_key=os.environ.get("MISTRAL_API_KEY")
)
agent = ToolCallingAgent(
tools=[], # Aucun tool disponible
model=model
)
result = agent.run("Quelle est la racine carrée de 6.12?")
Comme vous pouvez le voir, le LLM a donné une mauvaise réponse - ce qui était attendu. Le LLM utilisé est mistral-tiny (modèle trop petit), mais même pour les grands LLM, les calculs précis ne sont pas toujours évidents.
Étape 2 : Implémentation de notre premier tool
Maintenant, implémentons notre premier tool :
from smolagents import tool
@tool
def square_root_smolagent(number: float) -> str:
"""Calcule la racine carrée d'un nombre.
Args:
number: Le nombre dont on veut calculer la racine carrée
Returns:
La racine carrée du nombre
"""
import math
try:
result = math.sqrt(number)
return f"La racine carrée de {number} est {result}."
except ValueError as e:
return f"Erreur: {str(e)}"
Cette implémentation reste simple, n'est-ce pas ?
Étape 3 : Ajout du tool
from smolagents import ToolCallingAgent, LiteLLMModel
model = LiteLLMModel(
model_id="mistral/mistral-tiny",
api_key=os.environ.get("MISTRAL_API_KEY")
)
agent = ToolCallingAgent(tools=[square_root_smolagent], model=model)
result = agent.run("Quelle est la racine carrée de 6.12?")
EURÊKA ! La réponse est désormais correcte. Voilà, vous savez tout sur les tools et comment les intégrer à un LLM.
Design Pattern¶
L'un des défis majeurs quand on travaille avec plusieurs frameworks d'agents est la nécessité de réécrire les tools pour chaque framework. Voici une solution d'abstraction que j'ai développée, prête à l'utilisation :
En gros, il faut savoir que derrière les décorateurs, il y a des classes qui gèrent les docstrings pour les préparer. Cette abstraction consiste à partir de ces classes pour passer dynamiquement nos docstrings ou informations sans passer par des fonctions avec décorateur : Je vous invit à consulter les docs officiels de ces framework pour construire des Class elegant
Architecture de l'Abstraction¶
Ajouter ces méthodes dans les précédentes classes Action :
class SQLGeneratorAction(BaseAction):
# ... (code déjà vu dans l'article précédent) ...
def _create_smolagent_tool(self, **kwargs):
"""Crée un outil smolagent pour cette action"""
@smolagents.tool
def sql_generator_smolagent(query: str) -> str:
"""Génère une requête SQL à partir d'une question en langage naturel.
Args:
query: Question de l'utilisateur en langage naturel
Returns:
Requête SQL générée ou réponse textuelle selon le type de question
"""
return self.execute(query=query).output_value
return sql_generator_smolagent
def _create_langchain_tool(self, **kwargs):
"""Crée un outil langchain pour cette action"""
@langchain_core.tools.tool
def sql_generator_tool(query: str) -> str:
"""Génère une requête SQL à partir d'une question en langage naturel.
Args:
query: Question de l'utilisateur en langage naturel
Returns:
Requête SQL générée ou réponse textuelle selon le type de question
"""
return self.execute(query=query).output_value
return sql_generator_tool
class SQLExecutor(BaseAction):
# ... (code déjà vu dans l'article précédent) ...
def _create_smolagent_tool(self, **kwargs):
"""Crée un outil smolagent pour cette action"""
@smolagents.tool
def sql_executor_smolagent(sql_query: str) -> str:
"""Exécute un code SQL et retourne les résultats.
Args:
sql_query: Requête SQL valide à exécuter (SELECT uniquement)
Returns:
Résultats de la requête au format JSON string
"""
return self.execute(sql_query=sql_query).output_value
return sql_executor_smolagent
def _create_langchain_tool(self, **kwargs):
"""Crée un outil langchain pour cette action"""
@langchain_core.tools.tool
def sql_executor_tool(sql_query: str) -> str:
"""Exécute un code SQL et retourne les résultats.
Args:
sql_query: Requête SQL valide à exécuter (SELECT uniquement)
Returns:
Résultats de la requête en format list
"""
return self.execute(sql_query=sql_query).output_value
return sql_executor_tool
class PlotAction(BaseAction):
# ... (code déjà vu dans l'article précédent) ...
def _create_smolagent_tool(self, **kwargs):
"""Crée un outil smolagent pour cette action"""
@smolagents.tool
def plotter_smolagent(query: str, data: List[Dict], output_format: str = "altair") -> str:
"""Crée un graphique Altair à partir de données et d'une description.
Args:
query: Description du graphique souhaité en langage naturel
data: Données au format List[Dict]
output_format: Format de sortie ('altair', 'html', ou 'base64')
Returns:
Graphique généré au format demandé ou message d'erreur
"""
return self.execute(query=query, dataset=data, output_format=output_format).output_value
return plotter_smolagent
def _create_langchain_tool(self, **kwargs):
"""Crée un outil langchain pour cette action"""
@langchain_core.tools.tool
def plotter_tool(query: str, data: List[Dict], output_format: str = "altair") -> str:
"""Génère un graphique à partir des données générées selon la demande utilisateur.
Args:
query: Description du graphique souhaité en langage naturel
data: Données au format List[Dict]
output_format: Format de sortie ('altair', 'html', ou 'base64')
Returns:
Graphique généré au format demandé ou message d'erreur
"""
return self.execute(query=query, dataset=data, output_format=output_format).output_value
return plotter_tool
Implémenter cette approche nous permet de mieux gérer les docstrings et de ne plus avoir à recoder notre système pour chaque framework.
Applications des Agents React¶
Commençons par créer nos instances, ce sont les mêmes que dans l'article précédent :
# ... (instances déjà vues dans l'article précédent) ...
sql_generator = SQLGeneratorAction(llm)
sql_executor = SQLExecutor(mode="pandas")
sql_executor.add_table("pib_data", "./pib_data0.csv")
plot_generator = PlotAction(llm)
Allez, c'est parti pour une démonstration de React avec 2 frameworks.
1. SmolAgent¶
tool1_smolagent = sql_generator.as_smolagent_tool()
tool2_smolagent = sql_executor.as_smolagent_tool()
tool3_smolagent = plot_generator.as_smolagent_tool()
smolagent_tools = [tool1_smolagent, tool2_smolagent, tool3_smolagent]
model = smolagents.LiteLLMModel(
model_id="mistral/mistral-tiny",
api_key=os.environ.get("MISTRAL_API_KEY"),
)
agent_smolagent = smolagents.ToolCallingAgent(
tools=smolagent_tools,
model=model,
)
agent_smolagent.run("Donne-moi le PIB du Burkina Faso depuis 2020 sous forme de graphique")
Comme vous pouvez le remarquer, chaque étape correspond à l'utilisation d'un outil spécifique et l'appel des outils se fait avec la ligne :
Calling ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Calling tool: 'sql_executor_smolagent' with arguments: {'sql_query': "SELECT year, value FROM pib_data WHERE │
│ country_code = 'BFA' AND year >= '2020' ORDER BY year;"} │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
Cette interactivité et ces logs font la force de SmolAgent.
2. LangChain/LangGraph¶
tool1_langchain = sql_generator.as_langchain_tool()
tool2_langchain = sql_executor.as_langchain_tool()
tool3_langchain = plot_generator.as_langchain_tool()
langchain_tools = [tool1_langchain, tool2_langchain, tool3_langchain]
try:
# Configuration du modèle langchain
model_langchain = init_chat_model(
"mistral-medium",
api_key=config['api_key'],
)
agent_langchain_direct = create_react_agent(
model_langchain,
langchain_tools,
prompt="You are a data analysis expert assistant"
)
print("Agent Langchain direct créé avec succès !")
print(f"Nombre d'outils: {len(langchain_tools)}")
question = "Donne-moi le PIB du Burkina Faso en 2020 en graphique"
result = agent_langchain_direct.invoke({
"messages": [("human", question)]
})
print(f"Résultat: {result}")
except Exception as e:
print(f"Erreur: {e}")
EUREKA, ça marche aussi !
Discussions¶
Après avoir exploré les applications pratiques, il est important de discuter des limitations et défis que vous pourriez rencontrer avec les frameworks d'agents React.
Attention Production
Les agents React introduisent une complexité et des coûts imprévisibles qui nécessitent une vigilance particulière en production.
Problèmes Critiques¶
- Coûts Exponentiels
Impact Financier
Un agent React peut coûter 10 à 20 plus cher qu'un Router Agent selon la complexité de la tâche.
-
Debugging Complexe
-
Performances Variables
-
Dépendance aux LLM
Risques LLM
- Hallucinations : Invention d'appels d'outils inexistants
- Interprétation erronée : Mauvaise compréhension des résultats
- Inconsistance : Comportement variable selon la charge serveur
Spécificités par Framework¶
Choix Framework
Chaque framework a ses propres compromis entre simplicité, fonctionnalités et performance. Il y a beacuoup trop de framework.
LangChain : L'Écosystème Complexe
from langchain.agents import create_react_agent
# Nombreuses couches d'abstraction = debugging difficile
Problèmes spécifiques : - Abstractions opaques rendant le debugging complexe - Overhead de performance dû aux multiples couches - Documentation fragmentée pour les cas avancés
SmolAgent : La Simplicité Limitante
Contraintes : - Pas de gestion d'état persistant entre sessions - Workflows complexes non supportés et le paradigme Human-In-The-Loop - Écosystème d'outils plus restreint
LangGraph : La Configuration Verbeuse
workflow = StateGraph(State)
workflow.add_node("step1", node1)
workflow.add_edge("step1", "step2")
# Beaucoup de code pour des tâches simples
Défis d'adoption : - Courbe d'apprentissage très élevée - Over-engineering pour des cas d'usage simples - Configuration extensive requise
Recommandations de Production¶
Stratégies de Mitigation
Implémentez ces safeguards pour minimiser les risques en production.
Monitoring & Contrôle¶
# Exemple de wrapper sécurisé
class SafeReactAgent:
def __init__(self, agent, max_steps=5, timeout=30):
self.agent = agent
self.max_steps = max_steps # Limite les boucles infinies
self.timeout = timeout # Évite les blocages
def run_with_fallback(self, query):
try:
return self.agent.run(query)
except Exception:
# Fallback vers Router Agent
return self.router_fallback(query)
Solutions Recommandées¶
- Monitoring obligatoire : Langfuse, Logfire ou LiteLLM
- Fallback systems : Router Agent en cas d'échec
- Rate limiting :
max_steps <= 10par défaut - Testing rigoureux : Tests de charge et edge cases
Bilan
Ces limitations ne doivent pas vous dissuader d'utiliser les agents React, mais vous aider à prendre des décisions éclairées selon votre contexte.
Pour terminer, ceci est une matrice de décision pour resumer tout ça.
| Critère | Router Agent | React Agent |
|---|---|---|
| Coût | 💰 Bas (pas de réflexion LLM) | 💰💰 Moyen/Élevé (plus d'appels LLM) |
| Prédictibilité | ✅ Totale | ⚠️ Variable |
| Flexibilité | ❌ Limitée | ✅ Élevée |
| Debugging | ✅ Simple | ⚠️ Plus complexe lorsqu'il y a des erreurs de type |
| Performance | ✅ Rapide | ⚠️ Dépend du LLM |
| Maintenance | ❌ Code à modifier | ✅ Ajout de tools suffit |
Conclusion¶
La transition des Router Agents vers les React Agents représente un saut qualitatif important dans la sophistication de nos systèmes d'agents IA.
L'écosystème des agents IA évolue rapidement, mais les principes fondamentaux restent : clarté du design, robustesse de l'implémentation, et pragmatisme dans les choix d'architecture. En maîtrisant ces patterns, vous disposez maintenant des outils pour construire des agents à la fois puissants et maintenables, adaptés aux exigences de production moderne.
Dans le prochain article de cette série, nous explorerons le MCP (Model Context Protocol). Si une API est définie comme une communication machine-to-machine, alors MCP peut être caractérisé comme une communication LLM-to-LLM, ou plus précisément agent-to-agent.






