Aller au contenu principal

Module 8 — Agents : boucle de raisonnement et d'action

Un agent, en 2026, c'est un modèle qui décide, à chaque tour, s'il répond directement ou s'il appelle un outil. Le module précédent a montré la boucle à la main. Ici, on l'automatise avec LangGraph, on la borne (pour qu'elle ne tourne pas indéfiniment), et on y insère un humain quand la décision est trop lourde pour être déléguée. Notre assistant devient capable d'enchaîner « regarde le ticket, convertis en euros, vérifie le plafond, ajoute la ligne » sans que le développeur écrive la boucle.

Ce qu'un agent n'est pas

Avant les définitions, quelques négations utiles. Un agent n'est pas un modèle plus intelligent : c'est un modèle placé dans une boucle avec des outils. Un agent n'est pas un système fiable par défaut : sans limite d'itérations, un modèle qui n'obtient pas le résultat attendu peut réessayer indéfiniment. Un agent n'est pas toujours le bon choix : quand l'ordre des étapes est connu à l'avance, une chaîne LCEL est plus prévisible et moins chère.

Le patron ReAct en une phrase

ReAct (Reason + Act) décrit ce que fait un agent moderne à chaque tour : penser (décider quoi faire), agir (appeler un outil ou répondre), observer (recevoir le résultat), recommencer. Historiquement, on encodait ce cycle dans le prompt (« Thought: ... Action: ... Observation: ... »). Depuis l'appel d'outil natif, ReAct se réduit à une boucle très courte : le modèle appelle des outils, la chaîne les exécute et lui rend les résultats, jusqu'à ce que le modèle ne demande plus d'outil et rédige la réponse finale.

LangGraph : le graphe explicite

langgraph est aujourd'hui la bibliothèque conseillée pour bâtir des agents dans l'écosystème LangChain. Elle modélise l'exécution comme un graphe d'états : chaque nœud est une fonction qui reçoit et retourne un état, chaque arête décide de la suite.

Pour un agent à outils, l'objet clef est create_react_agent, qui construit ce graphe pour vous :

from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI

agent = create_react_agent(
model=ChatOpenAI(model="gpt-4o-mini", temperature=0),
tools=[convertir_en_eur, ajouter_ligne_tableur, retriever.as_tool(name="chercher_politique")],
prompt="Vous êtes l'assistant de notes de frais. Utilisez les outils quand nécessaire. Répondez ensuite brièvement en français.",
)

resultat = agent.invoke({"messages": [("user", "J'ai payé 45 USD au restaurant hier. Ajoute la ligne si c'est sous plafond.")]})
print(resultat["messages"][-1].content)

Le graphe interne ne contient que deux nœuds : agent (l'appel modèle) et tools (l'exécution des outils demandés), avec une arête conditionnelle qui boucle tant que le modèle appelle des outils. resultat["messages"] contient la trace complète, exploitable pour l'affichage ou la journalisation.

Un graphe personnalisé

Dès qu'on a besoin d'un contrôle plus fin — un nœud de validation, un nœud de journalisation, un embranchement selon le rôle utilisateur —, on écrit le graphe soi-même :

from langgraph.graph import StateGraph, END
from typing_extensions import TypedDict, Annotated
from langgraph.graph.message import add_messages

class Etat(TypedDict):
messages: Annotated[list, add_messages]

def noeud_agent(etat):
return {"messages": [modele_outille.invoke(etat["messages"])]}

def noeud_outils(etat):
dernier = etat["messages"][-1]
resultats = []
for appel in dernier.tool_calls:
fn = OUTILS[appel["name"]]
resultats.append(ToolMessage(content=str(fn.invoke(appel["args"])), tool_call_id=appel["id"]))
return {"messages": resultats}

def suite(etat):
return "outils" if etat["messages"][-1].tool_calls else END

graphe = StateGraph(Etat)
graphe.add_node("agent", noeud_agent)
graphe.add_node("outils", noeud_outils)
graphe.set_entry_point("agent")
graphe.add_conditional_edges("agent", suite)
graphe.add_edge("outils", "agent")

app = graphe.compile()

L'état est un dictionnaire enrichi à chaque tour ; add_messages accumule au lieu d'écraser. C'est cette forme qui rend l'agent traçable — on peut inspecter l'état à chaque étape.

Borner les itérations

L'échec classique de l'agent : un outil renvoie une erreur, le modèle retente, l'erreur revient, il retente encore. Sans limite, le compteur de jetons explose et l'utilisateur attend. Deux mécaniques imbriquées :

resultat = agent.invoke(
{"messages": [("user", "Ajoute cette ligne.")]},
config={"recursion_limit": 10},
)

recursion_limit fixe le nombre maximum de tours du graphe (10 par défaut dans LangGraph). Au-delà, une exception GraphRecursionError interrompt l'exécution, laissant à l'application le soin de renvoyer une erreur propre à l'utilisateur. En parallèle, chaque outil peut plafonner ses propres retries pour éviter que le modèle ne s'obstine sur une action condamnée.

L'humain dans la boucle

Certaines actions ne doivent pas s'exécuter sans validation humaine : dépasser un plafond, écrire dans un système de production, envoyer un e-mail à un tiers. LangGraph supporte nativement l'interruption sur un nœud :

from langgraph.checkpoint.memory import MemorySaver

app = graphe.compile(checkpointer=MemorySaver(), interrupt_before=["outils"])

config = {"configurable": {"thread_id": "conv-42"}}
app.invoke({"messages": [("user", "Ajoute cette ligne à 850 EUR.")]}, config=config)

# L'exécution s'arrête avant "outils". L'UI présente l'action à l'utilisateur.
# Après approbation :
app.invoke(None, config=config)

Le checkpointer sauvegarde l'état à chaque nœud ; l'interrupt_before marque un point d'arrêt. Reprendre après approbation ne coûte pas de rejouer les étapes précédentes. Ce patron est central pour tout agent qui touche des systèmes réels — c'est le lien avec le cours 30 sur les agents IA en production.

La boucle infinie, à quoi elle ressemble

Un agent qui boucle sans progrès a une signature reconnaissable dans la trace : les mêmes tool_calls reviennent, avec des arguments quasi identiques, tour après tour. Deux causes fréquentes :

  • Un outil qui échoue en retournant un message vague (« erreur »). Le modèle ne sait pas quoi corriger et refait la même chose. Corrigez côté outil : un ValueError("le champ date_depense est requis au format AAAA-MM-JJ") guide le modèle.
  • Un objectif mal formulé. Le modèle cherche à satisfaire une contrainte contradictoire (« ajoute la ligne sans dépasser le plafond ») pour une dépense qui dépasse. Un outil verifier_plafond qui retourne un statut clair (« refusé, dépassement de 120 EUR ») permet au modèle de conclure au lieu de réessayer.
L'agent n'est pas le premier réflexe

Pour un flux dont l'ordre est connu (« récupère → convertis → écris »), une chaîne LCEL est plus rapide, moins coûteuse et déterministe. On passe à l'agent quand l'utilisateur décide de l'ordre ou quand le nombre d'étapes dépend du contenu. Choisir un agent par défaut est un anti-patron courant en 2026.

En résumé

  • Un agent est un modèle placé dans une boucle avec des outils ; LangGraph en donne la représentation explicite en graphe d'états.
  • create_react_agent construit le graphe standard (nœud modèle, nœud outils, boucle conditionnelle) ; on écrit son propre graphe pour ajouter validation, journalisation ou embranchements.
  • Une recursion_limit et des messages d'erreur d'outil précis coupent les boucles infinies avant qu'elles ne coûtent.
  • L'humain dans la boucle s'implémente par un checkpointer + interrupt_before : l'agent s'arrête, l'humain approuve, l'agent reprend sans rejouer.

Module suivant : rendre tout ce qui précède observable — traces d'exécution, coûts par appel, jeux d'évaluation et détection de régression.