Aller au contenu principal
Construire un agent LLM en Python avec LangGraph
Retour au blog
IA

Construire un agent LLM en Python avec LangGraph

Patrice Huetz6 avril 20269 min

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 :

python
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.

ℹ️
LangGraph n'est pas LangChain. C'est une bibliothèque séparée construite sur le concept de graphes d'état. Tu peux utiliser LangGraph sans jamais importer LangChain — et c'est d'ailleurs ce que je recommande pour garder les choses simples.

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. 1.Raisonnement : le modèle analyse la question et décide quoi faire
  2. 2.Action : il appelle un outil (recherche web, calcul, API...)
  3. 3.Observation : il lit le résultat de l'outil
  4. 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

bash
pip install langgraph langchain-openai python-dotenv

Crée un fichier .env :

OPENAI_API_KEY=sk-...
⚠️
Ne commets jamais tes clés API dans Git. Utilise `.env` + `.gitignore`. Si tu travailles en équipe, utilise un gestionnaire de secrets comme AWS Secrets Manager ou HashiCorp Vault. Les fuites de clés API coûtent cher — j'ai vu des factures à 4 chiffres après un push accidentel.

É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.

python
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 :

python
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.

python
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.

python
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()
💡
Appelle `agent.get_graph().draw_mermaid()` pour obtenir un diagramme Mermaid de ton graphe. C'est très utile pour débuguer les routages complexes et pour la documentation. Tu peux aussi utiliser `agent.get_graph().draw_png()` si tu as graphviz installé.

Étape 4 : Exécuter l'agent

python
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. 1.Recevoir la question
  2. 2.Décider d'utiliser l'outil calculer
  3. 3.Appeler calculer("729 ** (1/3)") → résultat : 9
  4. 4.Appeler calculer("9 ** 2") → résultat : 81
  5. 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.

python
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.

python
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.

python
import os
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "ls__..."
ℹ️
Le tracing n'est pas un luxe — c'est une nécessité. Sans traces, débuguer un agent multi-étapes revient à chercher une aiguille dans une botte de foin. LangSmith est gratuit pour les faibles volumes. En alternative open source, il y a Langfuse.

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 :

python
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èreAPI bruteLangGraph
Temps de setup5 minutes15 minutes
Routage conditionnelManuel (if/else)Déclaratif (edges)
MémoireÀ implémenterIntégrée
StreamingComplexeNatif
Debuggingprint()LangSmith
ParallélisationThreading manuelNatif
Coût cognitifÉlevé à mesure que ça granditStable

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 :

python
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.

Agents LLM en Python
Agents LLM en Python

Des agents qui marchent. En Python.

Découvrir →
🔒

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

Commentaires

Chargement des commentaires...

Laisser un commentaire