Accueil / Articles

Un layout standard en sept sections pour chaque agent LangGraph.

Une structure de fichiers fixe pour les agents LangGraph, allant des importations et de l’état aux outils, nœuds, arêtes, assemblage et invocation, accompagnée de modèles et d’une extension multi-agents.

Éditeur : REACTAPP.TOP19 min de lecture4469 mots

Sources: The LangGraph Mental Model: A standardized architecture guide for every agent you’ll ever build

Tout ceux qui ont étudié LangGraph depuis un certain temps connaissent ce sentiment étrange : l’idée semble logique, mais le code refuse de venir. Un graphe contient des nœuds, les nœuds effectuent des tâches, les arêtes déterminent où aller ensuite, et l’état conserve la mémoire. On pourrait le schématiser sur un tableau blanc en cinq minutes. Puis on ouvre un fichier vide et rien ne vient. Les importations vous échappent, la classe dont vous avez besoin n’a pas de place évidente, et il semble y avoir cinq façons différentes d’écrire la même chose.

Pourquoi le code semble plus difficile que le concept

La raison fondamentale est que LangGraph n’impose aucune structure propre. Il vous fournit des éléments de construction fonctionnels et vous laisse décider de leur disposition. Chaque tutoriel et chaque répertoire organise les choses différemment, si bien que votre mémoire ne s’arrête jamais sur un seul schéma de référence.

La solution proposée ici consiste à choisir un layout standard et à l’utiliser pour chaque agent, qu’il s’agisse d’un petit chatbot ou d’un pipeline comprenant de nombreux agents collaborant entre eux. Pensez-y comme à un modèle que vous pouvez mémoriser et rappeler sans effort, un peu comme les diagrammes qui montrent ce qu’un agent a déjà fait, sauf que celui-ci vise le code source plutôt que l’historique en temps de exécution.

Le fichier dans son ensemble en un coup d’œil

Chaque application LangGraph peut être écrite sous forme d’un seul fichier contenant sept sections dans un ordre fixe, de haut en bas :

langgraph_agent.py
│
├── MODULE 1: IMPORTS & CONFIGURATION
│   └── All your libraries, API keys, model setup
│
├── MODULE 2: STATE
│   └── The TypedDict that defines your agent's memory
│
├── MODULE 3: TOOLS (optional, but common)
│   └── Functions decorated with @tool that the LLM can call
│
├── MODULE 4: NODES
│   └── Functions that do the actual work at each graph step
│
├── MODULE 5: EDGES & ROUTING
│   └── Functions that decide what happens next
│
├── MODULE 6: GRAPH ASSEMBLY
│   └── Where you build, wire, and compile the graph
│
└── MODULE 7: ENTRYPOINT
    └── The __main__ block or invoke() call that runs everything

C’est l’ossature. Lorsque vous commencez un projet, ouvrez un fichier et saisissez les sept titres de section en tant que commentaires, puis remplissez-les un par un. La suite de ce texte aborde chaque section, en expliquant à quoi elle sert, quels noms il faut retenir, ainsi qu’un modèle à copier.

Section 1 : importations et configuration

À quoi elle sert

Considérez cette section comme un outil de travail. Un menuisier dispose tous ses outils avant de construire quoi que ce soit ; ici, vous indiquez à Python quelles capacités externes vous avez besoin et vous configurez le modèle que l’agent utilisera.

Noms à connaître

  • ChatOpenAI ou ChatAnthropic est la classe du modèle de langage, c’est-à-dire le « cerveau » de l’agent. Créez-la une fois ici et réutilisez-la partout.
  • StateGraph est la classe principale qui construit le graphe, un peu comme le plan sur lequel vous dessinez.
  • END est une constante spéciale représentant le point terminal. Un bord qui y fait référence met fin à l’exécution.
  • MemorySaver est un point de contrôle qui conserve la mémoire au cours de plusieurs échanges.

Modèle

# --- Standard Library ---
import os
from typing import TypedDict, Annotated, Literal
# --- LangChain Core ---
from langchain_openai import ChatOpenAI          # or ChatAnthropic, ChatGroq, etc.
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
# --- LangGraph Core ---
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages  # The message reducer
from langgraph.prebuilt import ToolNode            # Pre-built node for tool execution
from langgraph.checkpoint.memory import MemorySaver
# --- Configuration ---
# Always name your model variable 'llm' - it's the standard in every node
llm = ChatOpenAI(
    model="gpt-4o",         # or "claude-3-5-sonnet-20241022", etc.
    temperature=0,          # 0 = deterministic; raise for creativity
    api_key=os.environ.get("OPENAI_API_KEY")
)

Pourquoi c’est organisé de cette manière

Les importations sont divisées en quatre groupes : la bibliothèque standard, LangChain (la couche de modèles), LangGraph (la couche de graphes) et les paramètres de configuration. Cela suit la structure en couches des bibliothèques elles-mêmes, où LangChain fournit des primitives d’IA et LangGraph gère l’orchestration. En séparant les groupes, il devient facile de voir ce qui appartient où en cas de problème.

La meilleure pratique ici est d’appeler le modèle llm. Chaque fonction de nœud fera référence à cette variable par la suite, et si un projet utilise model tandis qu’un autre utilise chat, le code perd son uniformité et devient plus difficile à parcourir rapidement.

Section 2 : état

À quoi sert-il

State représente la mémoire de travail de l’agent, une source unique de vérité qui circule à travers tout le graphe. Chaque nœud peut y lire et y écrire, tandis que le moteur gère son cycle de vie. Parmi tous les éléments de LangGraph, bien concevoir State est probablement le plus important.

Un tableau blanc de bureau en est une bonne illustration : quiconque entre dans la pièce, c’est-à-dire chaque nœud, peut lire ce qui y est écrit et ajouter quelque chose, et le tableau reste en place pendant toute la réunion, ce qui correspond à l’exécution complète du graphe.

Noms à connaître

  • TypedDict est une classe Python qui décrit la structure de l’état, un peu comme un schéma. Votre classe State en hérite.
  • Annotated est un enveloppe pour les indications de type. LangGraph l’utilise pour associer un réducteur à un champ, c’est-à-dire une fonction qui détermine comment une nouvelle valeur se combine avec celle qui existe déjà.
  • add_messages est le réducteur intégré pour le champ messages. Au lieu de remplacer la liste chaque fois qu’un nœud ajoute un message, il ajoute à la fin. C’est le réducteur que vous utiliserez le plus souvent.
  • operator.add est un autre réducteur courant, destiné aux champs qui doivent s’accumuler par addition, tels qu’une liste de résultats ou un compteur.
  • Modèle

    # ============================================================
    # MODULE 2: STATE
    # ============================================================
    class AgentState(TypedDict):
        # 'messages' is the heartbeat of almost every LangGraph agent.
        # Annotated[list, add_messages] means: "this is a list, and when
        # a node writes to it, append - don't replace."
        messages: Annotated[list[BaseMessage], add_messages]
    
        # Add custom fields below for your specific agent's needs.
        # Fields without a reducer are REPLACED each time a node writes to them.
    
        # Example: a simple string field (gets replaced each write)
        current_task: str
    
        # Example: a list you want to accumulate (use operator.add as reducer)
        # results: Annotated[list[str], operator.add]
    
        # Example: a counter
        # iteration_count: int
    

    En réfléchissant aux réducteurs

    Les réducteurs déroutent de nombreux débutants, c’est pourquoi voici une analogie. Imaginez l’état comme un formulaire papier qui circule dans un bureau. Le champ messages est un bloc de notes autocollantes : chacun qui traite le formulaire y ajoute une note, et personne ne supprime celles qui ont été écrites précédemment. C’est ce que fait add_messages.

    Un champ sans réducteur correspond à une seule zone de texte. Chaque personne efface ce que la précédente a écrit, ce qui correspond à la configuration par défaut de LangGraph : la dernière modification l’emporte.

    Une règle simple s’applique : attribuez toujours au champ messages la valeur Annotated[list, add_messages], choisissez Annotated[list, operator.add] pour chaque liste supplémentaire qui doit continuer à s’agrandir, et laissez tous les autres champs sans modification.

    Section 3 : outils

    À quoi ça sert

    Les outils sont les mains de l’agent. Le LLM est le cerveau, qui raisonne et prend des décisions, mais il ne peut pas agir seul dans le monde. Avec des outils, il peut naviguer sur le web, appeler une API, exécuter du code, rechercher des informations dans une base de données ou lancer n’importe quelle fonction Python que vous définissez.

    Cette section est optionnelle. Un agent qui ne produit que du texte et ne touche jamais à des systèmes externes peut la sauter, bien que la plupart des agents réels aient besoin d’outils.

    Noms à connaître

    • @tool est un décorateur de LangChain qui transforme une fonction Python ordinaire en un outil compris par LangGraph. La documentation de la fonction devient la description que le LLM lit pour décider s’il doit l’utiliser.
    • ToolNode est un nœud prêt à l’emploi, disponible dans langgraph.prebuilt, qui exécute les outils choisis par le LLM, vous évitant ainsi d’écrire vous-même la logique d’exécution.
  • bind_tools est une méthode de votre instance LLM qui y associe des outils. Par la suite, le modèle sait ce qui existe et peut demander une exécution en émettant un message spécial tool_calls au lieu de texte brut.
  • Modèle

    # ============================================================
    # MODULE 3: TOOLS
    # ============================================================
    @tool
    def search_web(query: str) -> str:
        """Search the web for current information about a topic.
    
        Use this when you need real-time information that is not in
        your training data, such as recent news or live prices.
    
        Args:
            query: The search query string.
    
        Returns:
            A string containing search results.
        """
        # Your actual implementation here (e.g., Tavily, SerpAPI, etc.)
        # Placeholder for illustration:
        return f"Search results for: {query}"
    
    @tool
    def calculate(expression: str) -> str:
        """Evaluate a mathematical expression and return the result.
    
        Use this for any arithmetic, algebra, or numerical computation.
    
        Args:
            expression: A valid Python math expression as a string, e.g. '2 + 2 * 10'
    
        Returns:
            The computed result as a string.
        """
        try:
            return str(eval(expression))
        except Exception as e:
            return f"Error: {e}"
    
    # Collect all tools into a list - this is the pattern you always follow
    tools = [search_web, calculate]
    # Bind tools to the LLM so it knows they exist and can choose to call them
    llm_with_tools = llm.bind_tools(tools)
    # Create the pre-built ToolNode that will execute tool calls automatically
    tool_node = ToolNode(tools)
    

    La documentation importe plus qu’il n’y paraît

    Les débutants manquent souvent ce point : le modèle s’appuie sur votre documentation pour choisir un outil. Il lit le texte entre guillemets triples. Si la documentation est vague ou absente, le LLM ne saura pas quand votre outil est le bon, alors écrivez des descriptions claires et précises qui indiquent ce que fait l’outil et quand l’utiliser.

    Section 4 : nœuds

    À quoi servent-ils

    Les nœuds sont les exécutants. Chacun est une fonction Python qui reçoit l’État actuel, effectue une action telle que l’appel à un LLM, le traitement de données ou l’invoque d’un outil, puis renvoie un dictionnaire des mises à jour de l’État. Telle est leur description de tâche complète.

    Un nœud ne décide jamais de ce qui se passe par la suite. Il effectue son travail et met à jour l’état, et le routage qui suit est géré dans la section 5.

    Noms à connaître

    • state est le paramètre que chaque nœud reçoit, un dictionnaire ayant la même structure que votre AgentState. On y trouve des valeurs telles que state["messages"] ou state["current_task"].
    • HumanMessage, AIMessage et SystemMessage sont les trois types de messages que vous utiliserez constamment. Le premier correspond à ce que dit l’utilisateur, le deuxième à la réponse du modèle, et le troisième contient vos instructions destinées au modèle, c’est-à-dire le prompt système.
    • invoke est la méthode permettant d’appeler le LLM, ou llm_with_tools, afin d’obtenir une réponse. Vous transmettez une liste de messages et vous recevez un AIMessage.
  • La valeur de retour est toujours un dictionnaire dont les clés correspondent aux noms de champs dans AgentState. Ses valeurs sont des mises à jour, et non l’état complet, et LangGraph les fusionne à l’aide des réducteurs que vous avez définis.
  • Modèle

    # ============================================================
    # MODULE 4: NODES
    # ============================================================
    # ── Node: Agent (the reasoning brain) ───────────────────────
    def agent_node(state: AgentState) -> dict:
        """The central reasoning node. Calls the LLM and decides
        whether to respond or call a tool."""
    
        # Build the message list to send to the LLM.
        # Always include a system message to set behavior.
        system_prompt = SystemMessage(content=(
            "You are a helpful assistant. Use the available tools "
            "when you need real-time information or computation. "
            "Respond clearly and concisely."
        ))
    
        # The LLM receives the system prompt + all previous messages in state
        messages_to_send = [system_prompt] + state["messages"]
    
        # Call the LLM. Use llm_with_tools if you have tools; plain llm if not.
        response = llm_with_tools.invoke(messages_to_send)
    
        # Return the LLM's response as a state update.
        # add_messages will APPEND this AIMessage to state["messages"].
        return {"messages": [response]}
    
    # ── Node: Summarizer (example of a non-LLM processing node) ─
    def summarize_node(state: AgentState) -> dict:
        """Summarizes the conversation so far to keep context short.
        This shows that nodes don't have to call an LLM - they can
        do any Python processing."""
    
        all_messages = state["messages"]
    
        # Summarize with the LLM (a different prompt, same LLM)
        summary_prompt = [
            SystemMessage(content="Summarize the following conversation in 2-3 sentences."),
            HumanMessage(content=str(all_messages))
        ]
    
        summary_response = llm.invoke(summary_prompt)
    
        # Replace messages with a fresh start containing just the summary
        return {
            "messages": [AIMessage(content=f"[Summary] {summary_response.content}")]
        }
    

    Une image pour les nœuds

    Imaginez un nœud comme un coureur de relais. Il prend le relais, qui représente l’état, effectue sa portion de course, c’est-à-dire qu’il accomplit le travail, puis transmet le relais avec des données mises à jour. Il ne choisit pas le prochain coureur ; c’est la tâche de celui qui dirige la course, c’est-à-dire des arêtes.

    Chaque signature de nœud a la même forme : def my_node(state: AgentState) -> dict. L’entrée est l’état complet, et la sortie est un dictionnaire ne contenant que les champs que vous souhaitez modifier.

    Section 5 : arêtes et routage

    À quoi ça sert

    Les arêtes constituent la logique de décision du graphe. Elles relient les nœuds entre eux et déterminent le flux d’exécution. LangGraph en possède deux types.

    Arêtes statiques : elles relient un nœud à un autre sans condition, par exemple « après le résumateur, aller vers l’agent ». Il n’y a pas de décision, juste une connexion directe.

    Arêtes conditionnelles : elles examinent l’état et retournent une chaîne de caractères indiquant le nœud suivant. C’est ici que réside l’intelligence de l’agent concernant son propre flux. Doit-il appeler une fonction, terminer ou revenir en arrière pour essayer à nouveau ?

    Noms à connaître

    • add_edge(A, B) crée une arête statique, ce qui signifie que le contrôle passe toujours de A à B.
    • add_conditional_edges(source, routing_function, mapping) crée une branche partant de source. La fonction de routage reçoit l’état actuel et retourne une chaîne de caractères, tandis que le dictionnaire mapping traduit cette chaîne en le nom réel du nœud suivant.
  • tools_condition est une fonction de routage prête à l’emploi provenant de langgraph.prebuilt. Elle examine le dernier message dans l’état afin de détecter d’éventuelles appels d’outils. S’il y en a, elle dirige le flux vers "tools" ; sinon, vers END. C’est le schéma standard pour les agents utilisant des outils et vaut la peine d’être mémorisé.
  • START est la constante représentant le début du graphe. Définir un point d’entrée signifie ajouter une arête allant de START vers le premier nœud.
  • Modèle

    # ============================================================
    # MODULE 5: EDGES & ROUTING
    # ============================================================
    # Import the pre-built tool routing function
    from langgraph.prebuilt import tools_condition
    # ── Custom Routing Function Example ─────────────────────────
    def should_continue(state: AgentState) -> Literal["tools", "summarize", "__end__"]:
        """Custom router for the agent node.
    
        Routing functions always:
        1. Receive the current state as input
        2. Return a string that maps to the next node (or END)
    
        The return values must match the keys in add_conditional_edges' mapping.
        """
    
        last_message = state["messages"][-1]  # Look at what the LLM just said
    
        # Case 1: The LLM decided to call a tool
        if hasattr(last_message, "tool_calls") and last_message.tool_calls:
            return "tools"
    
        # Case 2: The conversation is getting long - summarize before continuing
        if len(state["messages"]) > 20:
            return "summarize"
    
        # Case 3: The LLM gave a direct answer - we're done
        return "__end__"  # LangGraph's internal name for END
    

    Une image pour le routage

    Imaginez une fonction de routage comme un opérateur de signalisation ferroviaire. Un train arrive à une gare lorsque un nœud termine son traitement. L’opérateur vérifie le manifeste, qui représente l’état actuel, et actionne le bon interrupteur afin que le train emprunte le bon chemin. L’opérateur ne déplace jamais le train ; c’est LangGraph qui s’en charge.

    Règle à retenir : les fonctions de routage ne modifient jamais l’état. Elles ne font que le lire. Toute modification doit avoir lieu au niveau d’un nœud ; donc, si vous avez envie de mettre à jour l’état pendant le routage, déplacez cette logique dans un nœud.

    Section 6 : assemblage du graphe

    À quoi ça sert

    C’est ici que tout est assemblé. Considérez cette section comme la journée de construction. Les sections 1 à 5 correspondaient à la préparation, avec la définition des matériaux, le découpage des éléments et leur disposition ; maintenant, vous assemblez la structure en reliant les éléments et en compilant le tout.

    Le montage suit toujours les mêmes cinq étapes, qu’il vaut la peine de mémoriser : initialiser, enregistrer les nœuds, définir le point d’entrée, relier les arêtes, compiler.

    Noms à connaître

    • StateGraph(AgentState) crée l’outil de construction en utilisant votre classe State comme schéma, de sorte que chaque nœud doit retourner des mises à jour compatibles avec celui-ci.
    • add_node(name, function) enregistre un nœud. Le nom est la chaîne de caractères que vous utiliserez dans les arêtes, et la fonction correspond au nœud décrit à la section 4.
    • set_entry_point(name) indique quel nœud s’exécute en premier lorsque le graphe est invoqué.
    • add_edge(A, B) crée un lien direct et inconditionnel de A vers B.
    • add_conditional_edges(source, router, mapping) ajoute une branche conditionnelle à partir de la source, en utilisant votre fonction de routage ainsi qu’un mappage des chaînes de caractères retournées vers les noms de destination.
    • compile(checkpointer=...) scelle le graphe en un objet exécutable (CompiledGraph). Aucun nœud ni aucune arête ne peut être ajouté par la suite. Le paramètre optionnel checkpointer active la mémoire persistante.

    Modèle

    # ============================================================
    # MODULE 6: GRAPH ASSEMBLY
    # ============================================================
    # ── Step 1: Initialize ──────────────────────────────────────
    # Always pass your State class to StateGraph
    graph_builder = StateGraph(AgentState)
    # ── Step 2: Register All Nodes ──────────────────────────────
    # Format: add_node("node_name_as_string", node_function)
    # The string name is what you use in ALL edge definitions
    graph_builder.add_node("agent", agent_node)
    graph_builder.add_node("tools", tool_node)      # The pre-built ToolNode from Module 3
    graph_builder.add_node("summarize", summarize_node)
    # ── Step 3: Set Entry Point ─────────────────────────────────
    # Which node runs first when we invoke the graph?
    graph_builder.set_entry_point("agent")
    # ── Step 4: Wire the Edges ──────────────────────────────────
    # Conditional edge from agent: check if we need tools, a summary, or we're done
    graph_builder.add_conditional_edges(
        "agent",          # Source node
        should_continue,  # Routing function from Module 5
        {
            "tools": "tools",           # If router returns "tools" → go to tools node
            "summarize": "summarize",   # If router returns "summarize" → go to summarize node
            "__end__": END,             # If router returns "__end__" → stop the graph
        }
    )
    # Static edge: after tools run, always go back to agent (the ReAct loop)
    graph_builder.add_edge("tools", "agent")
    # Static edge: after summarization, always return to agent
    graph_builder.add_edge("summarize", "agent")
    # ── Step 5: Compile ─────────────────────────────────────────
    # Without checkpointer: no persistent memory (stateless per invocation)
    # With checkpointer: memory persists across turns (stateful conversations)
    memory = MemorySaver()
    graph = graph_builder.compile(checkpointer=memory)
    

    Une image pour l’assemblage

    La carte électronique constitue la meilleure comparaison. Chaque appel à add_node place un composant. add_edge et add_conditional_edges soudent les fils entre les composants. compile() effectue la vérification finale et finalise la carte. Après cela, elle est prête à être alimentée, ce qui se fait via invoke, afin d’accomplir sa fonction.

    Le modèle contient le cycle ReAct fondamental : agent → outils → agent → outils, et ainsi de suite, jusqu’à ce que l’agent décide qu’il a terminé et se dirige vers END. Ce cycle de raisonnement puis d’action sous-tend presque tous les agents LangGraph utilisant des outils, et en reconnaître le principe permet de comprendre la plupart des graphes que l’on rencontre.

    Section 7 : point d’entrée et invocation

    À quoi ça sert

    C’est la plateforme de lancement. Tout ce qui a précédé était la définition, et cette section correspond à l’exécution : le code qui fait fonctionner le graphe, fournit les entrées et collecte les sorties.

    Il existe deux façons de l’exécuter : invoke pour une seule itération et le streaming pour des sorties progressives. Vous avez également besoin de config, le dictionnaire contenant les paramètres de fonctionnement, en particulier thread_id utilisé pour la gestion de la mémoire.

    Noms à connaître

    • graph.invoke(input, config) exécute l’intégralité du graphe et restitue l’état final. Comme c’est synchrone, vous ne recevez rien tant que chaque étape n’est pas terminée.
    • graph.stream(input, config) exécute également le graphe, mais renvoie des mises à jour d’état partielles au fur et à mesure qu’elles se produisent, ce qui convient bien aux applications destinées aux utilisateurs qui doivent afficher un suivi en temps réel.
  • config est le dictionnaire transmis à l’une ou l’autre appel. Sa clé correspondante est {"configurable": {"thread_id": "some_id"}}. L’ID de thread fonctionne comme un ID de session, indiquant au checkpointer quelle mémoire restaurer et stocker. Deux utilisateurs ayant des IDs de thread différents ne partagent jamais de mémoire.
  • HumanMessage(content=...) est le moyen d’encadrer les entrées utilisateur avant de les transmettre au graphe.
  • Modèle

    # ============================================================
    # MODULE 7: ENTRYPOINT & INVOCATION
    # ============================================================
    if __name__ == "__main__":
    
        # ── Config: defines this conversation's memory session ──
        # Change thread_id to start a fresh conversation.
        # Keep the same thread_id to continue an existing one.
        config = {"configurable": {"thread_id": "user-session-001"}}
    
        # ── Single Invocation (synchronous) ─────────────────────
        user_input = "What is the current price of Bitcoin?"
    
        result = graph.invoke(
            input={"messages": [HumanMessage(content=user_input)]},
            config=config
        )
    
        # The result is the final state dictionary.
        # Access the last message to get the agent's final answer.
        final_answer = result["messages"][-1].content
        print(f"Agent: {final_answer}")
    
        # ── Streaming Invocation (for real-time output) ──────────
        for chunk in graph.stream(
            input={"messages": [HumanMessage(content=user_input)]},
            config=config,
            stream_mode="values"  # Yields the full state after each node runs
        ):
            # Each chunk is a state snapshot. The last message shows progress.
            latest = chunk["messages"][-1]
            if hasattr(latest, "content") and latest.content:
                print(f"[Streaming] {latest.content}")
    
        # ── Multi-turn Conversation Loop ─────────────────────────
        print("\n--- Starting Interactive Session ---")
        while True:
            user_text = input("You: ").strip()
            if user_text.lower() in ("exit", "quit", "bye"):
                break
    
            response = graph.invoke(
                input={"messages": [HumanMessage(content=user_text)]},
                config=config  # Same config = same memory thread
            )
    
            print(f"Agent: {response['messages'][-1].content}\n")
    

    Le modèle complet

    Ci-dessous se trouve le squelette minimal complet à copier dans un nouveau projet. C’est le seul fichier qu’il vaut la peine de marquer comme favori : remplacez les implémentations de remplacement par votre propre logique, et vous aurez la base de tout agent LangGraph.

    # ============================================================
    # LANGGRAPH CANONICAL AGENT TEMPLATE
    # Modules: Imports → State → Tools → Nodes → Edges → Assembly → Entrypoint
    # ============================================================
    # ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
    import os
    from typing import TypedDict, Annotated, Literal
    from langchain_openai import ChatOpenAI
    from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
    from langchain_core.tools import tool
    from langgraph.graph import StateGraph, END
    from langgraph.graph.message import add_messages
    from langgraph.prebuilt import ToolNode, tools_condition
    from langgraph.checkpoint.memory import MemorySaver
    llm = ChatOpenAI(model="gpt-4o", temperature=0)
    
    # ── MODULE 2: STATE ─────────────────────────────────────────
    class AgentState(TypedDict):
        messages: Annotated[list[BaseMessage], add_messages]
        # Add your custom fields here
    
    # ── MODULE 3: TOOLS ─────────────────────────────────────────
    @tool
    def my_tool(input: str) -> str:
        """Describe clearly what this tool does and when the LLM should use it."""
        return f"Result for: {input}"
    tools = [my_tool]
    llm_with_tools = llm.bind_tools(tools)
    tool_node = ToolNode(tools)
    
    # ── MODULE 4: NODES ─────────────────────────────────────────
    def agent_node(state: AgentState) -> dict:
        """The reasoning node. Calls the LLM, optionally triggers tool calls."""
        messages = [SystemMessage(content="You are a helpful assistant.")] + state["messages"]
        response = llm_with_tools.invoke(messages)
        return {"messages": [response]}
    
    # ── MODULE 5: EDGES & ROUTING ───────────────────────────────
    def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
        """Decide: did the LLM call a tool, or did it give a final answer?"""
        last_message = state["messages"][-1]
        if hasattr(last_message, "tool_calls") and last_message.tool_calls:
            return "tools"
        return "__end__"
    
    # ── MODULE 6: GRAPH ASSEMBLY ────────────────────────────────
    graph_builder = StateGraph(AgentState)
    graph_builder.add_node("agent", agent_node)
    graph_builder.add_node("tools", tool_node)
    graph_builder.set_entry_point("agent")
    graph_builder.add_conditional_edges(
        "agent",
        should_continue,
        {"tools": "tools", "__end__": END}
    )
    graph_builder.add_edge("tools", "agent")
    memory = MemorySaver()
    graph = graph_builder.compile(checkpointer=memory)
    
    # ── MODULE 7: ENTRYPOINT ────────────────────────────────────
    if __name__ == "__main__":
        config = {"configurable": {"thread_id": "session-001"}}
    
        while True:
            user_text = input("You: ").strip()
            if not user_text or user_text.lower() in ("exit", "quit"):
                break
            response = graph.invoke(
                {"messages": [HumanMessage(content=user_text)]},
                config=config
            )
            print(f"Agent: {response['messages'][-1].content}\n")
    

    Aller plus loin : les systèmes multi-agents

    Lorsque la conception à un seul agent semble naturelle, les systèmes multi-agents cessent d’être intimidants. L’idée centrale est que chaque agent n’est rien d’autre qu’un graphe compilé, et un graphe compilé peut lui-même être un nœud au sein d’un graphe superviseur plus grand.

    Noms à connaître pour le travail avec les multi-agents

    • Un nœud superviseur est un agent dont la tâche n’est pas de répondre à l’utilisateur, mais de choisir quel spécialiste doit s’occuper de la tâche. Il assure la redirection entre les sous-agents.
    • Un sous-graphe est un graphe compilé enregistré en tant que nœud dans un autre graphe. Il reçoit et renvoie des états comme toute fonction de nœud ordinaire.
  • Command est un modèle plus récent, disponible à partir de LangGraph 0.2, où un nœud renvoie à la fois une mise à jour d’état et une instruction de routage dans une seule valeur. Dans certains schémas multi-agents, il élimine la nécessité d’arêtes conditionnelles distinctes.
  • Modèle structurel

    # ── MULTI-AGENT PATTERN ─────────────────────────────────────
    # Each specialist is a compiled graph (a subgraph)
    
    # Sub-agent 1: A researcher
    researcher_graph = StateGraph(AgentState)
    # ... (built with its own nodes, edges, and tools)
    researcher = researcher_graph.compile()
    # Sub-agent 2: A writer
    writer_graph = StateGraph(AgentState)
    # ... (built with its own nodes, edges, and tools)
    writer = writer_graph.compile()
    
    # ── SUPERVISOR NODE ─────────────────────────────────────────
    def supervisor_node(state: AgentState) -> dict:
        """Decides which sub-agent should handle the current task."""
        # The supervisor LLM decides: "researcher" or "writer" or "FINISH"
        response = supervisor_llm.invoke(state["messages"])
        return {"messages": [response], "next_agent": response.content}
    
    def route_to_agent(state: AgentState) -> Literal["researcher", "writer", "__end__"]:
        """Routes to the appropriate sub-agent based on supervisor's decision."""
        return state.get("next_agent", "__end__")
    
    # ── SUPERVISOR GRAPH ────────────────────────────────────────
    supervisor_builder = StateGraph(AgentState)
    supervisor_builder.add_node("supervisor", supervisor_node)
    supervisor_builder.add_node("researcher", researcher)  # Subgraph as a node!
    supervisor_builder.add_node("writer", writer)          # Subgraph as a node!
    supervisor_builder.set_entry_point("supervisor")
    supervisor_builder.add_conditional_edges(
        "supervisor",
        route_to_agent,
        {"researcher": "researcher", "writer": "writer", "__end__": END}
    )
    supervisor_builder.add_edge("researcher", "supervisor")
    supervisor_builder.add_edge("writer", "supervisor")
    supervisor_graph = supervisor_builder.compile(checkpointer=MemorySaver())
    

    La version multi-agent possède la même structure que celle à un seul agent, et les mêmes sept sections s’appliquent. La seule différence réside dans le fait que certains des « nœuds » sont des graphes entièrement compilés. Le modèle mental s’adapte sans difficulté.

    Référence rapide

    Voici une fiche condensée que vous pouvez imprimer ou afficher, résumant les idées principales en un seul endroit.

    • État : TypedDict définit sa structure. Annotated[T, reducer] associe un réducteur à un champ. add_messages est le réducteur standard pour les listes de messages et ajoute des éléments plutôt que de les remplacer. operator.add accumule des listes ou des nombres.
    • LLM : ChatOpenAI ou ChatAnthropic représente la classe, toujours associée au nom llm. La fonction bind_tools(tools) informe le modèle des outils disponibles. La fonction invoke(messages) l’appelle et renvoie une réponse.
    • Messages : HumanMessage contient les entrées de l’utilisateur, AIMessage contient les sorties du modèle, et SystemMessage contient vos instructions. Ils sont toujours transmis à invoke sous forme de liste.
    • Outils : L’annotation @tool permet à une fonction Python d’être appelée par le modèle, la documentation de cette fonction servant à en décrire le fonctionnement. La fonction ToolNode(tools) exécute automatiquement les appels aux outils. La variable tools_condition vérifie si le modèle a demandé un outil.
    • Graphique : StateGraph(State) crée le constructeur ; add_node(name, fn) enregistre un nœud, set_entry_point(name) sélectionne le premier, add_edge(A, B) crée un lien inconditionnel, add_conditional_edges(source, router, mapping) crée un lien choisi en fonction de la valeur de retour du routeur, compile(checkpointer=...) finalise le processus, et END marque le point final.
    • Appel : graph.invoke(input, config) renvoie l’état final, graph.stream(input, config) fournit progressivement les mises à jour, config = {"configurable": {"thread_id": "..."}} identifie la session mémoire, et MemorySaver() est un point de contrôle en mémoire.

    En faire une habitude

    L’objectif n’est pas d’apprendre LangGraph à partir de rien. Il s’agit plutôt de fournir à votre cerveau un cadre stable que vous pouvez utiliser automatiquement, de la même manière qu’un musicien saisit une progression harmonique familière ou qu’un cuisinier dispose ses ingrédients avant de commencer.

    Sept sections — importations, état, outils, nœuds, arêtes, assemblage et point d’entrée — forment cette progression. Les noms présents dans chaque section représentent les notes, et le modèle complet constitue la partition musicale.

    Un chatbot de base ainsi qu’un pipeline comprenant des dizaines de nœuds s’adaptent tous deux à ce même schéma. La complexité augmente, mais la structure reste inchangée, et c’est cette stabilité qui en fait une norme.

    Une approche judicieuse consiste à commencer petit avec le cycle ReAct minimal : un nœud d’agent, un nœud d’outil et une arête conditionnelle utilisant tools_condition. Faites fonctionner cela en premier, puis ajoutez un autre nœud, puis un autre agent. Le schéma reste valide à chaque étape.

    Pour les backends de persistance, les modèles impliquant un intervenant humain et les options de déploiement, la documentation officielle de LangGraph est la prochaine ressource à consulter.