Un grand modèle de langage, seul, ne sait rien faire. Il prédit du texte. Il ne peut pas consulter votre base de commandes, appeler une API météo, ni même additionner deux grands nombres de façon fiable. Un agent, lui, le peut — non parce que le modèle est plus puissant, mais parce qu’on l’a placé dans une boucle qui lui donne des outils et exécute ses décisions.
Dans ce premier chapitre, on construit cette boucle à la main avec LangGraph.
Pas de create_react_agent magique : on assemble les pièces une à une pour que,
à la fin, plus rien ne soit une boîte noire.
Le problème : un LLM ne boucle pas tout seul
Posez une question à un modèle nu et demandez-lui la météo. Il répondra une phrase plausible… et inventée. Il n’a aucun moyen d’aller chercher l’information.
C’est le point de départ qu’il faut bien avoir en tête : un LLM est une fonction sans état et sans bras. Il transforme une suite de messages en un nouveau message. Pour qu’il devienne un agent, il faut lui adjoindre trois choses — des outils qu’il peut demander, un exécuteur qui réalise ces demandes, et une boucle qui relie les deux jusqu’à ce que la tâche soit finie.
Ce schéma est l’ossature de tout l’article. Chaque section qui suit en construit une partie : d’abord les outils, puis l’état, puis les deux nœuds, et enfin l’arête conditionnelle qui choisit le chemin.
Brique 1 — Déclarer des outils
La première brique consiste à donner au modèle des fonctions Python qu’il a le droit d’appeler.
from langchain_core.tools import tool
@tool
def meteo(ville: str) -> str:
"""Renvoie la météo actuelle pour une ville donnée."""
# En production : un vrai appel d'API. Ici, on simule.
fake = {"Paris": "18°C, nuageux", "Lyon": "22°C, ensoleillé"}
return fake.get(ville, f"Pas de données pour {ville}.")
@tool
def addition(a: float, b: float) -> float:
"""Additionne deux nombres."""
return a + b
outils = [meteo, addition]
Le décorateur @tool ne se contente pas d’enregistrer la fonction : il en
extrait la signature et la docstring pour produire un schéma que le modèle
comprendra. C’est ce schéma qui permet le tool calling — le modèle ne reçoit
pas votre code, mais sa description.
Brique 2 — L’état, mémoire de travail de l’agent
Un agent a besoin de se souvenir de la conversation en cours : la question, ses propres décisions, les résultats des outils. En LangGraph, cette mémoire de travail est l’état (state), un objet qui circule entre les étapes.
Pour un agent conversationnel, l’état se résume souvent à une liste de messages. LangGraph fournit un raccourci, mais écrivons-le explicitement pour voir le mécanisme clé — le reducer :
from typing import Annotated
from typing_extensions import TypedDict
from langgraph.graph.message import add_messages
class Etat(TypedDict):
messages: Annotated[list, add_messages]
add_messages est un reducer : il indique à LangGraph que, lorsqu’un nœud
renvoie de nouveaux messages, il faut les ajouter à la liste existante plutôt
que de l’écraser. Sans lui, chaque étape effacerait l’historique et l’agent
perdrait le fil.
Concrètement, l’état va s’enrichir à chaque tour de boucle. Voici comment il grossit sur une question qui déclenche un outil :
Brique 3 — Les deux nœuds : penser, puis agir
Un agent ReAct ne fait que deux choses, en alternance :
- Penser — le modèle examine l’état et décide : répondre, ou appeler un outil ?
- Agir — si un outil est demandé, on l’exécute et on renvoie son résultat au modèle.
Cela donne deux nœuds. Le premier appelle le modèle, augmenté de la
connaissance des outils via bind_tools :
from langchain.chat_models import init_chat_model
modele = init_chat_model("openai:gpt-4o-mini")
modele_outille = modele.bind_tools(outils)
def noeud_modele(etat: Etat) -> dict:
reponse = modele_outille.invoke(etat["messages"])
return {"messages": [reponse]}
bind_tools attache les schémas des outils au modèle. Sa réponse pourra
désormais contenir des tool_calls : la demande structurée d’exécuter telle
fonction avec tels arguments.
Le second nœud exécute ces appels. LangGraph fournit ToolNode, qui lit les
tool_calls du dernier message, exécute les bonnes fonctions et renvoie leurs
résultats sous forme de ToolMessage :
from langgraph.prebuilt import ToolNode
noeud_outils = ToolNode(outils)
Pour bien voir ce que fait ToolNode, déplions une invocation d’outil — ce qui
se passe entre le moment où le modèle « demande » et le moment où il « reçoit » :
Brique 4 — L’arête conditionnelle, le cœur agentique
Reste la question décisive : après que le modèle a parlé, où va-t-on ? S’il a demandé un outil, vers le nœud outils. Sinon, c’est qu’il a sa réponse finale — on s’arrête.
Cette décision prise à l’exécution, en inspectant l’état, c’est une arête conditionnelle. C’est elle qui transforme un graphe figé en agent.
from langgraph.graph import StateGraph, START, END
def router(etat: Etat) -> str:
dernier = etat["messages"][-1]
# Le modèle a-t-il demandé un ou plusieurs outils ?
if dernier.tool_calls:
return "outils"
return END
Assembler le graphe
On a toutes les pièces. On les câble :
Déclarer le graphe et ses nœuds
graphe = StateGraph(Etat)
graphe.add_node("modele", noeud_modele)
graphe.add_node("outils", noeud_outils)Définir le point d’entrée
L’exécution commence toujours par le modèle, qui réfléchit à la requête.
graphe.add_edge(START, "modele")Brancher l’arête conditionnelle
Après le modèle, on route selon sa décision : vers outils, ou vers la fin.
graphe.add_conditional_edges("modele", router, {"outils": "outils", END: END})Refermer la boucle
Après l’exécution d’un outil, on revient au modèle pour qu’il intègre le résultat. C’est cette arête de retour qui crée la boucle ReAct.
graphe.add_edge("outils", "modele")
agent = graphe.compile() Le graphe compilé correspond exactement à la Fig.01 : START → modèle, puis
la bifurcation conditionnelle vers outils (qui reboucle) ou vers END.
Le faire tourner
resultat = agent.invoke({
"messages": [{"role": "user", "content": "Quelle météo à Lyon ? Et 21 + 21 ?"}]
})
for m in resultat["messages"]:
m.pretty_print()
Déroulé typique : le modèle demande meteo("Lyon") et addition(21, 21),
le ToolNode exécute les deux, le modèle reçoit "22°C, ensoleillé" et 42,
puis rédige une réponse finale en langage naturel. La boucle s’est arrêtée
d’elle-même, parce que le dernier message du modèle ne contenait plus de
tool_calls.
Observer la boucle en streaming
Plutôt que d’attendre le résultat final, on peut streamer les étapes pour voir l’agent raisonner en direct. C’est le meilleur outil de débogage à ce stade :
for etape in agent.stream(
{"messages": [{"role": "user", "content": "Météo à Lyon ?"}]},
stream_mode="values",
):
etape["messages"][-1].pretty_print()
Chaque itération du stream correspond à un super-step du graphe — un passage
par un nœud. Vous voyez littéralement le AIMessage avec ses tool_calls, puis
le ToolMessage, puis la réponse. Si l’agent se trompe, c’est ici que vous
repérez à quelle étape la trajectoire a dévié.
Et create_react_agent dans tout ça ?
Tout ce que nous venons d’écrire tient en une ligne :
from langgraph.prebuilt import create_react_agent
agent = create_react_agent(modele, outils)
C’est exactement le même graphe — nœud modèle, ToolNode, routage par
tools_condition, boucle de retour. Vous l’utiliserez tout le temps. Mais
maintenant vous savez ce qu’il y a dedans : aucune magie, juste un graphe d’état
avec une arête conditionnelle bien placée.
Ce qu’il faut retenir
- Un agent = un LLM + des outils + une boucle qui exécute ses décisions.
- L’état porte la mémoire de travail ; le reducer
add_messagesempêche l’amnésie. - Le nœud modèle décide, le nœud outils agit.
- L’arête conditionnelle est le cœur agentique : c’est elle qui choisit de boucler ou de s’arrêter.
stream()rend la boucle visible — réflexe de débogage n°1.
Notre agent fonctionne, mais ses outils sont rudimentaires. Au chapitre suivant, on approfondit le tool calling : concevoir des outils que le modèle utilise à bon escient, gérer leurs erreurs, et comprendre ce que voit réellement le modèle quand il les appelle.