Accueil / Articles

ReAct dans LangGraph : de une boucle construite manuellement à ToolNode et StateGraph

Créez un agent ReAct deux fois : d’abord sous forme de boucle asynchrone manuelle, puis à l’aide de ToolNode, tools_condition et StateGraph de LangGraph pour gérer automatiquement les appels d’outils en chaîne.

Éditeur : REACTAPP.TOP6 min de lecture1391 mots

Sources: Agentic AI with LangChain — Part 4: ReAct Pattern in LangGraph

Un cycle de base pour l’appel d’outils effectue une seule passe : le modèle demande des outils, les résultats reviennent, puis une réponse est envoyée. Les tâches nécessitant des appels en chaîne, où le deuxième appel dépend de ce que le premier a retourné, exigent un agent capable de réfléchir, d’agir, d’examiner le résultat et d’agir à nouveau. Ce cycle constitue le pattern ReAct. Ci-dessous, vous trouverez une explication de ce concept, un cycle ReAct écrit à la main ainsi que le même agent reconstruit avec LangGraph, incluant le nœud d’outil, les messages d’outil, un bord conditionnel et un graphe d’état.

Les éléments fournis par LangGraph à cette fin :

  • Nœud d’outil : analyse les appels aux outils, exécute ces derniers et retourne les résultats.
  • Message d’outil : transmet la sortie d’un outil au LLM.
  • tools_condition : une fonction de routage qui vérifie les appels aux outils et active le bord conditionnel.
  • StateGraph : gère l’exécution du flux de travail.
  • État : un dictionnaire texte contenant les données en évolution de l’agent.
  • Nœuds : des fonctions Python qui mettent à jour l’état, un nœud d’outil en étant un exemple.
  • Arêtes : des transitions entre les nœuds, les arêtes conditionnelles permettant des branches et des boucles.
  • Mise en place

    Gardez les secrets dans un fichier .env situé à la racine du projet :

    OPENAI_API_KEY="<Your OpenAI API Key>"
    TAVILY_API_KEY=<Your TAVILY API Key>
    

    Chargez-les à l’aide d’une classe BaseSettings de Pydantic :

    class BaseConfig(BaseSettings):
        OPENAI_API_KEY: Optional[str]
        PINECONE_API_KEY: Optional[str]
        TAVILY_API_KEY: Optional[str]
    
    model_config = SettingsConfigDict(env_file=".env", extra="ignore")
    

    La structure du projet :

    project/
    ├── react_loop/
    │   └── weather_assistant.py # implement a vanilla react loop
    │
    ├── react_langgraph     # implement ReAct by LangGraph workflow
    │   ├── nodes.py        # graph nodes. Defined as Python functions
    │   ├── graph.py        # graph workflow
    │   ├── consts.py       # define node names as constants
    │   └── state.py        # typed dictionary to store graph states
    |
    ├── tools
    │   ├── tool_call.py  # implement simple tool-calling loop
    |   └── weather.py    # get_weather LangChain tool using open source APIs
    |
    |──    # implement tool-calling loop using LangChain tool
    ├── config.py         # pydantic BaseConfig
    ├── utils.py          # utility method to print messages
    ├── .env              # environment variable definition
    ├── .gitignore
    └── requirements.txt  # package requirements
    

    Que signifie ReAct

    ReAct est l’abréviation de Reasoning plus Acting. Un agent qui travaille sur une tâche en plusieurs étapes raisonne, agit, observe puis raisonne à nouveau jusqu’à ce que la tâche soit terminée. Le cycle de base ne permet qu’un seul exécution d’outil, ce qui constitue la lacune que ReAct comble.

    Une exécution comprend six étapes :

    1. L’utilisateur envoie un message.
    2. Le LLM lit la demande, détermine les informations nécessaires et décide s’il faut appeler des outils, et quels outils.
    3. L’agent exécute un ou plusieurs outils, tels que rechercher sur le web, interroger une API ou envoyer un e-mail.
    4. Les résultats reviennent et s’ajoutent à l’état de travail de l’agent.
    5. Le LLM évalue les nouvelles informations et répond soit directement, soit demande davantage d’appels d’outils.
    6. Lorsqu’aucune action supplémentaire n’est nécessaire, la réponse finale est envoyée.

    La boucle est puissante car l’agent peut réviser son plan en fonction de ce qu’il vient d’apprendre, de sorte que chaque décision peut dépendre des résultats précédents.

    Une boucle ReAct simple

    Ce version se trouve dans react_loop/weather_assistant.py et ressemble à la boucle à un seul passage, avec deux différences :

    async def chat(self, message: str):
        # Add user message
        self.messages.append(HumanMessage(content=message))
    
        # loop to handle serial tool calls
        while True:
    
            # Get AI response with potential tool calls
            response = await self.llm_with_tools.ainvoke(self.messages)
            self.messages.append(response)
    
            # Check if there is any tools to call
            if not response.tool_calls:
                # no tools to call
                break
                # Execute each tool call
    
            # process tool calls asynchronously
            results = await asyncio.gather(*[self.tools[tc["name"]].ainvoke(tc) for tc in response.tool_calls])
            self.messages.extend(results)
    

    Tout d’abord, les appels aux outils s’exécutent de manière asynchrone à l’aide de asyncio.gather, ce qui permet à plusieurs d’être exécutés en parallèle. Ensuite, l’étape d’exécution de l’outil se trouve à l’intérieur d’une boucle while, qui répète le cycle « raison → action » jusqu’à ce que la réponse du modèle ne contienne plus d’appels aux outils.

    Le même agent dans LangGraph

    La boucle manuelle fonctionne, mais elle oblige à gérer soi-même le cycle raison-action, la liste des messages, le dictionnaire des outils et la collecte des résultats. LangGraph prend en charge chacun de ces éléments : ToolNode analyse les appels et exécute les outils, StateGraph conserve les messages dans l’état du graphe, et le bord tools_condition remplace la boucle while écrite manuellement ainsi que sa vérification des appels aux outils.

    État. Un dictionnaire typé contient la liste des messages (react_langgraph/state.py) :

    from typing import TypedDict, Annotated
    from langgraph.graph.message import AnyMessage, add_messages
    
    class ReactState(TypedDict):
        """
        Define the state schema for our hotel assistant.
    
        Attributes:
            messages: list of messages
        """
    
        messages: Annotated[list[AnyMessage], add_messages]
    

    Les nœuds lisent et mettent à jour les messages par son intermédiaire, et le réducteur add_messages ajoute tout ce que renvoie un nœud.

    Nœuds. Dans react_langgraph/nodes.py, la configuration crée une liste d’outils LangChain, un prompt système qui indique au modèle comment les utiliser, ainsi qu’un modèle OpenAI associé à cette liste d’outils :

    tools = [TavilySearch(max_results=3, tavily_api_key=TAVILY_API_KEY), get_weather]
    
    SYSTEM_PROMPT = f"""You are an assistant for question-answering tasks with access to multiple tools.
        use these tools to retrieve context to answer the question. If you don't know the answer,
        just say that you don't know. Use three sentences maximum and keep the answer concise.
    
        Tool Use Instructions:
    
        - Use tools thoughtfully—only when they add value to the response
        - Chain tools when needed (e.g., search for info, then use that to search
          for relevant hotels)
          - Be conversational and helpful
        """
    
    system_message = SystemMessage(content=SYSTEM_PROMPT)
    
    llm = ChatOpenAI(api_key=api_key, model="gpt-4o-mini", temperature=0)
    llm_with_tools = llm.bind_tools(tools)
    
    
    async def run_agent_reasoning(state: ReactState) -> dict:
        """
        run agent reasoning node
        :param state: ReactState, manage graph state
        :return: dict containing new message
        """
    
        print("----AGENT REASONING------")
        messages = [system_message] + state["messages"]
    
        if isinstance(messages[-1], ToolMessage):
            print_message(messages[-1])
    
        response = await llm_with_tools.ainvoke(messages)
        print_message(response)
    
        return {"messages": [response]}
    
    
    tool_node = ToolNode(tools)
    

    Deux nœuds en résultent. Le nœud de raisonnement crée la liste de messages, appelle le LLM et renvoie sa réponse. Le nœud d’action est un ToolNode qui analyse les appels aux outils, exécute ces outils et renvoie leurs messages.

    Graphique. Dans react_langgraph/graph.py, un StateGraph enregistre les nœuds et les relie par des arêtes :

    workflow = StateGraph(ReactState)
    
    workflow.add_node(AGENT_REASON, run_agent_reasoning)
    workflow.add_node(ACT, tool_node)
    
    workflow.add_edge(START, AGENT_REASON)
    workflow.add_edge(ACT, AGENT_REASON)
    
    workflow.add_conditional_edges(
        AGENT_REASON,
        tools_condition,
        {"tools": ACT, END: END}
    )tion is invoked first
    

    L’exécution suit quatre règles. Le graphe commence au nœud de raisonnement. Si la réponse du LLM contient des appels d’outils, tools_condition redirige vers le nœud d’action. Une fois les outils exécutés, le contrôle revient toujours au raisonnement. Si la réponse ne contient pas d’appels d’outils, le graphe se termine et la réponse finale est renvoyée. Cela correspond au cycle ReAct sans boucle manuelle ni messages gérés manuellement.

    Compiler et exécuter. La compilation produit l’agent, et un diagramme du graphe peut être exporté sous forme de PNG :

    react_agent = workflow.compile()
    
    react_agent.get_graph().draw_mermaid_png(output_file_path="graphs.png")
    
    question = """
               tell me the first hotel you find in Boston if
                its temperature is lower than 30 degree."""
    
    
    async def main():
        print("hello ReAct agent by Langraph")
        message = HumanMessage(content=question)
        res = await react_agent.ainvoke({"messages": [message]})
    
    
    if __name__ == "__main__":
        asyncio.run(main())
    

    Appelons-le avec un message utilisateur, et la liste de messages résultante affiche les tours alternés du nœud de raisonnement et du nœud d’outil, ce qui constitue un flux multi-étapes fonctionnel.

    Où cela nous mène

    Trois fonctionnalités de LangGraph assurent ce travail : ToolNode pour trouver et exécuter des outils, tools_condition pour le contrôle des boucles et add_messages pour la gestion automatique des messages. Une fois ReAct compris et implémenté, la prochaine étape consiste à revenir à un pipeline RAG de LangGraph pour en faire un système entièrement agent.