16. Composants préconstruits et routage multi-branches
Au chapitre 15, nous avons assemblé un graphe d'agent à la main — un nœud de modèle, un nœud d'outils et une arête conditionnelle qui décide s'il faut continuer à boucler ou s'arrêter. Il s'agit essentiellement de la structure standard pour les agents à appel d'outils. C'est pourquoi LangChain et LangGraph la fournissent sous forme de composants préconstruits (prebuilt components) que vous pouvez utiliser au lieu de réécrire à chaque fois le même échafaudage.
Dans la première moitié de ce chapitre, nous allons reconstruire l'agent du chapitre 15 à l'aide de composants préconstruits. Nous remplacerons le nœud d'exécution d'outils et la fonction de routage par ToolNode et tools_condition, et finalement nous remplacerons tout l'assemblage du graphe par un unique appel à create_agent. Vous verrez que le comportement reste identique à celui du chapitre 15, tandis que le code se réduit considérablement.
Dans la seconde moitié, nous combinerons les composants préconstruits avec l'approche d'assemblage manuel de graphe du chapitre 15 pour construire un agent plus complexe. L'agent que nous allons construire achemine chaque requête vers un gestionnaire différent — les consultations complexes vont vers un modèle haute performance, tandis que les questions simples sont traitées par un modèle moins coûteux et plus petit. C'est une structure multi-branches où le chemin de traitement diverge selon le type de requête.
16.1) Composants préconstruits et create_agent
Dans cette section, nous remplacerons la fonction tool_node et la fonction should_continue du graphe du chapitre 15 par les composants préconstruits ToolNode et tools_condition. Ensuite, nous éviterons complètement l'assemblage manuel et créerons tout le graphe avec un unique appel à create_agent. L'élément à surveiller à chaque étape est que le code raccourcit tandis que le comportement de l'agent reste identique à celui du chapitre 15.
16.1.1) ToolNode et tools_condition
ToolNode est un nœud préconstruit qui gère l'exécution des outils à votre place. Lorsque le dernier message de l'état (State) — l'AIMessage renvoyé par le LLM — contient des tool_calls, il exécute les outils demandés et ajoute les résultats à messages sous forme d'objets ToolMessage. Il fait le même travail que la fonction tool_node que nous avons écrite au chapitre 15. En plus de cela, lorsque le LLM demande plusieurs outils à la fois, il les exécute en parallèle.
Il prend également en charge la gestion des exceptions pendant l'exécution des outils. Si vous définissez ToolNode(tools, handle_tool_errors=True), le graphe ne plantera pas même lorsqu'un outil lève une exception. L'exception est convertie en un ToolMessage contenant les détails de l'erreur, qui est transmis au LLM afin qu'il puisse constater l'échec et réessayer avec des arguments corrigés.
Vous créez un ToolNode en lui passant une liste d'outils. Nous utiliserons les deux mêmes outils que ceux du chapitre 15.
from langgraph.prebuilt import ToolNode
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Obtient la météo actuelle pour une ville."""
fake_data = {"Tokyo": "18°C, nuageux", "Le Caire": "31°C, ensoleillé"}
return fake_data.get(city, f"Aucune donnée météo pour {city}.")
@tool
def calculate(expression: str) -> str:
"""Évalue une expression arithmétique simple. Exemple : '3 * 21'."""
return str(eval(expression)) # Attention : eval() est un risque de sécurité. À ne pas utiliser en production.
tools = [get_weather, calculate]
# La fonction tool_map + tool_node du chapitre 15 remplacée par cette seule ligne
tool_node = ToolNode(tools)ToolNode construit une correspondance interne nom-vers-outil à partir de la liste d'outils, tout comme le tool_map du chapitre 15. À l'exécution, il recherche chaque outil par le nom demandé par le LLM et l'appelle. Autrement dit, le dictionnaire tool_map, la boucle for sur les tool_calls et le code qui construit et collecte les objets ToolMessage — tout cela vit désormais à l'intérieur de ToolNode.
tools_condition est la fonction de routage préconstruite qui remplace la fonction should_continue du chapitre 15. Revoyons le should_continue du chapitre 15.
def should_continue(state: AgentState) -> Literal["tool_node", "__end__"]:
"""Décide s'il faut exécuter les outils ou terminer le graphe."""
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tool_node"
return ENDElle renvoyait "tool_node" (le nom enregistré de notre nœud d'outils) lorsque le dernier message avait des tool_calls, et END sinon. tools_condition fonctionne exactement de la même manière, avec une différence dans le nom qu'elle renvoie. Alors que should_continue était écrite pour renvoyer "tool_node" — le nom que nous avons enregistré dans notre graphe — tools_condition est codée en dur pour renvoyer "tools".
Comme nous l'avons appris à la section 15.2.4, la valeur qu'une fonction de routage renvoie est le nom du prochain nœud à exécuter. Si aucun nœud portant ce nom n'existe dans le graphe, le routage échoue. Ainsi, lorsque vous utilisez tools_condition, le nœud d'outils doit être enregistré sous le nom "tools".
from langgraph.prebuilt import ToolNode, tools_condition
builder.add_node("tools", ToolNode(tools)) # Enregistré avec le nom "tools"
builder.add_conditional_edges("llm_call", tools_condition) # Achemine vers "tools" ou ENDDe cette manière, lorsque tools_condition renvoie "tools", cela se connecte exactement au ToolNode que nous venons d'enregistrer.
Maintenant, réassemblons le graphe complet du chapitre 15, y compris toutes les pièces restantes. Les outils, l'état et le nœud llm_call sont inchangés par rapport au chapitre 15.
from langgraph.graph import StateGraph, MessagesState, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""Obtient la météo actuelle pour une ville."""
fake_data = {"Tokyo": "18°C, nuageux", "Le Caire": "31°C, ensoleillé"}
return fake_data.get(city, f"Aucune donnée météo pour {city}.")
@tool
def calculate(expression: str) -> str:
"""Évalue une expression arithmétique simple. Exemple : '3 * 21'."""
return str(eval(expression)) # Attention : eval() est un risque de sécurité. À ne pas utiliser en production.
tools = [get_weather, calculate]
class AgentState(MessagesState):
llm_calls: int
llm = ChatOpenAI(model="gpt-5-mini")
model_with_tools = llm.bind_tools(tools)
def llm_call(state: AgentState):
"""Appelle le LLM et renvoie sa réponse."""
response = model_with_tools.invoke(state["messages"])
return {
"messages": [response],
"llm_calls": state.get("llm_calls", 0) + 1,
}
builder = StateGraph(AgentState)
builder.add_node("llm_call", llm_call)
builder.add_node("tools", ToolNode(tools)) # ToolNode au lieu de la fonction tool_node du chapitre 15
builder.add_edge(START, "llm_call")
builder.add_conditional_edges("llm_call", tools_condition) # tools_condition au lieu du should_continue du chapitre 15
builder.add_edge("tools", "llm_call")
agent = builder.compile()Comparez ceci avec le code du chapitre 15. Le dictionnaire tool_map, la fonction tool_node et la fonction should_continue ont tous disparu. Le travail qu'ils effectuaient est désormais géré par ToolNode(tools) et tools_condition. Le nœud d'outils est enregistré sous le nom "tools" pour correspondre au nom vers lequel tools_condition achemine. Exécutons-le avec la même question qu'au chapitre 15.
result = agent.invoke({
"messages": [HumanMessage(content="Obtiens la température au Caire, puis multiplie ce nombre par 3.")],
"llm_calls": 0,
})
print(result["messages"][-1].content)
print(f"\nNombre total d'appels LLM : {result['llm_calls']}")Sortie :
Température actuelle au Caire : 31°C. Multipliée par 3 = 93.
Nombre total d'appels LLM : 3Le résultat est identique à celui du chapitre 15. L'agent recherche la météo, effectue le calcul et produit la réponse finale — le même comportement est préservé, tandis que le code que nous devons écrire et maintenir a rétréci.
Et si vous vouliez enregistrer le nœud d'outils sous un nom autre que "tools" ? Dans ce cas, passez un dictionnaire de correspondance comme troisième argument de add_conditional_edges, en spécifiant à quel nœud chaque valeur de retour de tools_condition doit se connecter. Puisque tools_condition renvoie soit "tools", soit END, vous les utilisez comme clés et les faites correspondre aux nœuds cibles. Par exemple, si vous enregistrez le nœud d'outils sous le nom "run_tools" :
builder.add_node("run_tools", ToolNode(tools))
builder.add_conditional_edges(
"llm_call",
tools_condition,
{"tools": "run_tools", END: END} # retour "tools" → nœud run_tools, retour END → terminer
)ToolNode et tools_condition remplacent des parties individuelles du graphe — les plus fastidieuses — mais l'ajout des nœuds et leur câblage nous incombent toujours. Pourrait-on déléguer cet assemblage également ? C'est exactement ce que fait create_agent.
16.1.2) create_agent
create_agent est une fonction factory de LangChain qui gère l'intégralité de l'assemblage du graphe pour un agent à appel d'outils. Passez-lui un modèle et une liste d'outils, et il construit un graphe ayant la même structure que celle que nous avons assemblée en 16.1.1, déjà compilé et prêt à s'exécuter. Essayons-le.
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_core.messages import HumanMessage
@tool
def get_weather(city: str) -> str:
"""Obtient la météo actuelle pour une ville."""
fake_data = {"Tokyo": "18°C, nuageux", "Le Caire": "31°C, ensoleillé"}
return fake_data.get(city, f"Aucune donnée météo pour {city}.")
@tool
def calculate(expression: str) -> str:
"""Évalue une expression arithmétique simple. Exemple : '3 * 21'."""
return str(eval(expression)) # Attention : eval() est un risque de sécurité. À ne pas utiliser en production.
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
system_prompt="Vous êtes un assistant utile.",
)
result = agent.invoke({
"messages": [HumanMessage(content="Obtiens la température au Caire, puis multiplie ce nombre par 3.")],
})
print(result["messages"][-1].content)Sortie :
Température actuelle au Caire : 31°C. Multipliée par 3 = 93.Aucune définition d'état, aucune fonction de nœud, aucun add_node ni add_edge. Un unique appel à create_agent a fait tout cela, et le résultat est identique à celui de 16.1.1.
Ce qui se passe en interne est exactement ce que nous connaissons déjà. create_agent crée un nœud d'appel LLM à partir du modèle que vous passez, construit un ToolNode à partir de la liste d'outils, et les connecte avec une arête tools_condition et une arête de retour en boucle. Le résultat est un graphe à structure de boucle identique à celui que nous avons assemblé en 16.1.1.
Examinons les paramètres. create_agent ne nous est en réalité pas nouveau — nous l'avons brièvement utilisé au chapitre 11 lors de la construction d'un RAG conversationnel, mais nous n'avons pas détaillé les paramètres. Passons-les en revue un par un.
model: le LLM que l'agent utilisera. L'approche la plus simple consiste à passer une chaîne de fournisseur comme"openai:gpt-5-mini". Si vous devez configurer directement les paramètres du modèle, passez une instance de modèle initialisée commeChatOpenAI(model="gpt-5-mini"). En interne, le nœud d'appel LLM utilise ce modèle.tools: la liste des outils que l'agent peut utiliser. UnToolNodeest construit à partir de ceux-ci en interne.system_prompt: les instructions comportementales pour l'agent. Elles sont ajoutées en tête de la liste de messages sous forme de message système à chaque appel LLM.checkpointer: sauvegarde l'état de la conversation afin que l'agent puisse se souvenir des tours précédents. C'est le même paramètre que nous avons utilisé avecInMemorySaver()etthread_idau chapitre 11 pour implémenter des conversations multi-tours. Nous verrons en détail son fonctionnement au chapitre 17.response_format: utilisez-le lorsque vous souhaitez que la réponse finale de l'agent soit une sortie structurée. Passez un modèle Pydantic (le même concept que celui du chapitre 7), et l'objet validé sera disponible dansresult["structured_response"].middleware: enregistre des fonctions à exécuter à des points spécifiques de la boucle d'exécution de l'agent. C'est le paramètre que nous avons utilisé pour enregistrertrim_old_messagesau chapitre 11. Nous l'expliquerons en détail ci-dessous.
Voyons comment response_format fonctionne en pratique.
from pydantic import BaseModel
from langchain.agents import create_agent
class WeatherReport(BaseModel):
city: str
temperature: str
condition: str
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
response_format=WeatherReport,
)
result = agent.invoke({
"messages": [HumanMessage(content="Quel temps fait-il à Tokyo ?")],
})
print(result["structured_response"])Sortie :
city='Tokyo' temperature='18°C' condition='nuageux'L'agent a appelé l'outil get_weather, puis a organisé les informations dans un objet WeatherReport correspondant au schéma.
middleware
La boucle de l'agent comporte des étapes distinctes. Elle appelle le LLM, exécute les outils, appelle à nouveau le LLM — ces étapes se répètent. Le middleware vous permet d'insérer vos propres fonctions avant ou après ces étapes. Vous spécifiez le moment avec un décorateur : @before_model signifie juste avant l'appel LLM, et @after_model signifie juste après la réponse du LLM. Enregistrez la fonction dans le paramètre middleware, et elle s'exécutera au point désigné à chaque fois.
Nous avons déjà utilisé le middleware au chapitre 11. Nous avons décoré une fonction trim_old_messages avec @before_model et l'avons enregistrée — puisqu'elle s'exécutait avant chaque appel LLM, elle pouvait élaguer la liste de messages à chaque fois.
Construisons un middleware qui affiche le nombre de messages juste avant chaque appel LLM, afin que nous puissions voir exactement quand il s'exécute.
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
@before_model
def log_llm_call(state: AgentState, runtime) -> None:
"""Affiche le nombre de messages juste avant chaque appel LLM."""
print(f"[before_model] Sur le point d'appeler le LLM, messages actuels : {len(state['messages'])}")
agent = create_agent(
model="openai:gpt-5-mini",
tools=[get_weather, calculate],
middleware=[log_llm_call],
)
result = agent.invoke({
"messages": [HumanMessage(content="Obtiens la température au Caire, puis multiplie ce nombre par 3.")],
})
print(result["messages"][-1].content)Sortie :
[before_model] Sur le point d'appeler le LLM, messages actuels : 1
[before_model] Sur le point d'appeler le LLM, messages actuels : 3
[before_model] Sur le point d'appeler le LLM, messages actuels : 5
Température actuelle au Caire : 31°C. Multipliée par 3 = 93.Le middleware log_llm_call s'est exécuté trois fois. Le LLM a été appelé trois fois pendant le traitement de la requête de l'utilisateur, et le middleware s'est exécuté juste avant chaque appel. Les nombres de messages nous renseignent sur l'état à chaque point : avant le premier appel, il n'y avait que la question de l'utilisateur (HumanMessage) — 1 message. Après chaque itération de la boucle, l'AIMessage demandant un appel d'outil et le ToolMessage contenant le résultat ont été ajoutés, augmentant à 3, puis à 5.
Une chose à savoir : avant LangChain 1.0, ce rôle était rempli par une fonction appelée
create_react_agentdu côté LangGraph, et elle est désormais dépréciée. Si vous voyezfrom langgraph.prebuilt import create_react_agentdans d'anciens tutoriels ou articles de blog, comprenez qu'il s'agit d'une version précédente ducreate_agentque vous apprenez maintenant.
Nous avons reconstruit l'agent du chapitre 15 de manière concise en utilisant ToolNode, tools_condition et create_agent. Dans la section suivante, nous combinerons ces composants préconstruits avec l'assemblage manuel de graphe pour construire un agent plus complexe.
16.2) Construire un agent multi-branches
L'agent multi-branches (multi-branch agent) que nous allons construire dans cette section détermine d'abord à quel type de requête il a affaire, puis traite chaque type avec un modèle différent ou un ensemble d'outils différent. Nous assemblerons le graphe global manuellement en suivant l'approche du chapitre 15, et utiliserons create_agent pour les parties qui nécessitent une boucle d'appel d'outils.
16.2.1) Exigences et conception
Nous allons construire l'agent de support client mentionné dans l'introduction de ce chapitre. Voici les exigences :
- Demandes simples (« Quels sont vos horaires d'ouverture ? ») → un petit modèle peu coûteux répond directement.
- Consultations complexes (« Ma commande est arrivée endommagée — devrais-je obtenir un échange ou un remboursement ? ») → un modèle haute performance répond.
- Recherches de commandes (« Quel est le statut de livraison de la commande n° 12345 ? ») → un agent doté d'un outil de recherche de commandes s'en charge.
Chaque type de demande nécessite une configuration de modèle et d'outils différente, nous avons donc besoin d'un graphe qui classe d'abord chaque requête, puis l'achemine vers le bon gestionnaire. Voici la structure :
Lorsqu'une requête arrive, le nœud classify détermine de quel type de demande il s'agit et enregistre le résultat dans l'état. Une arête conditionnelle lit ensuite le type enregistré depuis l'état et achemine vers le nœud approprié. simple_handler traite les demandes simples, et complex_handler gère les consultations complexes. order_agent utilise un outil de recherche de commandes pour vérifier le statut de livraison et répondre. Puisque order_agent est un nœud d'appel d'outils standard, nous le construirons avec create_agent. Maintenant, construisons chaque pièce dans l'ordre.
16.2.2) Le nœud de classification et la fonction de routage
D'abord, définissons l'état. Nous ajouterons un champ intent pour stocker le résultat de la classification. Les trois types seront représentés par les valeurs "simple", "complex" et "order".
from langgraph.graph import MessagesState
class State(MessagesState):
intent: str # Résultat de la classification : "simple", "complex", "order"Vient ensuite le nœud de classification. Il utilise un LLM pour déterminer à laquelle des trois catégories appartient la requête de l'utilisateur, et enregistre le résultat dans intent. Nous recevons le résultat de la classification à l'aide d'une sortie structurée, que nous avons apprise au chapitre 7. Lorsque nous déclarons le champ intent du schéma avec un type Literal, la réponse du LLM est contrainte à l'une des valeurs déclarées.
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
class IntentRoute(BaseModel):
"""Résultat de la classification d'une demande client."""
intent: Literal["simple", "complex", "order"] = Field(
description=(
"simple : questions générales comme les horaires d'ouverture ou les salutations. "
"complex : consultations qui nécessitent un raisonnement soigné, comme les litiges ou les remboursements. "
"order : requêtes visant à rechercher une commande spécifique."
)
)
classifier_llm = ChatOpenAI(model="gpt-5.4-nano").with_structured_output(IntentRoute)
def classify(state: State):
"""Classe le type de demande client."""
question = state["messages"][-1].content
result = classifier_llm.invoke(
f"Classe la requête du client.\n\nRequête : {question}"
)
return {"intent": result.intent}Nous avons utilisé le plus petit modèle (gpt-5.4-nano) pour la classification. Décider « de quel type est cette demande ? » est une tâche simple qui ne nécessite pas de modèle haute performance. Et puisque le nœud de classification est une porte d'entrée par laquelle passe chaque requête, un modèle peu coûteux et rapide est préférable.
Vient ensuite la fonction de routage. Elle renvoie simplement le résultat de la classification stocké dans l'état.
def route_by_intent(state: State) -> Literal["simple", "complex", "order"]:
"""Détermine le prochain nœud en fonction du résultat de la classification."""
return state["intent"]Le nœud classify a déjà décidé quel nœud doit s'exécuter ensuite et l'a enregistré dans intent, donc la fonction de routage renvoie simplement cette valeur telle quelle.
16.2.3) Nœuds gestionnaires par type
Construisons maintenant les gestionnaires pour chacun des trois types de demandes.
Le gestionnaire de demandes simples appelle un petit modèle une fois. Dans un véritable agent de support client, vous appliqueriez le RAG pour rechercher des réponses dans des documents internes, mais nous avons gardé le gestionnaire simple afin de rester concentrés sur le sujet de ce chapitre.
simple_llm = ChatOpenAI(model="gpt-5.4-mini")
def simple_handler(state: State):
"""Répond aux demandes simples avec un petit modèle."""
response = simple_llm.invoke(state["messages"])
return {"messages": [response]}Le gestionnaire de consultations complexes utilise un modèle haute performance. Pour la même raison que le gestionnaire de demandes simples, nous l'avons gardé simple — il se contente de générer une réponse.
complex_llm = ChatOpenAI(model="gpt-5.4")
def complex_handler(state: State):
"""Répond aux consultations complexes avec un modèle haute performance."""
response = complex_llm.invoke(state["messages"])
return {"messages": [response]}Le gestionnaire de recherche de commandes doit utiliser un outil de recherche de commandes, ce qui signifie qu'il nécessite une boucle d'appel d'outils. Puisque sa structure est identique à celle d'un agent à appel d'outils standard, nous le construirons avec create_agent.
from langchain.tools import tool
from langchain.agents import create_agent
@tool
def get_order_status(order_id: str) -> str:
"""Recherche le statut de livraison d'une commande par son numéro."""
fake_data = {"12345": "En transit, attendue demain", "67890": "Livrée"}
return fake_data.get(order_id, f"Commande {order_id} introuvable.")
order_agent = create_agent(
model="openai:gpt-5.4-mini",
tools=[get_order_status],
)16.2.4) Assembler et exécuter le graphe
Connectons tous les nœuds que nous avons construits en un graphe. Nous placerons classify au point de départ, le connecterons aux trois gestionnaires via une arête conditionnelle, et configurerons chaque gestionnaire pour qu'il se termine une fois son travail achevé.
from langgraph.graph import StateGraph, START, END
builder = StateGraph(State)
builder.add_node("classify", classify)
builder.add_node("simple", simple_handler)
builder.add_node("complex", complex_handler)
builder.add_node("order", order_agent) # Enregistre le graphe create_agent comme un nœud
builder.add_edge(START, "classify")
builder.add_conditional_edges("classify", route_by_intent)
builder.add_edge("simple", END)
builder.add_edge("complex", END)
builder.add_edge("order", END)
agent = builder.compile()Exécutons une demande de chaque type et voyons quel gestionnaire la traite.
from langchain_core.messages import HumanMessage
for question in [
"Quels sont vos horaires d'ouverture ?",
"Ma commande est arrivée endommagée. Devrais-je obtenir un échange ou un remboursement ?",
"Quel est le statut de livraison de la commande 12345 ?",
]:
result = agent.invoke({"messages": [HumanMessage(content=question)]})
print(f"Q : {question}")
print(f"[{result['intent']}] R : {result['messages'][-1].content}\n")Sortie :
Q : Quels sont vos horaires d'ouverture ?
[simple] R : Je n'ai pas d'horaires d'ouverture fixes — je suis disponible 24h/24 et 7j/7.
...
Q : Ma commande est arrivée endommagée. Devrais-je obtenir un échange ou un remboursement ?
[complex] R : Si votre commande est arrivée endommagée, vous devriez généralement avoir droit à un **remplacement/échange ou à un remboursement complet**.
...
Q : Quel est le statut de livraison de la commande 12345 ?
[order] R : La commande 12345 est **en transit** et est **attendue demain**.Chaque demande a été traitée par un chemin différent. simple_handler a répondu à la demande simple avec un petit modèle, complex_handler a répondu à la consultation complexe avec un modèle haute performance, et order_agent a appelé l'outil get_order_status pour répondre à la recherche de commande.