Un agent sans bons outils est un cerveau sans mains. Dans le premier agent, on a câblé un outil météo en deux lignes. Ça marche pour une démo — mais en pratique, la qualité des outils détermine la qualité de l’agent bien plus que le choix du modèle. Ce chapitre y est entièrement consacré.
Anatomie d’un outil
Le décorateur @tool transforme une fonction Python en outil exploitable par le
modèle. Trois éléments sont extraits et envoyés au modèle :
from langchain_core.tools import tool
@tool
def convertir_devise(montant: float, de: str, vers: str) -> float:
"""Convertit un montant d'une devise à une autre au taux du jour.
Utiliser pour toute question de conversion monétaire (EUR, USD, GBP...)."""
taux = recuperer_taux(de, vers)
return round(montant * taux, 2)
Ce que le modèle reçoit, ce n’est pas le corps de la fonction, mais son schéma :
- le nom (
convertir_devise) — il doit être explicite ; - les paramètres typés (
montant: float,de: str,vers: str) — les types guident le modèle vers des arguments valides ; - la docstring — la seule explication de quand et comment utiliser l’outil.
Des arguments structurés avec Pydantic
Pour des entrées plus riches, on décrit explicitement le schéma des arguments avec
Pydantic. Les Field(description=...) sont, eux aussi, lus par le modèle.
from pydantic import BaseModel, Field
class RechercheVol(BaseModel):
depart: str = Field(description="Code IATA de l'aéroport de départ, ex. CDG")
arrivee: str = Field(description="Code IATA de l'aéroport d'arrivée, ex. JFK")
date: str = Field(description="Date au format AAAA-MM-JJ")
@tool(args_schema=RechercheVol)
def chercher_vol(depart: str, arrivee: str, date: str) -> str:
"""Recherche les vols disponibles entre deux aéroports à une date donnée."""
...
Plus la description des champs est précise (format attendu, exemple), moins le modèle produit d’arguments invalides. C’est un investissement très rentable.
Ce qui se passe vraiment lors d’un appel
Quand le modèle décide d’utiliser un outil, il ne l’exécute pas — il demande son exécution, dans un message structuré. Décortiquons un tour complet :
Le modèle émet un tool_call
Sa réponse ne contient pas de texte final mais une intention structurée :
AIMessage(content="", tool_calls=[
{"name": "convertir_devise",
"args": {"montant": 100, "de": "EUR", "vers": "USD"},
"id": "call_abc123"}
])Le runtime exécute l’outil
Le ToolNode lit ce tool_call, appelle la vraie fonction Python avec ces
arguments, et capture le résultat.
Le résultat repart vers le modèle
Le résultat est emballé dans un ToolMessage, relié à l’appel par son id :
ToolMessage(content="108.5", tool_call_id="call_abc123")Le modèle intègre et conclut
De retour dans la boucle, le modèle lit le résultat et rédige sa réponse finale — ou demande un autre outil. C’est la boucle ReAct.
Plusieurs outils d’un seul tour
Un modèle moderne peut demander plusieurs outils en une seule réponse quand ils
sont indépendants. Le ToolNode les exécute alors en parallèle et renvoie autant
de ToolMessage, chacun relié à son appel par son id.
C’est un levier de latence important : trois recherches indépendantes lancées ensemble coûtent le temps d’une seule, pas de trois. Concevez vos outils pour qu’ils soient atomiques et sans dépendance mutuelle quand c’est possible.
Gérer les erreurs sans casser la boucle
Un outil réel échoue : API indisponible, argument invalide, time-out. Si l’exception remonte brute, tout l’agent s’arrête. La bonne approche est de renvoyer l’erreur au modèle comme un résultat, pour qu’il puisse réagir — reformuler, réessayer, ou expliquer poliment l’échec.
from langchain_core.tools import tool
@tool
def lire_facture(numero: str) -> str:
"""Récupère le détail d'une facture par son numéro."""
facture = db.get(numero)
if facture is None:
# On NE lève PAS d'exception : on informe le modèle.
return f"Aucune facture trouvée pour le numéro {numero}."
return facture.to_text()
Par défaut, ToolNode intercepte d’ailleurs les exceptions et renvoie leur
message au modèle plutôt que de planter — un comportement configurable via
handle_tool_errors. Mais renvoyer un message d’erreur clair et exploitable
reste votre responsabilité : « Numéro de facture introuvable » aide le modèle ;
une stacktrace Python ne l’aide pas.
Forcer ou restreindre l’usage d’un outil
Parfois, on veut obliger le modèle à utiliser un outil (par exemple toujours
passer par la recherche avant de répondre). bind_tools accepte un paramètre pour
cela :
# Forcer l'appel d'un outil précis
modele.bind_tools(outils, tool_choice="chercher_doc")
# Forcer l'usage d'au moins un outil (lequel, c'est le modèle qui choisit)
modele.bind_tools(outils, tool_choice="any")
À manier avec parcimonie : forcer un outil retire de l’autonomie à l’agent. C’est utile pour cadrer un comportement critique, pas comme réglage par défaut.
Ce qu’il faut retenir
- Le modèle ne voit que la description des outils : nom, types, docstring. C’est là que se gagne ou se perd la fiabilité.
- Préférez des outils atomiques et bien nommés à un outil fourre-tout.
- Une erreur d’outil doit revenir au modèle comme un message exploitable, pas comme une exception qui casse la boucle.
tool_choicepermet de forcer un outil quand le comportement est critique.
Votre agent sait maintenant agir. Mais entre deux requêtes, il oublie tout. Au chapitre suivant, on lui donne une mémoire conversationnelle.