Créer un agent ReAct en Python pur : pensée, action, observation
Écrivez un cycle d’agent ReAct à partir de zéro en Python : le contrat Pensée, Action, Observation, une boucle while pilotée par PAUSE, un retour d’erreur et deux modes de défaillance de production.
Sources: Building a ReAct AI Agent from Scratch in Pure Python
Un agent de type planification et exécution dispose d’un astuce ingénieuse : le modèle établit au préalable une liste numérotée des étapes, que Python exécute ensuite ligne par ligne. Cela fonctionne tant que la réalité ne contredit pas le plan. Un point d’entrée se déplace, une recherche renvoie un 404, ou le modèle se trompe simplement concernant la deuxième étape. L’agent continue néanmoins d’exécuter les étapes trois à dix, car sa conception ne lui permet pas de faire des vérifications.
ReAct résout ce problème en remplaçant le plan complet par une boucle. Ci-dessous, cette boucle est écrite en Python pur, sans framework, avant d’être examinée sous l’angle des deux manières dont elle peut échouer lorsque des utilisateurs réels interviennent.
De l’itinéraire fixe au cycle de feedback
Imaginez deux manières de traverser une forêt inconnue. Un voyageur note « 1 km vers le nord, puis 500 m vers l’est » et suit ces indications à la lettre, ce qui signifie qu’il continue de marcher même lorsqu’un précipice apparaît à 200 m. L’autre s’arrête tous les quelques pas et réfléchit à voix haute : le chemin vers le nord est bloqué par un précipice, il faut donc essayer de prendre un détour vers l’ouest, et en effet, il y a un chemin sûr par là-bas.
Le premier voyageur est l’agent de type planification-exécution. Il consomme peu de ressources et est bien organisé, mais il est fragile : si l’étape 2 échoue, les étapes 3 à 10 deviennent inutiles. Le second est l’agent ReAct (raisonnement + action). Il effectue une seule étape à la fois et vérifie à nouveau son environnement avant de passer à l’étape suivante. Cela consomme plus de ressources, mais il résiste bien mieux lorsque les choses tournent mal.
Le commentaire en temps réel tenu par le deuxième voyageur est un monologue intérieur, et c’est ce mécanisme qui permet à un agent de paraître adaptatif plutôt que programmé selon un scénario prédéfini. Cependant, pour être utile, ce monologue doit pouvoir déclencher des actions réelles.
Appel d’outil en une seule phrase
Un modèle de langage ne peut émettre que du texte. Pour lui donner des moyens concrets d’agir, on utilise un dictionnaire simple qui associe des noms à des fonctions Python, comme {"get_weather": get_weather_api}. Lorsque la sortie du modèle contient la chaîne get_weather, votre code la détecte et appelle la fonction correspondante. Les outils restent exactement les mêmes dans un agent ReAct. Ce qui change, c’est la manière dont le modèle décide de les utiliser.
C’est là la partie quelque peu étrange de toute cette conception : pour faire croire à un modèle qu’il pense par lui-même, il faut le restreindre fortement.
Le contrat sous-jacent au monologue
Au lieu de demander une réponse, on contraint le modèle à un format de sortie strict et on ne lui permet pas d’utiliser aucun outil tant qu’il n’a pas respecté ce format. La boucle se compose de trois étapes :
- Réflexion. Le modèle examine la question, tout ce qui a été collecté jusqu’à présent ainsi que les échecs précédents, puis note ce qu’il doit faire ensuite et pourquoi.
- Action. Sur la base de cette réflexion, il choisit un outil dans la liste et établit l’entrée exacte à fournir.
- Observation. C’est l’étape cruciale. Le modèle doit s’arrêter. Votre code Python prend le relais, lit l’action définie, exécute la fonction réelle (une requête météo en temps réel, une consultation de base de données, etc.) et renvoie le résultat brut au modèle.
Chaque boucle du cycle fournit au modèle de nouvelles preuves sur lesquelles il peut raisonner. Le cycle d’hypothèse, d’exécution et de vérification se répète jusqu’à ce qu’une pensée aboutisse enfin à une conclusion que l’utilisateur peut comprendre. En bref, le modèle raisonne et prend des décisions, tandis que du code ordinaire agit comme arbitre, interceptant chaque appel d’outil et fournissant des informations sur ce qui s’est réellement produit.
En théorie, c’est clair. La question pratique est de savoir comment faire en sorte qu’un prédicteur de texte s’arrête en cours de route, attende un script puis reprend son raisonnement.
Rédiger la boucle manuellement
Des frameworks tels que LangChain, CrewAI et AutoGen cachent tout cela derrière une seule appelation .run(). C’est pratique tant que l’agent ne fait pas des cercles ou n’invente pas d’outil inexistant. Déboguer une boîte noire est difficile, il est donc utile de connaître son contenu.
Commencez par l’instruction système, qui établit les règles de fonctionnement et définit le contrat :
system_prompt = """
You are a dynamic AI Agent. You work in a continuous
loop of
- Thought,
- Action,
- Observation.
- PAUSE
Your available tools are:
- get_weather(location: str): Returns the current weather.
- calculate(expression: str): Evaluates a math expression.
Use the following format exactly:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [get_weather, calculate]
Action Input: the input to the action
PAUSE
(You will then receive an Observation from the environment)
Thought: I now know the answer
Final Answer: the final answer to the original input question
"""
La ligne PAUSE s’occupe de la tâche principale, et l’instruction précise également les noms exacts des champs (Question, Thought, Action, Action Input, Final Answer) afin qu’une expression régulière puisse les trouver de manière fiable dans des sorties en format libre. Elle demande au modèle d’arrêter de générer, ce qui restitue le contrôle à Python pour que l’outil puisse s’exécuter et que sa sortie puisse être enregistrée dans le contexte en tant qu’Observation.
Les modèles ne respectent pas parfaitement les instructions de format, et vous le verrez ci-dessous. Un prompt n’est cependant qu’un manuel de règles. Il faut bien quelque chose pour le faire respecter : détecter le PAUSE, exécuter la fonction et maintenir le fonctionnement. Ce quelque chose, c’est une boucle while, le cœur battant de l’agent. À chaque itération, elle envoie une demande au modèle, analyse sa réponse, exécute les outils demandés et ajoute l’ensemble de l’échange à l’historique, s’arrêtant lorsque le modèle fournit une réponse finale ou que le budget d’actions est épuisé.
import re
def react_agent(self, user_question, max_turns=5):
# Initialize our conversational memory with the system prompt
messages = [
{"role": "system", "content": self.system_prompt},
{"role": "user", "content": f"Question: {user_question}"}
]
turn_count = 0
while turn_count < max_turns:
print(f"\n--- Turn {turn_count + 1} ---")
# 1. Get the LLM's response (The Thought + Action)
response = my_react_agent.call_llm(messages)
print(response)
# Add the LLM's generation to the memory
messages.append({"role": "assistant", "content": response})
# 2. Check if the agent has reached a conclusion
if "Final Answer:" in response:
print("\n✅ Task Complete.")
#print(json.dumps(messages, indent=2))
return response.split("Final Answer:")[-1].strip()
# 3. Parse the Action and Action Input using Regex
action_match = re.search(r"Action: (.*)", response)
input_match = re.search(r"Action Input: (.*)", response)
if action_match and input_match:
action = action_match.group(1).strip()
action_input = input_match.group(1).strip()
# 4. The Observation Phase (Python takes control)
print(f"⚙️ System Executing: {action}({action_input})")
try:
action_input = json.loads(action_input)
observation_result = my_react_agent.execute_tool(action, action_input)
except (TypeError, ValueError, json.JSONDecodeError) as error_messgae:
observation_result = error_messgae
# Format the observation and feed it back to the agent
observation_text = f"Observation: {observation_result}"
messages.append({"role": "user", "content": observation_text})
print(observation_text)
else:
# If the LLM breaks the API contract, gently correct it
messages.append({"role": "user", "content": "Error: Invalid format. Please provide an Action and Action Input, or a Final Answer."})
turn_count += 1
return "❌ Agent timed out before reaching a final answer."
Que vous apporte la possession du boucle
Puisque le boucle est votre propre code, c’est vous qui décidez de ce qui se passe en cas d’échec. Un traceur généré lors d’une exécution avec un outil de type météo montre pourquoi cela est important.
Dans ce cas précis, le modèle détermine correctement qu’il a besoin d’un outil appelé get_planet_mass, mais il écrit l’entrée entre guillemets simples, {'planet': 'Earth'}. Ce n’est pas un JSON valide, ce qui fait que le parseur côté Python lance une erreur standard : il s’attendait à des noms de propriétés entre guillemets doubles.
Un script typique s’arrêterait là. Ce boucle, lui, ne lance pas d’erreur. Il prend le texte d’erreur brut et l’inclut dans la conversation en tant que prochaine Observation.
Au tour deux, le modèle lit ce message et reconnaît dans sa réflexion que le format d’entrée était incorrect et qu’il doit respecter les règles JSON. Il renvoie la requête avec {"planet": "Earth"}, le routeur exécute l’outil et la tâche est terminée.
Remarquez qu’aucun code spécial n’a été écrit pour ce cas. Il n’y a pas de mécanisme de tentative supplémentaire pour les JSON mal formatés ; seuls l’habitude de retourner des erreurs sous forme d’observations et la capacité de compréhension du texte du modèle suffisent. C’est ainsi que fonctionne en pratique un monologue intérieur. Un planificateur statique aurait planté au tour un et nécessiterait une intervention humaine pour être redémarré. La boucle ReAct a permis au modèle de diagnostiquer et de corriger sa propre syntaxe en cours d’exécution. Il n’a pas simplement suivi un plan, il a réagi à ce qui s’est réellement produit.
Où ReAct échoue en production
Le code ci-dessus semble presque magique la première fois qu’il est exécuté. Avant de le déployer, vous devez connaître les deux points où il peut échouer.
La spirale des hallucinations. Parfois, un modèle devient obstinément convaincu qu’un outil défectueux va fonctionner. Il appelle get_weather, rencontre une erreur, appelle à nouveau get_weather, voit la même erreur, et continue ainsi tandis que vos crédits API s’épuisent. La protection réside dans la limite max_turns=5 intégrée dans la boucle. Un disjoncteur aussi strict est indispensable, et aucun agent ReAct ne doit pouvoir fonctionner indéfiniment.
Inflation du contexte. Chaque cycle ajoute ses réflexions, actions et observations à la conversation. Si un outil renvoie un envoi JSON de 10 000 lignes, tout cela finit dans la fenêtre de contexte. Au tour suivant, le modèle est submergé par les données, la qualité de sa réflexion diminue et il peut perdre de vue la question initiale. Ne passez jamais directement des réponses brutes d’API ou des extraits complets du web dans une observation. Même une limite approximative, comme les premiers quelques milliers de caractères accompagnés d’une note indiquant que le reste a été tronqué, vaut mieux que de submerger le modèle. Mettez en place une petite fonction intermédiaire qui résume ou tronque d’abord les résultats, afin que le monologue reste court et clair.
Il existe également une limite que aucun mécanisme de protection ne peut résoudre. Lorsqu’un seul agent doit gérer trop d’outils volumineux ainsi que beaucoup de contexte accumulé, son raisonnement devient encombré et il s’éloigne de sa tâche. Un agent ReAct est un tacticien redoutable, mais il se révèle médiocre en tant qu’exécutant.
Combinaison des deux approches
Ces deux modèles résolvent des problèmes différents. La méthode « planifier et exécuter » apporte de la structure, tandis que ReAct confère de la résilience. De nombreux systèmes en production finissent par utiliser les deux : un planificateur élabore les étapes de haut niveau, puis confie chaque sous-tâche complexe à un agent ReAct qui s’occupe des détails en temps réel. Coordonner plusieurs agents de cette manière, construits à partir de zéro sans framework, constitue l’étape suivante naturelle après ce cycle.