LangChain Masterclass : construire des agents IA autonomes
Résumé rapide
đĄ IntermĂ©diaireâ±ïž 23 min đ Sommaire Introduction : l'Ăšre des agents IA autonomes Agents vs Chains : comprendre la diffĂ©rence fondamentale Le pattern ReAct : raisonner puis agir Anatomie d'un agent LangChain CrĂ©er des tools custom Types de mĂ©moire pour agents Construire un agent de recherche web Construire un agent d'analyse de code Orchestration multi-agents Gestion d'erreurs et garde-fous Patterns avancĂ©s et bonnes pratiques Conclusion et perspectives 1. Introduction :...
đ Sommaire
- Introduction : l’Ăšre des agents IA autonomes
- Agents vs Chains : comprendre la différence fondamentale
- Le pattern ReAct : raisonner puis agir
- Anatomie d’un agent LangChain
- Créer des tools custom
- Types de mémoire pour agents
- Construire un agent de recherche web
- Construire un agent d’analyse de code
- Orchestration multi-agents
- Gestion d’erreurs et garde-fous
- Patterns avancés et bonnes pratiques
- Conclusion et perspectives
1. Introduction : l’Ăšre des agents IA autonomes
Jusqu’Ă rĂ©cemment, les applications d’IA se limitaient Ă des interactions simples : un prompt en entrĂ©e, une rĂ©ponse en sortie. Le modĂšle ne pouvait ni chercher des informations sur le web, ni exĂ©cuter du code, ni interagir avec des APIs externes. C’Ă©tait comme avoir un expert brillant mais enfermĂ© dans une piĂšce sans tĂ©lĂ©phone ni ordinateur.
Les agents IA changent fondamentalement la donne. Un agent est un systĂšme autonome qui utilise un LLM comme “cerveau” pour dĂ©cider quelles actions entreprendre, dans quel ordre, et comment interprĂ©ter les rĂ©sultats pour atteindre un objectif. L’agent peut utiliser des outils (tools) â recherche web, exĂ©cution de code, appels API, lecture de fichiers â et s’adapter dynamiquement en fonction des rĂ©sultats intermĂ©diaires.
Imaginez un assistant qui, lorsque vous lui demandez “Compare les revenus d’Apple et Microsoft au dernier trimestre”, peut automatiquement : (1) chercher les rapports financiers sur le web, (2) extraire les chiffres pertinents, (3) crĂ©er un tableau comparatif, (4) gĂ©nĂ©rer une analyse structurĂ©e â le tout sans intervention humaine. C’est exactement ce que permet un agent IA bien conçu.
LangChain est le framework de référence pour construire ces agents en Python. Avec son écosystÚme riche et sa communauté active, il fournit les abstractions nécessaires pour créer des agents sophistiqués en quelques dizaines de lignes de code. Dans cette masterclass, nous allons construire plusieurs agents concrets, du plus simple au plus complexe, en explorant chaque concept en profondeur.
đĄ Astuce
Les agents IA sont puissants mais ne sont pas une solution miracle. Ils conviennent parfaitement aux tĂąches qui nĂ©cessitent plusieurs Ă©tapes de raisonnement et d’action, mais ajoutent de la complexitĂ© et du coĂ»t (chaque Ă©tape consomme des tokens). Pour les tĂąches simples, une chain classique reste plus efficace et prĂ©visible.
2. Agents vs Chains : comprendre la différence fondamentale
Pour bien comprendre les agents, il faut d’abord clarifier en quoi ils diffĂšrent des chains (chaĂźnes) classiques de LangChain.
Une Chain est un pipeline sĂ©quentiel prĂ©dĂ©fini. Le dĂ©veloppeur dĂ©termine Ă l’avance quelles Ă©tapes seront exĂ©cutĂ©es et dans quel ordre. Par exemple : “prendre la question â chercher dans la base vectorielle â formuler la rĂ©ponse”. Le flux est linĂ©aire et dĂ©terministe â c’est prĂ©visible mais rigide.
Un Agent est un systĂšme dynamique oĂč le LLM dĂ©cide en temps rĂ©el quelles actions entreprendre. L’agent observe l’Ă©tat courant, rĂ©flĂ©chit, choisit un outil, exĂ©cute l’action, observe le rĂ©sultat, et recommence jusqu’Ă atteindre son objectif. Le flux est non-linĂ©aire et adaptatif â c’est flexible mais moins prĂ©visible.
Voici une analogie concrĂšte. Une chain, c’est comme suivre une recette de cuisine : Ă©tape 1, Ă©tape 2, Ă©tape 3, terminĂ©. Un agent, c’est comme un chef expĂ©rimentĂ© qui goĂ»te Ă chaque Ă©tape, ajuste les assaisonnements, change d’approche si nĂ©cessaire, et improvise si un ingrĂ©dient manque. Le rĂ©sultat est souvent meilleur, mais le chemin est imprĂ©visible.
En termes de code, la différence est éloquente :
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# ============================================================
# CHAIN : flux prédéfini et linéaire
# ============================================================
chain = (
ChatPromptTemplate.from_template("Traduis en français : {text}")
| ChatOpenAI(model="gpt-4o-mini")
| StrOutputParser()
)
# Toujours le mĂȘme chemin : prompt â LLM â parse
result = chain.invoke({"text": "Hello world"})
# â "Bonjour le monde"
# ============================================================
# AGENT : flux dynamique et adaptatif
# ============================================================
from langchain.agents import create_react_agent, AgentExecutor
from langchain_community.tools import DuckDuckGoSearchRun, WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [
DuckDuckGoSearchRun(name="web_search"),
WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper()),
]
# L'agent DĂCIDE quels outils utiliser et dans quel ordre
agent = create_react_agent(llm, tools, prompt_template)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = agent_executor.invoke({
"input": "Quel est le PIB de la France en 2025 et comment se compare-t-il Ă l'Allemagne ?"
})
# L'agent peut :
# 1. Chercher "PIB France 2025" sur le web
# 2. Chercher "PIB Allemagne 2025" sur le web
# 3. Consulter Wikipedia pour le contexte historique
# 4. Synthétiser et comparer
# Le nombre d'Ă©tapes et l'ordre sont dĂ©cidĂ©s par le LLM !â ïž Attention
Les agents consomment significativement plus de tokens qu’une chain simple. Chaque cycle “rĂ©flexion â action â observation” implique un appel au LLM avec tout l’historique des Ă©tapes prĂ©cĂ©dentes. Un agent qui fait 5 actions peut consommer 10-20x plus de tokens qu’une chain directe. Surveillez vos coĂ»ts et fixez toujours un max_iterations pour Ă©viter les boucles infinies.
3. Le pattern ReAct : raisonner puis agir
Le pattern ReAct (Reasoning + Acting) est le paradigme fondamental derriĂšre les agents LangChain. ProposĂ© par Yao et al. en 2022, il structure le raisonnement de l’agent en un cycle itĂ©ratif de trois phases :
- Thought (Pensée) : le LLM réfléchit à la situation et décide de la prochaine action
- Action : le LLM choisit un outil et formule l’entrĂ©e pour cet outil
- Observation : le rĂ©sultat de l’action est retournĂ© au LLM comme contexte
Ce cycle se rĂ©pĂšte jusqu’Ă ce que le LLM estime avoir assez d’informations pour formuler une rĂ©ponse finale (Final Answer). Voici un exemple concret de trace ReAct :
Question: Quelle est la population de Tokyo et comment se compare-t-elle Ă Paris ?
Thought: Je dois trouver la population actuelle de Tokyo et de Paris.
Je vais commencer par chercher Tokyo.
Action: web_search
Action Input: "population Tokyo 2025"
Observation: Tokyo compte environ 13,96 millions d'habitants dans la
ville proprement dite et 37,4 millions dans l'agglomération (2025).
Thought: J'ai la population de Tokyo. Maintenant je dois chercher
celle de Paris pour comparer.
Action: web_search
Action Input: "population Paris 2025"
Observation: Paris intra-muros compte environ 2,1 millions d'habitants.
L'agglomĂ©ration parisienne (Ăle-de-France) rassemble 12,3 millions de personnes.
Thought: J'ai les deux chiffres. Je peux maintenant comparer et formuler
une réponse complÚte.
Final Answer: Tokyo (13,96 millions en ville, 37,4M en agglomération) est
significativement plus peuplée que Paris (2,1 millions en ville, 12,3M en
agglomération). Tokyo ville est environ 6,6 fois plus peuplée que Paris
intra-muros, et son agglomĂ©ration est 3 fois plus grande que celle de Paris.Le pattern ReAct est remarquablement efficace parce qu’il combine deux capacitĂ©s que les LLMs maĂźtrisent bien : le raisonnement verbal (chain-of-thought) et la prise de dĂ©cision. En verbalisant son raisonnement, le LLM produit des dĂ©cisions plus cohĂ©rentes et plus facilement debuggables.
L’avantage majeur du ReAct par rapport Ă un simple raisonnement sans action (chain-of-thought) est que le LLM peut corriger son raisonnement en cours de route grĂące aux observations du monde rĂ©el. Si une recherche retourne un rĂ©sultat inattendu, l’agent s’adapte â exactement comme un humain qui ajuste sa stratĂ©gie face Ă de nouvelles informations.
4. Anatomie d’un agent LangChain
Un agent LangChain se compose de quatre briques essentielles : le LLM (cerveau), les outils (capacitĂ©s), le prompt (instructions), et l’exĂ©cuteur (boucle de contrĂŽle). Voyons chaque composant en dĂ©tail.
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool
from langchain_community.tools import DuckDuckGoSearchRun
# ============================================================
# 1. LE LLM (cerveau de l'agent)
# ============================================================
llm = ChatOpenAI(
model="gpt-4o",
temperature=0, # Déterministe pour les agents
max_tokens=4096,
request_timeout=60,
)
# ============================================================
# 2. LES OUTILS (capacités de l'agent)
# ============================================================
@tool
def calculer(expression: str) -> str:
"""Ăvalue une expression mathĂ©matique. Exemple: '2 + 3 * 4'"""
try:
# Sécurisé : n'évalue que des expressions mathématiques
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return "Erreur: expression invalide"
result = eval(expression)
return str(result)
except Exception as e:
return f"Erreur de calcul: {e}"
@tool
def obtenir_date_heure() -> str:
"""Retourne la date et l'heure actuelles."""
from datetime import datetime
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
search_tool = DuckDuckGoSearchRun(name="recherche_web")
tools = [search_tool, calculer, obtenir_date_heure]
# ============================================================
# 3. LE PROMPT (instructions de l'agent)
# ============================================================
prompt = ChatPromptTemplate.from_messages([
("system", """Tu es un assistant de recherche intelligent et méthodique.
Tu as accĂšs Ă des outils pour chercher des informations, faire des calculs,
et obtenir l'heure actuelle.
RĂšgles :
- Utilise les outils quand c'est nécessaire, pas systématiquement
- Vérifie les informations importantes avec une seconde source si possible
- Cite tes sources quand tu utilises la recherche web
- Sois précis et structuré dans tes réponses
- Si tu ne trouves pas l'information, dis-le honnĂȘtement"""),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
# ============================================================
# 4. L'EXĂCUTEUR (boucle de contrĂŽle)
# ============================================================
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # Afficher la trace de raisonnement
max_iterations=10, # Limite de sécurité
max_execution_time=120, # Timeout en secondes
handle_parsing_errors=True,
return_intermediate_steps=True, # Pour le debugging
)
# ============================================================
# UTILISATION
# ============================================================
result = agent_executor.invoke({
"input": "Quelle est la racine carrée de la population de la France ?"
})
print(result["output"])
# L'agent va :
# 1. Chercher la population de la France
# 2. Calculer la racine carrée du résultat
# 3. Formuler la réponse
# Accéder aux étapes intermédiaires
for step in result.get("intermediate_steps", []):
action, observation = step
print(f"Action: {action.tool} â {observation[:100]}")5. CrĂ©er des tools custom
Les outils sont les “bras” de votre agent â ses capacitĂ©s d’interaction avec le monde extĂ©rieur. LangChain fournit des dizaines d’outils prĂ©-construits, mais la vraie puissance rĂ©side dans la crĂ©ation d’outils custom adaptĂ©s Ă votre domaine mĂ©tier.
Il existe plusieurs façons de créer des outils dans LangChain. Le décorateur @tool est la plus simple pour les cas courants, tandis que la classe BaseTool offre plus de contrÎle pour les cas complexes.
from langchain_core.tools import tool, BaseTool, ToolException
from pydantic import BaseModel, Field
from typing import Optional, Type
import httpx
import json
# ============================================================
# Méthode 1 : Décorateur @tool (simple et rapide)
# ============================================================
@tool
def analyser_sentiment(texte: str) -> str:
"""Analyse le sentiment d'un texte et retourne positif, négatif ou neutre.
Args:
texte: Le texte Ă analyser (max 1000 caractĂšres)
"""
# Simulation â en production, appelez votre modĂšle
mots_positifs = {"excellent", "super", "génial", "parfait", "bravo", "merci", "bien"}
mots_negatifs = {"mauvais", "horrible", "nul", "problÚme", "erreur", "pire", "déçu"}
mots = set(texte.lower().split())
score_pos = len(mots & mots_positifs)
score_neg = len(mots & mots_negatifs)
if score_pos > score_neg:
return f"Sentiment: POSITIF (score: +{score_pos})"
elif score_neg > score_pos:
return f"Sentiment: NĂGATIF (score: -{score_neg})"
return "Sentiment: NEUTRE"
@tool
def requete_api(url: str, methode: str = "GET") -> str:
"""Effectue une requĂȘte HTTP vers une API externe.
Args:
url: L'URL de l'API Ă appeler
methode: La méthode HTTP (GET ou POST)
"""
try:
with httpx.Client(timeout=10) as client:
if methode.upper() == "GET":
response = client.get(url)
else:
response = client.post(url)
return json.dumps({
"status_code": response.status_code,
"body": response.text[:2000],
}, ensure_ascii=False)
except Exception as e:
return f"Erreur API: {str(e)}"
# ============================================================
# Méthode 2 : Classe BaseTool (contrÎle total)
# ============================================================
class RechercheBaseConnaissancesInput(BaseModel):
"""Schéma d'entrée pour la recherche dans la base de connaissances."""
query: str = Field(description="La question ou les mots-clés de recherche")
categorie: Optional[str] = Field(
default=None,
description="Catégorie pour filtrer (ex: 'technique', 'juridique', 'finance')"
)
max_results: int = Field(
default=3,
ge=1,
le=10,
description="Nombre maximum de résultats"
)
class RechercheBaseConnaissances(BaseTool):
"""Outil de recherche dans la base de connaissances interne."""
name: str = "recherche_base_connaissances"
description: str = """Recherche des informations dans la base de connaissances
interne de l'entreprise. Utile pour trouver des procédures, de la documentation
technique, des politiques internes, et des FAQ."""
args_schema: Type[BaseModel] = RechercheBaseConnaissancesInput
# Attributs personnalisés
vectorstore: any = None
def __init__(self, vectorstore, **kwargs):
super().__init__(**kwargs)
self.vectorstore = vectorstore
def _run(
self,
query: str,
categorie: Optional[str] = None,
max_results: int = 3,
) -> str:
"""Exécution synchrone de la recherche."""
try:
# Construire les filtres
search_kwargs = {"k": max_results}
if categorie:
search_kwargs["filter"] = {"category": categorie}
# Recherche vectorielle
docs = self.vectorstore.similarity_search(
query, **search_kwargs
)
if not docs:
return "Aucun résultat trouvé dans la base de connaissances."
results = []
for i, doc in enumerate(docs):
source = doc.metadata.get("source", "inconnu")
results.append(
f"[{i+1}] Source: {source}\n{doc.page_content[:500]}"
)
return "\n\n---\n\n".join(results)
except Exception as e:
raise ToolException(f"Erreur de recherche: {str(e)}")
async def _arun(self, query: str, **kwargs) -> str:
"""Version asynchrone (optionnel)."""
return self._run(query, **kwargs)
# ============================================================
# Méthode 3 : Outil avec gestion d'erreurs robuste
# ============================================================
@tool(handle_tool_error=True)
def executer_sql(query: str) -> str:
"""ExĂ©cute une requĂȘte SQL en lecture seule sur la base de donnĂ©es.
ATTENTION : Seules les requĂȘtes SELECT sont autorisĂ©es.
Args:
query: La requĂȘte SQL Ă exĂ©cuter (SELECT uniquement)
"""
# Sécurité : vérifier que c'est un SELECT
if not query.strip().upper().startswith("SELECT"):
raise ToolException(
"Seules les requĂȘtes SELECT sont autorisĂ©es. "
"Reformulez votre requĂȘte en lecture seule."
)
# Sécurité : interdire certains mots-clés
dangerous = ["DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "TRUNCATE"]
if any(kw in query.upper() for kw in dangerous):
raise ToolException("RequĂȘte dangereuse dĂ©tectĂ©e. OpĂ©ration refusĂ©e.")
# Exécution (simulée)
return json.dumps({
"columns": ["id", "name", "revenue"],
"rows": [
[1, "Product A", 150000],
[2, "Product B", 230000],
],
"row_count": 2,
})đĄ Astuce
La description de l’outil est cruciale â c’est ce que le LLM lit pour dĂ©cider quand utiliser l’outil. Une description vague donnera des rĂ©sultats alĂ©atoires. Soyez spĂ©cifique : mentionnez les cas d’usage, les limites, et le format attendu. Par exemple, “Cherche dans la documentation technique interne” est bien meilleur que “Cherche des informations”.
6. Types de mémoire pour agents
La mĂ©moire permet Ă un agent de maintenir le contexte d’une conversation et de se souvenir des informations prĂ©cĂ©dentes. Sans mĂ©moire, chaque interaction est indĂ©pendante â l’agent “oublie” tout entre chaque message. LangChain propose plusieurs types de mĂ©moire adaptĂ©s Ă diffĂ©rents besoins.
from langchain.memory import (
ConversationBufferMemory,
ConversationSummaryMemory,
ConversationBufferWindowMemory,
ConversationSummaryBufferMemory,
)
from langchain_openai import ChatOpenAI
# ============================================================
# 1. Buffer Memory : stocke tout l'historique brut
# ============================================================
# Simple mais consomme beaucoup de tokens sur les longues conversations
buffer_memory = ConversationBufferMemory(
memory_key="chat_history",
return_messages=True,
)
# Ajouter des messages
buffer_memory.save_context(
{"input": "Je travaille sur un projet de chatbot médical."},
{"output": "Intéressant ! Quel type de données médicales utilisez-vous ?"}
)
# Récupérer l'historique
history = buffer_memory.load_memory_variables({})
print(history["chat_history"])
# ============================================================
# 2. Window Memory : garde les N derniers échanges
# ============================================================
# Compromis entre contexte et consommation de tokens
window_memory = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=10, # Garder les 10 derniers échanges
)
# ============================================================
# 3. Summary Memory : résume l'historique avec un LLM
# ============================================================
# Idéal pour les longues conversations : le résumé est compact
summary_memory = ConversationSummaryMemory(
llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
memory_key="chat_history",
return_messages=True,
)
# ============================================================
# 4. Summary Buffer Memory : résumé + derniers messages
# ============================================================
# Le meilleur des deux mondes : résumé du passé + messages récents
summary_buffer_memory = ConversationSummaryBufferMemory(
llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
memory_key="chat_history",
return_messages=True,
max_token_limit=2000, # Résume quand l'historique dépasse 2000 tokens
)
# ============================================================
# Intégration avec un agent
# ============================================================
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "Tu es un assistant utile avec de la mémoire."),
MessagesPlaceholder("chat_history"), # â Injecte la mĂ©moire ici
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
memory=summary_buffer_memory, # â Attacher la mĂ©moire
verbose=True,
)
# Conversation avec mémoire
r1 = agent_executor.invoke({"input": "Je m'appelle Lucas et je développe en Python."})
r2 = agent_executor.invoke({"input": "Quel langage j'utilise ?"})
# â "Vous dĂ©veloppez en Python, Lucas !"7. Construire un agent de recherche web
Construisons un agent pratique qui peut chercher des informations sur le web, les synthĂ©tiser, et fournir des rĂ©ponses sourcĂ©es. C’est l’un des cas d’usage les plus courants et les plus utiles des agents.
"""
Agent de recherche web avancé
Capable de chercher, lire des pages web, et synthétiser les informations.
"""
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool
from langchain_community.tools import DuckDuckGoSearchResults
from langchain_community.document_loaders import WebBaseLoader
from langchain.memory import ConversationSummaryBufferMemory
import json
from datetime import datetime
# ============================================================
# Outils de recherche
# ============================================================
search = DuckDuckGoSearchResults(
name="recherche_web",
max_results=5,
output_format="list",
)
@tool
def lire_page_web(url: str) -> str:
"""Lit et extrait le contenu textuel d'une page web.
Args:
url: L'URL de la page Ă lire
"""
try:
loader = WebBaseLoader(url)
docs = loader.load()
if docs:
content = docs[0].page_content[:3000]
return f"Contenu de {url}:\n\n{content}"
return "Impossible d'extraire le contenu de cette page."
except Exception as e:
return f"Erreur lors de la lecture de {url}: {str(e)}"
@tool
def obtenir_date() -> str:
"""Retourne la date et l'heure actuelles. Utilise cet outil quand
tu as besoin de savoir la date du jour."""
return datetime.now().strftime("Nous sommes le %d/%m/%Y, il est %H:%M.")
@tool
def noter_information(titre: str, contenu: str) -> str:
"""Sauvegarde une information importante pour référence ultérieure.
Utile pour garder trace des découvertes pendant la recherche.
Args:
titre: Titre court de la note
contenu: Contenu de la note
"""
# En production, sauvegarder dans une base de données
return f"â
Note sauvegardée : '{titre}'"
tools = [search, lire_page_web, obtenir_date, noter_information]
# ============================================================
# Configuration de l'agent
# ============================================================
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", """Tu es un agent de recherche expert et méthodique.
Ton processus de recherche :
1. Analyse la question pour identifier les informations nécessaires
2. Effectue des recherches ciblées avec des mots-clés précis
3. Lis les pages les plus pertinentes pour approfondir
4. Vérifie les informations critiques avec une seconde source
5. Synthétise les résultats de maniÚre structurée
RĂšgles :
- Cite TOUJOURS tes sources avec l'URL
- Distingue les faits vérifiés des estimations
- Si les sources se contredisent, mentionne les différentes perspectives
- Structure tes réponses avec des titres et des listes
- Sois concis mais complet
Date actuelle : utilise l'outil obtenir_date si nécessaire."""),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
memory = ConversationSummaryBufferMemory(
llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
memory_key="chat_history",
return_messages=True,
max_token_limit=2000,
)
agent = create_tool_calling_agent(llm, tools, prompt)
research_agent = AgentExecutor(
agent=agent,
tools=tools,
memory=memory,
verbose=True,
max_iterations=15,
max_execution_time=180,
handle_parsing_errors=True,
)
# ============================================================
# Utilisation
# ============================================================
# Recherche simple
result = research_agent.invoke({
"input": "Quelles sont les derniÚres avancées en informatique quantique en 2026 ?"
})
print(result["output"])
# Recherche comparative
result = research_agent.invoke({
"input": "Compare les performances de Claude 3.5 et GPT-4o sur les benchmarks de code."
})
print(result["output"])8. Construire un agent d’analyse de code
Un agent d’analyse de code peut lire, comprendre, exĂ©cuter et debugger du code Python. C’est un outil puissant pour l’automatisation de tĂąches de dĂ©veloppement.
"""
Agent d'analyse et d'exécution de code Python
Capable de lire des fichiers, exécuter du code, et analyser les résultats.
"""
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.tools import tool
from langchain_experimental.tools import PythonREPLTool
import subprocess
import os
# ============================================================
# Outils de code
# ============================================================
# Outil d'exécution Python (sandboxé en production !)
python_repl = PythonREPLTool(
name="executer_python",
description="""Exécute du code Python et retourne le résultat.
Utile pour les calculs, l'analyse de données, la création de graphiques.
Le code a accĂšs Ă pandas, numpy, matplotlib.""",
)
@tool
def lire_fichier(chemin: str) -> str:
"""Lit le contenu d'un fichier source.
Args:
chemin: Chemin vers le fichier Ă lire
"""
try:
if not os.path.exists(chemin):
return f"Fichier non trouvé: {chemin}"
# Sécurité : limiter la taille
size = os.path.getsize(chemin)
if size > 100000:
return f"Fichier trop volumineux ({size} bytes). Max 100KB."
with open(chemin, 'r', encoding='utf-8') as f:
content = f.read()
return f"Contenu de {chemin} ({len(content)} caractĂšres):\n\n```\n{content}\n```"
except Exception as e:
return f"Erreur: {str(e)}"
@tool
def lister_fichiers(repertoire: str = ".") -> str:
"""Liste les fichiers et dossiers dans un répertoire.
Args:
repertoire: Chemin du répertoire (défaut: répertoire courant)
"""
try:
items = []
for item in sorted(os.listdir(repertoire)):
path = os.path.join(repertoire, item)
if os.path.isdir(path):
items.append(f"đ {item}/")
else:
size = os.path.getsize(path)
items.append(f"đ {item} ({size:,} bytes)")
return f"Contenu de {repertoire}:\n" + "\n".join(items)
except Exception as e:
return f"Erreur: {str(e)}"
@tool
def analyser_code_statique(code: str) -> str:
"""Analyse un snippet de code Python pour détecter les problÚmes
potentiels (imports manquants, variables non utilisées, bugs courants).
Args:
code: Le code Python Ă analyser
"""
issues = []
lines = code.split('\n')
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Détections basiques (en production, utilisez pylint/ruff)
if 'eval(' in stripped and 'safe' not in stripped.lower():
issues.append(f"L{i}: â ïž Usage de eval() â risque de sĂ©curitĂ©")
if 'except:' in stripped and 'Exception' not in stripped:
issues.append(f"L{i}: â ïž except trop large (bare except)")
if 'password' in stripped.lower() and '=' in stripped:
issues.append(f"L{i}: đŽ Mot de passe potentiellement en dur")
if stripped.startswith('import *'):
issues.append(f"L{i}: â ïž Import wildcard â Ă©vitez 'from X import *'")
if 'TODO' in stripped or 'FIXME' in stripped:
issues.append(f"L{i}: đ {stripped.strip()}")
if not issues:
return "â
Aucun problÚme détecté dans l'analyse statique."
return "ProblÚmes détectés:\n" + "\n".join(issues)
tools = [python_repl, lire_fichier, lister_fichiers, analyser_code_statique]
# ============================================================
# Agent de code
# ============================================================
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", """Tu es un ingénieur logiciel senior expert en Python.
Tes capacités :
- Lire et analyser du code source
- Exécuter du code Python pour tester des hypothÚses
- Détecter des bugs et proposer des corrections
- Expliquer du code complexe de maniĂšre claire
- Ăcrire du code propre et documentĂ©
Méthodologie :
1. Comprends d'abord le problĂšme complet avant de coder
2. Lis les fichiers pertinents si nécessaire
3. Ăcris du code testĂ© et documentĂ©
4. Vérifie les résultats en exécutant le code
5. Explique ton raisonnement"""),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
code_agent = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=15,
handle_parsing_errors=True,
)
# Utilisation
result = code_agent.invoke({
"input": """Crée une fonction Python qui analyse un fichier CSV de ventes
et génÚre un rapport avec :
- Total des ventes par mois
- Produit le plus vendu
- Tendance (croissance/décroissance)
Teste-la avec des données d'exemple."""
})â ïž Attention
L’exĂ©cution de code par un agent IA est potentiellement dangereuse. En production, sandboxez TOUJOURS l’exĂ©cution dans un conteneur Docker isolĂ©, avec des limites de CPU/mĂ©moire/rĂ©seau. Ne donnez jamais accĂšs au systĂšme de fichiers rĂ©el ou au rĂ©seau sans restrictions. Des outils comme E2B, Modal, ou Docker-in-Docker sont conçus pour cela.
9. Orchestration multi-agents
Pour les tĂąches complexes, un seul agent ne suffit pas toujours. L’orchestration multi-agents permet de spĂ©cialiser chaque agent dans un domaine et de les faire collaborer. C’est le pattern le plus avancĂ© et le plus puissant de l’Ă©cosystĂšme agent.
LangGraph, la bibliothĂšque de LangChain pour les workflows complexes, permet de crĂ©er des graphes d’agents qui communiquent entre eux. Voici un exemple d’architecture multi-agents pour une tĂąche de rĂ©daction d’article :
"""
SystÚme multi-agents pour la rédaction d'articles
- Agent Rechercheur : collecte les informations
- Agent Rédacteur : écrit l'article
- Agent Ăditeur : vĂ©rifie la qualitĂ© et corrige
"""
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from typing import TypedDict, Annotated, Sequence
import operator
# ============================================================
# Ătat partagĂ© entre les agents
# ============================================================
class ArticleState(TypedDict):
"""Ătat global du workflow d'Ă©criture."""
topic: str
research_notes: str
draft: str
review_feedback: str
final_article: str
revision_count: int
messages: Annotated[Sequence, operator.add]
# ============================================================
# Agents spécialisés
# ============================================================
researcher_llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
writer_llm = ChatOpenAI(model="gpt-4o", temperature=0.7)
editor_llm = ChatOpenAI(model="gpt-4o", temperature=0.2)
def researcher_node(state: ArticleState) -> dict:
"""Agent Rechercheur : collecte les informations sur le sujet."""
messages = [
SystemMessage(content="""Tu es un chercheur expert. Ton travail est de
rassembler les informations clés sur un sujet pour un article de blog.
Fournis des faits, des chiffres, des citations, et des angles intéressants.
Organise les notes par thĂšme."""),
HumanMessage(content=f"""Recherche des informations sur le sujet suivant
pour un article de blog :
Sujet : {state['topic']}
Fournis des notes de recherche structurées avec des faits vérifiables."""),
]
response = researcher_llm.invoke(messages)
return {
"research_notes": response.content,
"messages": [AIMessage(content=f"[Rechercheur] Notes complétées.")]
}
def writer_node(state: ArticleState) -> dict:
"""Agent Rédacteur : écrit le brouillon basé sur la recherche."""
feedback = ""
if state.get("review_feedback"):
feedback = f"\n\nFeedback de l'éditeur à prendre en compte :\n{state['review_feedback']}"
messages = [
SystemMessage(content="""Tu es un rédacteur de blog technique talentueux.
Ăcris des articles engageants, bien structurĂ©s, avec un ton professionnel
mais accessible. Utilise des titres, des listes, et des exemples concrets."""),
HumanMessage(content=f"""Ăcris un article de blog basĂ© sur ces notes de recherche :
Notes : {state['research_notes']}
{feedback}
L'article doit ĂȘtre complet, bien structurĂ©, et faire au moins 1000 mots."""),
]
response = writer_llm.invoke(messages)
return {
"draft": response.content,
"messages": [AIMessage(content=f"[Rédacteur] Brouillon rédigé.")]
}
def editor_node(state: ArticleState) -> dict:
"""Agent Ăditeur : vĂ©rifie la qualitĂ© et donne du feedback."""
messages = [
SystemMessage(content="""Tu es un éditeur en chef exigeant mais constructif.
Ăvalue l'article sur : clartĂ©, structure, prĂ©cision factuelle, engagement,
grammaire, et complétude.
Si l'article est satisfaisant, réponds EXACTEMENT "APPROVED" suivi de
suggestions mineures optionnelles.
Sinon, fournis un feedback détaillé pour amélioration."""),
HumanMessage(content=f"""Ăvalue cet article :
{state['draft']}
Donne ton verdict : APPROVED ou feedback détaillé pour révision."""),
]
response = editor_llm.invoke(messages)
return {
"review_feedback": response.content,
"revision_count": state.get("revision_count", 0) + 1,
"messages": [AIMessage(content=f"[Ăditeur] Review #{state.get('revision_count', 0) + 1} terminĂ©e.")]
}
def should_revise(state: ArticleState) -> str:
"""Décide si l'article nécessite une révision."""
if "APPROVED" in state.get("review_feedback", ""):
return "finalize"
if state.get("revision_count", 0) >= 3:
return "finalize" # Max 3 révisions
return "revise"
def finalize_node(state: ArticleState) -> dict:
"""Finalise l'article approuvé."""
return {
"final_article": state["draft"],
"messages": [AIMessage(content="[SystĂšme] Article finalisĂ© â
")]
}
# ============================================================
# Construction du graphe
# ============================================================
workflow = StateGraph(ArticleState)
# Ajouter les nĆuds
workflow.add_node("researcher", researcher_node)
workflow.add_node("writer", writer_node)
workflow.add_node("editor", editor_node)
workflow.add_node("finalizer", finalize_node)
# Définir le flux
workflow.set_entry_point("researcher")
workflow.add_edge("researcher", "writer")
workflow.add_edge("writer", "editor")
workflow.add_conditional_edges(
"editor",
should_revise,
{
"revise": "writer", # Retour au rédacteur
"finalize": "finalizer", # Article approuvé
}
)
workflow.add_edge("finalizer", END)
# Compiler
app = workflow.compile()
# ============================================================
# Exécution
# ============================================================
result = app.invoke({
"topic": "L'impact de l'IA générative sur le développement logiciel en 2026",
"research_notes": "",
"draft": "",
"review_feedback": "",
"final_article": "",
"revision_count": 0,
"messages": [],
})
print(f"\nđ Article final ({result['revision_count']} rĂ©visions) :")
print(result["final_article"])đĄ Astuce
LangGraph supporte nativement la persistance d’Ă©tat avec des checkpointers (SQLite, PostgreSQL). Cela permet de reprendre un workflow interrompu, d’implĂ©menter des points de validation humaine (“human-in-the-loop”), et de debugger en inspectant l’Ă©tat Ă chaque Ă©tape. Activez-le avec workflow.compile(checkpointer=SqliteSaver(db_path)).
10. Gestion d’erreurs et garde-fous
Les agents IA sont par nature imprĂ©visibles. Une gestion d’erreurs robuste est essentielle pour la production. Voici les principaux risques et comment les mitiger.
1. Boucles infinies. L’agent peut entrer dans une boucle oĂč il rĂ©pĂšte les mĂȘmes actions sans progresser. Solution : fixez toujours max_iterations et max_execution_time.
2. Hallucinations d’outils. Le LLM peut inventer des noms d’outils qui n’existent pas ou fournir des paramĂštres invalides. Solution : handle_parsing_errors=True et des descriptions d’outils trĂšs claires.
3. Coûts incontrÎlés. Un agent bavard peut consommer des centaines de milliers de tokens. Solution : monitoring des coûts en temps réel et alertes sur les seuils.
4. Actions dangereuses. Un agent avec accĂšs Ă l’exĂ©cution de code ou aux APIs peut causer des dĂ©gĂąts. Solution : sandboxing, permissions minimales, validation humaine pour les actions critiques.
from langchain.callbacks import CallbackHandler
from langchain_core.callbacks import BaseCallbackHandler
import tiktoken
class SafetyCallbackHandler(BaseCallbackHandler):
"""Callback pour monitorer et limiter les agents."""
def __init__(self, max_tokens: int = 100000, max_cost: float = 1.0):
self.total_tokens = 0
self.max_tokens = max_tokens
self.total_cost = 0.0
self.max_cost = max_cost
self.actions_taken = []
def on_llm_end(self, response, **kwargs):
"""Appelé aprÚs chaque appel LLM."""
if hasattr(response, 'llm_output') and response.llm_output:
usage = response.llm_output.get('token_usage', {})
tokens = usage.get('total_tokens', 0)
self.total_tokens += tokens
# Estimation du coût (GPT-4o)
self.total_cost += tokens * 0.000005 # Approximatif
if self.total_tokens > self.max_tokens:
raise RuntimeError(
f"â ïž Limite de tokens dĂ©passĂ©e: {self.total_tokens}/{self.max_tokens}"
)
if self.total_cost > self.max_cost:
raise RuntimeError(
f"â ïž Limite de coĂ»t dĂ©passĂ©e: ${self.total_cost:.2f}/${self.max_cost}"
)
def on_tool_start(self, tool, input_str, **kwargs):
"""Appelé avant chaque utilisation d'outil."""
self.actions_taken.append({"tool": tool.get("name"), "input": input_str[:200]})
# DĂ©tecter les boucles (mĂȘme outil, mĂȘme input)
recent = self.actions_taken[-5:]
if len(recent) >= 3:
if all(a["tool"] == recent[0]["tool"] and a["input"] == recent[0]["input"]
for a in recent):
raise RuntimeError("â ïž Boucle dĂ©tectĂ©e: mĂȘme action rĂ©pĂ©tĂ©e 3+ fois")
# Utilisation
safety = SafetyCallbackHandler(max_tokens=50000, max_cost=0.50)
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
callbacks=[safety],
max_iterations=10,
max_execution_time=120,
)11. Patterns avancés et bonnes pratiques
Pour construire des agents robustes en production, voici les patterns avancés et les leçons apprises des déploiements réels :
1. Planification avant exĂ©cution (Plan-and-Execute). Au lieu de laisser l’agent dĂ©cider Ă chaque Ă©tape, demandez-lui de crĂ©er un plan d’actions complet d’abord, puis exĂ©cutez chaque Ă©tape. Cela rĂ©duit les erreurs et rend le comportement plus prĂ©visible.
2. Fallback gracieux. Si l’agent Ă©choue, revenez Ă une approche plus simple (chain directe ou rĂ©ponse prĂ©-formatĂ©e) plutĂŽt que de retourner une erreur. L’utilisateur prĂ©fĂšre une rĂ©ponse partielle Ă aucune rĂ©ponse.
3. ObservabilitĂ©. Utilisez LangSmith ou des callbacks custom pour tracer chaque Ă©tape de l’agent. En production, vous devez pouvoir reconstituer le raisonnement de l’agent pour debugger les rĂ©ponses incorrectes.
4. Ăvaluation systĂ©matique. CrĂ©ez un benchmark de 50-100 questions avec les rĂ©ponses attendues. Ăvaluez chaque modification de votre agent sur ce benchmark avant de dĂ©ployer. Le taux de rĂ©ussite, le nombre moyen d’Ă©tapes, et le coĂ»t moyen par requĂȘte sont les mĂ©triques clĂ©s.
5. Choix du modĂšle par Ă©tape. Utilisez un modĂšle puissant (GPT-4o) pour le raisonnement de l’agent et un modĂšle rapide/Ă©conomique (GPT-4o-mini) pour les sous-tĂąches simples (rĂ©sumĂ©, reformulation). Cela rĂ©duit les coĂ»ts de 50-70 % sans impact notable sur la qualitĂ©.
đĄ Astuce
La rĂšgle d’or des agents : commencez simple, complexifiez si nĂ©cessaire. Un agent avec 3 outils bien choisis et un prompt clair surpassera presque toujours un agent avec 15 outils et un prompt vague. Chaque outil supplĂ©mentaire augmente la probabilitĂ© que le LLM fasse un mauvais choix. Testez, mesurez, itĂ©rez.
12. Conclusion et perspectives
Les agents IA reprĂ©sentent une avancĂ©e majeure dans la maniĂšre dont nous utilisons les LLMs. En donnant aux modĂšles la capacitĂ© d’agir dans le monde rĂ©el â chercher des informations, exĂ©cuter du code, appeler des APIs â nous passons d’assistants passifs Ă des collaborateurs actifs capables de rĂ©soudre des problĂšmes complexes de bout en bout.
Les points clés de cette masterclass :
- Le pattern ReAct structure le raisonnement de l’agent de maniĂšre efficace et debuggable
- Les tools custom sont le levier principal pour adapter un agent à votre domaine métier
- La mémoire est essentielle pour les conversations multi-tours et le suivi de contexte
- Le multi-agents avec LangGraph permet de résoudre des problÚmes complexes par la spécialisation
- La sécurité et les garde-fous sont non négociables en production
L’avenir des agents s’annonce passionnant avec l’Ă©mergence de standards comme le Model Context Protocol (MCP) d’Anthropic pour la connexion aux outils, l’amĂ©lioration continue des capacitĂ©s de raisonnement des LLMs, et les architectures multi-agents de plus en plus sophistiquĂ©es. Les agents IA deviendront bientĂŽt aussi courants que les applications web â et LangChain vous positionne idĂ©alement pour ce futur.
“Les agents IA ne remplaceront pas les dĂ©veloppeurs, ils les rendront 10x plus productifs. L’avenir appartient Ă ceux qui savent orchestrer des agents pour automatiser les tĂąches rĂ©pĂ©titives et se concentrer sur la crĂ©ativitĂ© et l’architecture.”
â Dr. Ămilie Martin, Chercheuse en IA Ă l’INRIA
đ Sources
- ReAct: Synergizing Reasoning and Acting in Language Models – Yao et al., 2022 â article fondateur du pattern ReAct
- Documentation LangChain Agents – Guide officiel pour construire des agents
- Documentation LangGraph – Framework pour les workflows multi-agents
- AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation – Microsoft Research
- LangSmith Documentation – ObservabilitĂ© et debugging des agents
- Toolformer: Language Models Can Teach Themselves to Use Tools – Schick et al., 2023
- Model Context Protocol (MCP) – Standard ouvert pour la connexion LLM-outils par Anthropic