Construire un agent LLM en Python avec LangGraph
Tu veux construire un agent IA qui raisonne, utilise des outils et prend des décisions autonomes ? Pas un simple chatbot qui régurgite du texte — un vrai agent capable de chercher sur le web, interroger une base de données, exécuter du code et formuler une réponse synthétique. En 2026, LangGraph est devenu le framework de référence pour ça. Et je vais te montrer comment en construire un de zéro.
Pourquoi LangGraph plutôt que les appels API bruts ?
Avant de plonger dans le code, posons le problème. Tu peux tout à fait construire un agent avec l'API OpenAI ou Anthropic directement. Voici à quoi ça ressemble :
import openai
messages = [{"role": "system", "content": "Tu es un assistant."}]
while True:
response = openai.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=my_tools,
)
msg = response.choices[0].message
messages.append(msg)
if msg.tool_calls:
for call in msg.tool_calls:
result = execute_tool(call)
messages.append({"role": "tool", "content": result})
else:
breakÇa fonctionne. Mais dès que tu dépasses le cas trivial, tu te retrouves à gérer manuellement :
- •L'état de la conversation (et sa persistance)
- •Le routage conditionnel (« si l'outil échoue, retente avec d'autres paramètres »)
- •La mémoire à long terme
- •Les boucles infinies (le modèle qui appelle le même outil en boucle)
- •Le streaming des réponses intermédiaires
- •La parallélisation des appels d'outils
Tu finis par réinventer un framework. Autant en utiliser un qui a été conçu pour ça.
L'architecture ReAct en 60 secondes
L'architecture ReAct (Reason + Act) est le pattern dominant pour les agents LLM. Le principe est simple : le modèle alterne entre raisonnement et action.
- 1.Raisonnement : le modèle analyse la question et décide quoi faire
- 2.Action : il appelle un outil (recherche web, calcul, API...)
- 3.Observation : il lit le résultat de l'outil
- 4.Répétition : il recommence jusqu'à avoir assez d'informations pour répondre
En LangGraph, cette boucle se traduit par un graphe avec deux nodes et un edge conditionnel. C'est tout. Pas besoin de classes abstraites, d'héritage multiple ou de decorators ésotériques.
Installation et setup
pip install langgraph langchain-openai python-dotenvCrée un fichier .env :
OPENAI_API_KEY=sk-...Étape 1 : Définir l'état du graphe
L'état est le coeur de LangGraph. C'est un objet typé qui circule entre les nodes du graphe. Chaque node peut le lire et le modifier.
from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]C'est minimaliste, mais c'est suffisant pour un agent ReAct. Le champ messages contient l'historique de la conversation. L'annotation add_messages indique à LangGraph de fusionner les messages plutôt que de les remplacer.
Tu peux enrichir l'état selon tes besoins :
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
iteration_count: int # Compteur anti-boucle infinie
tool_results: dict # Cache des résultats d'outils
final_answer: str | None # Réponse finale formatéeÉtape 2 : Définir les outils
Les outils sont des fonctions Python décorées avec @tool. LangGraph (via LangChain) gère automatiquement la conversion en JSON Schema pour l'API du modèle.
from langchain_core.tools import tool
import math
@tool
def calculer(expression: str) -> str:
"""Évalue une expression mathématique Python.
Exemples : '2 + 2', 'math.sqrt(144)', '3.14 * 5**2'
"""
try:
result = eval(expression, {"math": math, "__builtins__": {}})
return f"Résultat : {result}"
except Exception as e:
return f"Erreur : {e}"
@tool
def rechercher_web(query: str) -> str:
"""Recherche des informations sur le web.
Utilise cette fonction pour des faits récents ou des données actuelles.
"""
# En production : appel à Tavily, SerpAPI, ou Brave Search
return f"Résultats pour '{query}' : [résultats simulés]"
@tool
def lire_fichier(chemin: str) -> str:
"""Lit le contenu d'un fichier local.
Chemin relatif au répertoire de travail.
"""
try:
with open(chemin) as f:
return f.read()[:2000] # Limite à 2000 caractères
except FileNotFoundError:
return f"Fichier non trouvé : {chemin}"
tools = [calculer, rechercher_web, lire_fichier]La docstring de chaque outil est critique. C'est elle que le modèle lit pour décider quel outil utiliser et comment. Une mauvaise docstring = des appels d'outils incorrects. Sois précis, donne des exemples.
Étape 3 : Construire le graphe
Voici le coeur du système. On crée deux nodes (agent et tools) et un edge conditionnel qui décide de la suite.
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
# Initialiser le modèle avec les outils
llm = ChatOpenAI(model="gpt-4o", temperature=0)
llm_with_tools = llm.bind_tools(tools)
# Node agent : le modèle réfléchit et décide
def agent_node(state: AgentState) -> dict:
messages = state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
# Node tools : exécute les outils demandés
tool_node = ToolNode(tools)
# Fonction de routage : continuer ou s'arrêter ?
def should_continue(state: AgentState) -> str:
last_message = state["messages"][-1]
if last_message.tool_calls:
return "tools"
return END
# Assembler le graphe
graph = StateGraph(AgentState)
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue, {
"tools": "tools",
END: END,
})
graph.add_edge("tools", "agent")
# Compiler
agent = graph.compile()Étape 4 : Exécuter l'agent
from langchain_core.messages import HumanMessage
result = agent.invoke({
"messages": [
HumanMessage(content="Quel est le carré de la racine cubique de 729 ?")
]
})
# Afficher la réponse finale
print(result["messages"][-1].content)L'agent va :
- 1.Recevoir la question
- 2.Décider d'utiliser l'outil
calculer - 3.Appeler
calculer("729 ** (1/3)")→ résultat : 9 - 4.Appeler
calculer("9 ** 2")→ résultat : 81 - 5.Formuler la réponse : « Le carré de la racine cubique de 729 est 81. »
Deux appels d'outils, deux itérations de la boucle ReAct, une réponse correcte. Le graphe a fait son travail.
Les 5 pièges qui tuent les agents en production
Piège 1 : La boucle infinie
Le modèle appelle un outil, l'outil renvoie une erreur, le modèle réessaie avec les mêmes paramètres. Indéfiniment. Ta facture API explose.
La solution : un compteur de récursion.
agent = graph.compile()
result = agent.invoke(
{"messages": [HumanMessage(content="...")]},
config={"recursion_limit": 10} # Max 10 itérations
)LangGraph lève une GraphRecursionError quand la limite est atteinte. Gère-la proprement.
Piège 2 : L'explosion des tokens
Chaque itération ajoute des messages à l'état. Après 8-10 appels d'outils, tu approches les limites de contexte du modèle. Et tu paies pour chaque token.
Solution : tronquer l'historique ou résumer les messages intermédiaires.
def agent_node(state: AgentState) -> dict:
messages = state["messages"]
# Garder uniquement les 20 derniers messages
if len(messages) > 20:
messages = [messages[0]] + messages[-19:]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}Piège 3 : Les outils qui ne valident pas leurs entrées
Le modèle envoie des paramètres mal formés ? Ton outil plante. Le modèle voit l'erreur et réessaie — parfois avec les mêmes paramètres. Valide toujours les entrées et retourne des messages d'erreur clairs.
Piège 4 : Le mauvais modèle
Tous les modèles ne sont pas égaux pour le tool calling. GPT-4o et Claude Sonnet 4 excellent. Les modèles plus petits (Llama 8B, Phi-3) échouent souvent sur le format JSON des appels d'outils. Teste avec le modèle que tu vas utiliser en production.
Piège 5 : L'absence de logging
En production, tu dois savoir pourquoi ton agent a pris telle décision. LangGraph s'intègre avec LangSmith pour le tracing. Active-le dès le début — tu me remercieras au premier bug en prod.
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "ls__..."Aller plus loin : mémoire et persistance
Un agent sans mémoire oublie tout entre les conversations. LangGraph propose un système de checkpointing pour persister l'état :
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
agent = graph.compile(checkpointer=memory)
# Chaque conversation a un thread_id
config = {"configurable": {"thread_id": "user-123"}}
result = agent.invoke(
{"messages": [HumanMessage(content="Je m'appelle Patrice")]},
config=config,
)
# Plus tard, l'agent se souvient
result = agent.invoke(
{"messages": [HumanMessage(content="Comment je m'appelle ?")]},
config=config,
)
# → "Tu t'appelles Patrice."Pour la production, remplace MemorySaver par SqliteSaver ou un backend Redis/PostgreSQL.
Comparaison : LangGraph vs API brute
| Critère | API brute | LangGraph |
|---|---|---|
| Temps de setup | 5 minutes | 15 minutes |
| Routage conditionnel | Manuel (if/else) | Déclaratif (edges) |
| Mémoire | À implémenter | Intégrée |
| Streaming | Complexe | Natif |
| Debugging | print() | LangSmith |
| Parallélisation | Threading manuel | Natif |
| Coût cognitif | Élevé à mesure que ça grandit | Stable |
Pour un prototype rapide, l'API brute suffit. Pour tout ce qui doit tenir en production, LangGraph te fait gagner des semaines.
Le code complet en 50 lignes
Voici l'agent minimal fonctionnel, prêt à copier-coller :
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage
import math
class State(TypedDict):
messages: Annotated[list, add_messages]
@tool
def calculer(expression: str) -> str:
"""Évalue une expression mathématique."""
return str(eval(expression, {"math": math, "__builtins__": {}}))
tools = [calculer]
llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
def agent(state: State):
return {"messages": [llm.invoke(state["messages"])]}
def router(state: State):
return "tools" if state["messages"][-1].tool_calls else END
g = StateGraph(State)
g.add_node("agent", agent)
g.add_node("tools", ToolNode(tools))
g.set_entry_point("agent")
g.add_conditional_edges("agent", router, {"tools": "tools", END: END})
g.add_edge("tools", "agent")
app = g.compile()
r = app.invoke({"messages": [HumanMessage(content="Racine carrée de 256 ?")]})
print(r["messages"][-1].content)30 lignes de code effectif. Un agent complet qui raisonne et agit.
Conclusion
LangGraph transforme la construction d'agents LLM d'un exercice de plomberie en un acte d'architecture. Tu dessines un graphe, tu définis des nodes, tu connectes des edges — et le framework gère le reste : la boucle d'exécution, le routage, la mémoire, le streaming.
Le plus important n'est pas le framework. C'est la qualité de tes outils et de leurs docstrings. Un agent avec trois outils bien documentés battra toujours un agent avec vingt outils mal décrits.
J'ai détaillé l'architecture complète d'un agent de production — avec RAG, mémoire multi-couches et sécurité — dans mon guide.
Soutenez mon travail sur Patreon
Accès anticipé aux articles, contenu exclusif, et la satisfaction de soutenir un auteur indépendant.
Rejoindre — à partir de 3€/mois