Avant de construire un agent, il faut intérioriser le modèle mental de LangGraph, car tout en découle. L’idée tient en une phrase : on modélise une application comme un graphe dont les nœuds transforment un état partagé et dont les arêtes décrivent les transitions.
Pour bien voir la mécanique, on va construire un graphe sans LLM — un petit pipeline de traitement de texte. Aucune intelligence, juste les rouages. Une fois ces rouages compris, un agent ne sera qu’un graphe particulier où un nœud se trouve être un appel de modèle.
Les quatre primitives
LangGraph repose sur quatre concepts. Apprenez-les une fois, réutilisez-les partout.
- L’état (State) — l’objet typé qui circule dans le graphe et porte toute l’information.
- Le nœud (Node) — une fonction qui reçoit l’état et renvoie une mise à jour.
- L’arête (Edge) — une transition fixe d’un nœud vers le suivant.
- L’arête conditionnelle — une transition dont la cible est décidée à l’exécution.
L’état : le fil qui traverse tout
L’état est un schéma. On le déclare avec un TypedDict. Chaque nœud en reçoit la
version courante et en renvoie une mise à jour partielle.
from typing_extensions import TypedDict
class Etat(TypedDict):
texte: str
nb_mots: int
resume: str
Par défaut, lorsqu’un nœud renvoie une clé, sa valeur écrase la précédente.
C’est le comportement voulu pour nb_mots ou resume. Mais parfois on veut
accumuler plutôt qu’écraser — et c’est là qu’interviennent les reducers (on y
revient plus bas).
Les nœuds : des fonctions pures sur l’état
Un nœud est une simple fonction état → mise à jour de l'état. Écrivons-en deux.
def compter_mots(etat: Etat) -> dict:
n = len(etat["texte"].split())
return {"nb_mots": n}
def resumer(etat: Etat) -> dict:
# Ici, un vrai LLM ferait le travail. On simule.
premiere_phrase = etat["texte"].split(".")[0]
return {"resume": premiere_phrase.strip() + "."}
Remarquez : chaque nœud ne renvoie que ce qu’il modifie. Il ne reconstruit pas l’état entier. LangGraph se charge de fusionner.
Câbler le graphe avec des arêtes
On déclare un StateGraph, on y ajoute les nœuds, puis on les relie. START et
END sont deux nœuds spéciaux qui marquent l’entrée et la sortie.
from langgraph.graph import StateGraph, START, END
graphe = StateGraph(Etat)
graphe.add_node("compter", compter_mots)
graphe.add_node("resumer", resumer)
graphe.add_edge(START, "compter")
graphe.add_edge("compter", "resumer")
graphe.add_edge("resumer", END)
app = graphe.compile()
compile() valide le graphe (pas de nœud orphelin, pas d’impasse) et renvoie un
objet exécutable. On l’invoque avec un état initial :
resultat = app.invoke({"texte": "LangGraph modélise un agent comme un graphe. C'est puissant."})
print(resultat["nb_mots"]) # 9
print(resultat["resume"]) # "LangGraph modélise un agent comme un graphe."
Les reducers : accumuler au lieu d’écraser
Reprenons le cas où l’on veut ajouter à une valeur plutôt que la remplacer — typiquement une liste de logs, ou… une liste de messages de conversation. On annote alors le champ avec un reducer : une fonction qui dit comment fusionner l’ancienne et la nouvelle valeur.
from typing import Annotated
import operator
class Etat(TypedDict):
# operator.add concatène les listes au lieu de les écraser
journal: Annotated[list[str], operator.add]
Désormais, si un nœud renvoie {"journal": ["étape A"]} et un autre
{"journal": ["étape B"]}, l’état final contiendra ["étape A", "étape B"].
L’arête conditionnelle : la primitive qui rend agentique
Jusqu’ici, le chemin est figé : compter → resumer → END. Pour un agent, on a
besoin que le graphe décide où aller, en fonction de l’état. C’est l’arête
conditionnelle : on lui donne une fonction de routage qui inspecte l’état et
renvoie le nom du prochain nœud.
def router(etat: Etat) -> str:
if etat["nb_mots"] > 100:
return "resumer" # texte long → on résume
return END # texte court → inutile
graphe.add_conditional_edges("compter", router, {"resumer": "resumer", END: END})
C’est tout le secret. Un workflow utilise des arêtes fixes ; un agent utilise une arête conditionnelle pour décider, à chaque tour, s’il continue à travailler ou s’il s’arrête. Le modèle ne fait que fournir la matière de cette décision.
Inspecter ce qui circule
Comme l’état est un objet bien défini, on peut l’observer à chaque étape via le streaming — un réflexe qui démystifie totalement le graphe :
for etape in app.stream({"texte": "..."}, stream_mode="values"):
print(etape) # l'état complet après chaque nœud
stream_mode="values" émet l’état entier après chaque super-step ;
stream_mode="updates" n’émet que la mise à jour produite par le nœud. Voir
l’état évoluer pas à pas est la meilleure façon de comprendre — et de déboguer —
un graphe.
Du graphe à l’agent
Vous avez maintenant les quatre primitives. Un agent ReAct, que l’on construit au chapitre suivant, n’est rien de plus que :
- un état = une liste de messages (avec le reducer
add_messages) ; - un nœud qui appelle le LLM ;
- un nœud qui exécute les outils ;
- une arête conditionnelle qui, après le LLM, choisit : exécuter un outil, ou terminer.
Rien de neuf — juste les briques de ce chapitre, assemblées avec un LLM au milieu.
Ce qu’il faut retenir
- LangGraph modélise une application comme un graphe d’état : nœuds, arêtes, état partagé.
- Un nœud est une fonction
état → mise à jour; il ne renvoie que ce qu’il change. - Un reducer (comme
add_messages) permet d’accumuler au lieu d’écraser. - L’arête conditionnelle est la primitive qui rend un graphe agentique : elle décide du chemin à l’exécution.
stream()permet de voir l’état évoluer nœud par nœud.
Les rouages sont en main. Mettons un LLM au cœur du graphe et construisons votre premier agent.