Cours Python · Module 10: IA et modèles de langage

Agents ReAct - Raisonner et agir

13 min de lecture
Dans cette leçon7

Bienvenue ! Ici Darwin. Demande à un modèle ordinaire : "Quel temps fait-il au Serengeti et quels animaux y vivent ?". Il répondra avec aisance, mais inventera la météo, car il n'a accès à aucune station météo. Dans les leçons précédentes, nous avons donné des outils au modèle. Nous allons maintenant lui apprendre à s'en servir étape par étape, comme un pisteur qui réfléchit d'abord, suit ensuite les traces, et évalue enfin ce qu'il a trouvé.

ReAct (Reasoning and Acting) est un patron de conception pour agents d'IA qui combine le raisonnement et la prise d'actions. L'agent alterne entre "penser" (reasoning) et "agir" (acting), ce qui donne de meilleurs résultats ! Le patron a été décrit par des chercheurs de Princeton et de Google dans l'article de 2022 "ReAct: Synergizing Reasoning and Acting in Language Models".

Qu'est-ce que ReAct ?

ReAct est un paradigme dans lequel un agent d'IA :

  1. Thought - analyse la situation, planifie
  2. Action - effectue une action (par ex. appelle un outil)
  3. Observation - observe le résultat de l'action
  4. Repeat - répète le cycle jusqu'à ce que le problème soit résolu

La boucle tourne jusqu'à ce que l'agent juge qu'il a assez d'informations. Le bas du schéma montre le journal d'une expédition :

1┌─────────────────────────────────────────────────────────┐
2│                    Boucle ReAct                         │
3├─────────────────────────────────────────────────────────┤
4│                                                         │
5│  ┌──────────┐    ┌──────────┐    ┌─────────────┐       │
6│  │ Thought  │───▶│  Action  │───▶│ Observation │       │
7│  │ (Penser) │    │ (Agir)   │    │ (Observer)  │       │
8│  └──────────┘    └──────────┘    └──────┬──────┘       │
9│       ▲                                  │              │
10│       └──────────────────────────────────┘              │
11│                    (Répéter)                            │
12│                                                         │
13│  Exemple :                                              │
14│  Thought : Je dois vérifier la météo au Serengeti       │
15│  Action : get_weather("Serengeti")                      │
16│  Observation : Température 28°C, ensoleillé              │
17│  Thought : Je peux maintenant répondre à l'utilisateur  │
18│  Action : final_answer("La météo au Serengeti...")      │
19│                                                         │
20└─────────────────────────────────────────────────────────┘

L'Observation est la clé : le modèle ne devine pas le résultat d'une action, il reçoit la vraie réponse de l'outil.

Implémentation de base de ReAct

La classe ReActAgent stocke un dict d'outils, une limite d'itérations et l'historique des étapes. Le system prompt insère les descriptions des outils, tirées des docstrings des fonctions (func.__doc__), et apprend au modèle le format Thought/Action/Answer :

1from openai import OpenAI
2from typing import Callable
3import json
4import re
5
6client = OpenAI()
7
8class ReActAgent:
9    """Un agent implémentant le patron ReAct."""
10
11    def __init__(self, tools: dict[str, Callable], max_iterations: int = 10):
12        self.tools = tools
13        self.max_iterations = max_iterations
14        self.history: list[str] = []
15
16    def _create_system_prompt(self) -> str:
17        """Crée un prompt système avec la description des outils."""
18        tool_descriptions = "\n".join([
19            f"- {name}: {func.__doc__}"
20            for name, func in self.tools.items()
21        ])
22
23        return f"""Tu es un assistant Safari serviable qui résout les problèmes étape par étape.
24
25Outils disponibles :
26{tool_descriptions}
27
28Utilise ce format :
29Thought: [ton raisonnement sur ce qu'il faut faire ensuite]
30Action: [tool_name(arguments)]
31
32Quand tu as assez d'informations pour répondre :
33Thought: [raisonnement final]
34Answer: [ta réponse pour l'utilisateur]
35
36Réfléchis toujours avant d'agir. Analyse les observations avant de passer à l'étape suivante."""

La limite max_iterations est un fusible : sans elle, un agent pris dans une boucle brûlerait des tokens sans fin.

Le parseur extrait trois fragments avec des expressions régulières du module re. Le drapeau re.DOTALL permet au point de couvrir aussi les retours à la ligne :

1    def _parse_response(self, response: str) -> tuple[str, str, str]:
2        """Analyse la réponse en pensée, action et réponse."""
3        thought_match = re.search(r'Thought:\s*(.+?)(?=Action:|Answer:|$)', response, re.DOTALL)
4        action_match = re.search(r'Action:\s*(.+?)(?=Thought:|Observation:|Answer:|$)', response, re.DOTALL)
5        answer_match = re.search(r'Answer:\s*(.+)', response, re.DOTALL)
6
7        thought = thought_match.group(1).strip() if thought_match else ""
8        action = action_match.group(1).strip() if action_match else ""
9        answer = answer_match.group(1).strip() if answer_match else ""
10
11        return thought, action, answer

Une absence de correspondance ne lève aucune exception, elle renvoie simplement une chaîne vide.

L'exécution d'une action transforme un texte du genre get_weather("Serengeti") en appel d'une vraie fonction :

1    def _execute_action(self, action_str: str) -> str:
2        """Exécute une action et renvoie l'observation."""
3        # Analyser le nom de la fonction et les arguments
4        match = re.match(r'(\w+)\((.*)\)', action_str)
5        if not match:
6            return f"Erreur : Format d'action invalide : {action_str}"
7
8        func_name = match.group(1)
9        args_str = match.group(2)
10
11        if func_name not in self.tools:
12            return f"Erreur : Outil inconnu : {func_name}"
13
14        try:
15            # Analyse simple des arguments
16            if args_str:
17                args = [arg.strip().strip('"').strip("'") for arg in args_str.split(',')]
18                result = self.tools[func_name](*args)
19            else:
20                result = self.tools[func_name]()
21            return str(result)
22        except Exception as e:
23            return f"Erreur d'exécution : {str(e)}"

Chaque erreur revient au modèle sous forme de texte d'observation au lieu d'arrêter le programme. Le découpage des arguments par split(',') est volontairement simple et casse sur une virgule à l'intérieur d'une chaîne : en production, mieux vaut utiliser le function calling natif, que je montre plus bas.

La méthode run relie le tout en une boucle :

1    def run(self, query: str) -> str:
2        """Exécute l'agent ReAct."""
3        self.history = [f"User: {query}"]
4
5        for i in range(self.max_iterations):
6            # Construire le contexte
7            context = "\n".join(self.history)
8
9            # Appeler le LLM
10            response = client.chat.completions.create(
11                model="gpt-4.1-mini",
12                messages=[
13                    {"role": "system", "content": self._create_system_prompt()},
14                    {"role": "user", "content": context}
15                ],
16                temperature=0
17            )
18
19            llm_response = response.choices[0].message.content
20            thought, action, answer = self._parse_response(llm_response)
21
22            # Enregistrer la pensée
23            if thought:
24                self.history.append(f"Thought: {thought}")
25                print(f"Thought : {thought}")
26
27            # S'il y a une réponse finale
28            if answer:
29                print(f"Answer : {answer}")
30                return answer
31
32            # Exécuter l'action
33            if action:
34                self.history.append(f"Action: {action}")
35                print(f"Action : {action}")
36
37                observation = self._execute_action(action)
38                self.history.append(f"Observation: {observation}")
39                print(f"Observation : {observation}")
40
41        return "Limite d'itérations dépassée sans trouver de réponse."

Chaque tour ajoute la pensée, l'action et l'observation à history, et à l'appel suivant, le modèle reçoit tout l'historique. temperature=0 donne des décisions aussi reproductibles que possible, c'est pourquoi j'ai choisi gpt-4.1-mini : les modèles de raisonnement comme gpt-5-mini n'acceptent que la température par défaut.

Exemple d'utilisation de ReAct

Les outils sont des fonctions ordinaires avec des docstrings. Ici, elles renvoient des données codées en dur, dans une vraie application elles appelleraient une API météo ou une base d'espèces :

1# Définitions des outils
2def get_weather(location: str) -> str:
3    """Récupère la météo actuelle pour un lieu donné."""
4    weather_data = {
5        "Serengeti": "28°C, ensoleillé, humidité 45%",
6        "Kilimanjaro": "5°C, nuageux, neige au sommet",
7        "Nairobi": "22°C, partiellement nuageux"
8    }
9    return weather_data.get(location, f"Pas de données météo pour {location}")
10
11def search_animals(query: str) -> str:
12    """Recherche des informations sur les animaux du Safari."""
13    animals = {
14        "lion": "Panthera leo - le plus grand félin d'Afrique, vit en groupes appelés troupes",
15        "elephant": "Loxodonta africana - le plus grand animal terrestre, vit 60-70 ans",
16        "giraffe": "Giraffa camelopardalis - l'animal le plus grand du monde, jusqu'à 5,5 m de haut"
17    }
18    for key, value in animals.items():
19        if key in query.lower():
20            return value
21    return f"Aucune information trouvée sur : {query}"
22
23def calculate_distance(from_loc: str, to_loc: str) -> str:
24    """Calcule la distance entre des lieux du Safari."""
25    distances = {
26        ("Nairobi", "Serengeti"): 350,
27        ("Serengeti", "Kilimanjaro"): 280,
28        ("Nairobi", "Kilimanjaro"): 200
29    }
30    key = (from_loc, to_loc)
31    reverse_key = (to_loc, from_loc)
32
33    if key in distances:
34        return f"{distances[key]} km"
35    elif reverse_key in distances:
36        return f"{distances[reverse_key]} km"
37    return f"Distance inconnue entre {from_loc} et {to_loc}"

La docstring n'est pas décorative : elle part dans le system prompt, et c'est sur elle que le modèle s'appuie pour choisir l'outil.

On enregistre maintenant les outils et on envoie l'agent sur le terrain :

1# Créer l'agent
2tools = {
3    "get_weather": get_weather,
4    "search_animals": search_animals,
5    "calculate_distance": calculate_distance
6}
7
8agent = ReActAgent(tools=tools)
9
10# Exécuter
11result = agent.run("Quelle est la météo au Serengeti et quels animaux y vivent ?")
12print(f"\nRéponse finale : {result}")

Pour cette question, l'agent effectue généralement deux actions, get_weather et search_animals, avant de renvoyer une Answer.

ReAct avec LangChain

LangChain propose un agent ReAct tout prêt, donc tu n'écris pas la boucle toi-même : le décorateur @tool transforme une fonction en outil et prend la description dans sa docstring :

1from langchain.agents import create_react_agent, AgentExecutor
2from langchain_openai import ChatOpenAI
3from langchain.tools import tool
4from langchain import hub
5
6# Outils sous forme d'outils LangChain
7@tool
8def safari_weather(location: str) -> str:
9    """Récupère la météo d'un lieu du Safari."""
10    return f"Météo à {location} : 26°C, ensoleillé"
11
12@tool
13def animal_info(animal_name: str) -> str:
14    """Recherche des informations sur un animal du Safari."""
15    return f"Informations sur {animal_name} : Un magnifique animal africain !"
16
17@tool
18def safari_distance(from_location: str, to_location: str) -> str:
19    """Calcule la distance entre des points du Safari."""
20    return f"Distance de {from_location} à {to_location} : 150 km"

On relie maintenant le modèle, un prompt ReAct tout prêt du LangChain Hub et les outils, et AgentExecutor pilote toute la boucle :

1# LLM et prompt
2llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
3prompt = hub.pull("hwchase17/react")
4
5# Agent ReAct
6tools = [safari_weather, animal_info, safari_distance]
7agent = create_react_agent(llm, tools, prompt)
8
9# Executor
10agent_executor = AgentExecutor(
11    agent=agent,
12    tools=tools,
13    verbose=True,
14    handle_parsing_errors=True,
15    max_iterations=10
16)
17
18# Exécuter
19result = agent_executor.invoke({
20    "input": "Vérifie la météo au Serengeti et parle-moi des lions"
21})
22print(result["output"])

handle_parsing_errors=True renvoie l'erreur de format au modèle au lieu de tout arrêter, et verbose=True affiche chaque étape. Attention aux versions : ce code fonctionne avec LangChain 0.3. Dans LangChain 1.0, create_react_agent, AgentExecutor et hub ont été déplacés dans le paquet langchain-classic (on les importe depuis langchain_classic), et le successeur recommandé est create_agent de langchain.agents.

ReAct avec OpenAI Function Calling

Les modèles modernes savent appeler des outils nativement, donc il n'y a plus de texte à parser. On décrit l'outil avec un schéma JSON, et la fonction d'exécution reçoit des arguments tout prêts :

1from openai import OpenAI
2import json
3
4client = OpenAI()
5
6# Définitions des outils pour OpenAI
7tools = [
8    {
9        "type": "function",
10        "function": {
11            "name": "get_safari_info",
12            "description": "Récupère des informations sur le Safari",
13            "parameters": {
14                "type": "object",
15                "properties": {
16                    "topic": {
17                        "type": "string",
18                        "description": "Sujet : météo, animaux, lieux"
19                    },
20                    "query": {
21                        "type": "string",
22                        "description": "Requête détaillée"
23                    }
24                },
25                "required": ["topic", "query"]
26            }
27        }
28    }
29]
30
31def execute_safari_tool(topic: str, query: str) -> str:
32    """Exécute un outil Safari."""
33    if topic == "weather":
34        return f"Météo pour {query} : 28°C, ensoleillé"
35    elif topic == "animals":
36        return f"Informations sur {query} : Un animal fascinant du Safari !"
37    elif topic == "locations":
38        return f"Lieu {query} : Une destination Safari populaire"
39    return "Sujet inconnu"

Le champ description joue le même rôle qu'une docstring, et required indique quels arguments le modèle ne peut pas omettre.

La boucle a l'air familière, sauf qu'au lieu d'expressions régulières, on lit message.tool_calls :

1def react_with_functions(query: str) -> str:
2    """ReAct en utilisant le function calling d'OpenAI."""
3    messages = [
4        {
5            "role": "system",
6            "content": "Tu es un expert du Safari. Réponds étape par étape, en utilisant les outils disponibles."
7        },
8        {"role": "user", "content": query}
9    ]
10
11    for _ in range(5):  # Nombre max d'itérations
12        response = client.chat.completions.create(
13            model="gpt-5-mini",
14            messages=messages,
15            tools=tools,
16            tool_choice="auto"
17        )
18
19        message = response.choices[0].message
20        messages.append(message)
21
22        # Vérifier les appels d'outils
23        if message.tool_calls:
24            for tool_call in message.tool_calls:
25                args = json.loads(tool_call.function.arguments)
26                result = execute_safari_tool(**args)
27
28                messages.append({
29                    "role": "tool",
30                    "tool_call_id": tool_call.id,
31                    "content": result
32                })
33        else:
34            # Pas d'appel d'outil = réponse finale
35            return message.content
36
37    return "Limite d'itérations dépassée"
38
39# Utilisation
40answer = react_with_functions("Quelle est la météo au Serengeti ?")
41print(answer)

La réponse du modèle contenant tool_calls doit être ajoutée à messages avant les résultats, et chaque résultat est relié à son appel par tool_call_id. tool_choice="auto" laisse le modèle décider s'il a besoin d'un outil. La pensée n'est pas affichée ici comme Thought, mais le cycle action et observation reste le même. Pour les nouveaux projets, je te recommande justement cette version, car elle résiste le mieux aux erreurs de format.

ReAct avancé avec mémoire

Un agent peut aussi évaluer ses propres progrès. D'abord une structure pour une étape : @dataclass génère le constructeur, et field(default_factory=datetime.now) insère l'heure de création :

1from dataclasses import dataclass, field
2from typing import Optional
3from datetime import datetime
4
5@dataclass
6class ThoughtStep:
7    """Une étape de raisonnement de l'agent."""
8    thought: str
9    action: Optional[str] = None
10    observation: Optional[str] = None
11    timestamp: datetime = field(default_factory=datetime.now)

Optional[str] signifie que l'action et l'observation peuvent être vides, par exemple pour la pensée finale.

Un agent avec réflexion demande au modèle, toutes les quelques étapes, d'évaluer le chemin parcouru :

1class AdvancedReActAgent:
2    """Agent ReAct avancé avec mémoire et réflexion."""
3
4    def __init__(self, tools: dict, model: str = "gpt-5-mini"):
5        self.tools = tools
6        self.model = model
7        self.client = OpenAI()
8        self.thought_history: list[ThoughtStep] = []
9        self.long_term_memory: list[str] = []
10
11    def reflect(self) -> str:
12        """Réfléchit sur les étapes réalisées jusqu'à présent."""
13        if len(self.thought_history) < 2:
14            return ""
15
16        steps_summary = "\n".join([
17            f"- Thought: {s.thought}, Action: {s.action}, Result: {s.observation}"
18            for s in self.thought_history[-3:]
19        ])
20
21        response = self.client.chat.completions.create(
22            model=self.model,
23            messages=[
24                {
25                    "role": "system",
26                    "content": "Analyse les étapes réalisées jusqu'à présent et suggère des améliorations."
27                },
28                {
29                    "role": "user",
30                    "content": f"Étapes jusqu'à présent :\n{steps_summary}"
31                }
32            ],
33            max_completion_tokens=1000
34        )
35
36        return response.choices[0].message.content
37
38    def should_reflect(self) -> bool:
39        """Décide si l'agent doit réfléchir."""
40        # Réfléchir toutes les 3 étapes ou quand la dernière action a échoué
41        if len(self.thought_history) % 3 == 0 and len(self.thought_history) > 0:
42            return True
43        if self.thought_history and "error" in (self.thought_history[-1].observation or "").lower():
44            return True
45        return False
46
47    def run(self, query: str) -> str:
48        """Exécute l'agent avec réflexion."""
49        # ... implémentation similaire à la version de base
50        # mais avec la réflexion ajoutée
51
52        for i in range(10):
53            if self.should_reflect():
54                reflection = self.reflect()
55                print(f"Réflexion : {reflection}")
56
57            # ... reste de la logique ReAct
58
59        return "Réponse"

La méthode run est un squelette à compléter dans l'exercice. Dans reflect, j'ai utilisé max_completion_tokens : les modèles de raisonnement n'acceptent pas l'ancien max_tokens, et la nouvelle limite couvre aussi les tokens de raisonnement cachés, alors ne la fixe pas trop bas.

Patrons ReAct

La boucle de base peut être enrichie de plusieurs façons. La docstring du haut est une carte des variantes, et la classe en dessous esquisse la variante avec planification :

1"""
2Patrons populaires utilisés dans les agents ReAct :
3
41. ReAct de BASE
5   Thought -> Action -> Observation -> Repeat
6
72. ReAct avec Réflexion
8   Thought -> Action -> Observation -> Reflect -> Repeat
9
103. ReAct avec Planification
11   Plan -> Thought -> Action -> Observation -> Replan -> Repeat
12
134. ReAct avec Auto-cohérence
14   Plusieurs traces ReAct -> Vote sur la meilleure réponse
15
165. ReAct avec Recherche (RAG-ReAct)
17   Thought -> Retrieve -> Augment -> Action -> Observation
18"""
19
20# Exemple : ReAct avec Planification
21class PlanningReActAgent:
22    def __init__(self, tools: dict):
23        self.tools = tools
24        self.plan: list[str] = []
25        self.current_step = 0
26
27    def create_plan(self, query: str) -> list[str]:
28        """Crée un plan d'action avant de commencer."""
29        # Le LLM crée un plan étape par étape
30        response = client.chat.completions.create(
31            model="gpt-5-mini",
32            messages=[
33                {
34                    "role": "system",
35                    "content": "Crée un plan étape par étape pour résoudre la tâche. Chaque étape sur une nouvelle ligne."
36                },
37                {"role": "user", "content": query}
38            ]
39        )
40        plan = response.choices[0].message.content.split("\n")
41        return [step.strip() for step in plan if step.strip()]
42
43    def replan(self, remaining_steps: list[str], new_info: str) -> list[str]:
44        """Met à jour le plan en fonction des nouvelles informations."""
45        # Le LLM peut modifier les étapes restantes
46        return remaining_steps  # ou des étapes modifiées

L'auto-cohérence (self-consistency) lance plusieurs parcours indépendants et retient la réponse désignée par la majorité. Mon conseil : commence par le ReAct de base et n'ajoute des variantes que lorsque tu vois un problème concret.

ReAct est un patron puissant pour les agents d'IA. Il combine le raisonnement et l'action, ce qui mène à des réponses plus réfléchies et plus précises. Dans le prochain module, tu découvriras des techniques encore plus avancées : le RAG et les systèmes multi-agents !

Retiens : un bon agent est comme un bon pisteur - il réfléchit d'abord, vérifie ensuite les traces, et ne répond qu'après les avoir vues de ses propres yeux.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Qu'est-ce que le patron ReAct dans le contexte des agents d'IA ?

Tâches pratiques dans le jeu

  • Tri vertical

    Range les étapes de la boucle ReAct dans le bon ordre :

  • Tri vertical

    Range les étapes de la boucle ReAct dans le bon ordre :

  • Éditeur de code

    Crée une classe ReActAgent avec les méthodes run() et execute_action().

  • Clique dans l'ordre

    Clique sur les boutons pour créer un agent ReAct avec LangChain :

  • Tri vertical

    Range les étapes de ReAct avec réflexion :

  • Éditeur de code

    Implémente un agent avec les outils : get_weather, search_animals, calculate_distance.

Articles utiles