Intelligence Artificielle

Donner des outils à un agent LangChain (tool calling)

14 دقائق للقراءة

📍 Article principal de la série : LangChain, LangGraph et CrewAI : le guide des frameworks d’agents IA
Ce tutoriel fait partie de la série « Construire un agent IA avec LangChain ». Pour la vue d’ensemble, lisez d’abord le guide principal.

Jusqu’ici, l’Assistant Teranga lisait un catalogue en mémoire et fouillait une base documentaire. Mais un vrai assistant de support doit agir sur le système de la coopérative : retrouver l’état d’une commande dans la base, vérifier un paiement mobile money, ouvrir un ticket pour le gérant. Toutes ces actions vivent derrière des fonctions et des API. Ce tutoriel montre comment les brancher proprement à l’agent — c’est le cœur de ce qu’on appelle le tool calling.

Un outil bien conçu, ce n’est pas qu’une fonction Python décorée. C’est un contrat clair : un nom parlant, des arguments typés, une description que le modèle lit pour décider quand l’appeler, et une gestion des erreurs qui évite que tout s’effondre quand l’API distante tousse. On va construire ce contrat pas à pas.

🎯 Ce que vous allez apprendre

  • Écrire des outils avec des arguments typés et décrits, pour que le modèle les remplisse correctement.
  • Connecter un outil à une vraie API HTTP (suivi de commande).
  • Gérer les erreurs d’un outil sans planter l’agent.
  • Donner plusieurs outils à l’agent et le laisser les enchaîner.

🛠️ Ce que vous allez construire

Trois outils réels pour l’Assistant Teranga : suivre_commande (interroge une API de suivi), verifier_paiement (contrôle un paiement mobile money), et ouvrir_ticket (escalade vers un humain). À la fin, un client peut écrire « ma commande CMD-2025, elle en est où ? » et l’agent va chercher la réponse dans le vrai système.

Prérequis

  • Avoir suivi le premier tutoriel (modèle, @tool, create_agent).
  • Python 3.10+, le projet installé, et pip install -U requests pour les appels HTTP.
  • Test express : si vous savez écrire une fonction Python qui fait un GET avec requests, vous êtes prêt.
  • ⏱️ Temps estimé : ~40 minutes.

Étape 1 — Un outil aux arguments typés et décrits

Le modèle ne devine pas comment appeler votre fonction : il s’appuie sur le nom, les types des arguments et leurs descriptions. Plus ce contrat est précis, moins l’agent se trompe. En LangChain, on enrichit chaque argument avec Annotated et une description ; le modèle voit alors « cet outil attend un numéro de commande au format CMD-XXXX » et remplit le bon champ.

from typing import Annotated
from langchain.tools import tool

@tool
def suivre_commande(
    numero: Annotated[str, "Numéro de commande au format CMD-1234"]
) -> str:
    """Donne le statut d'une commande de la coopérative à partir de son numéro."""
    # (simulation ; on branchera l'API réelle à l'étape suivante)
    faux = {"CMD-2025": "expédiée le 12/05, livraison prévue le 16/05"}
    return faux.get(numero, f"Commande {numero} introuvable.")

La docstring décrit ce que fait l’outil ; l’annotation décrit chaque argument. Ces deux textes finissent dans le prompt envoyé au modèle : ce sont eux qui guident sa décision. Un outil sans description, c’est un outil que l’agent utilise mal ou pas du tout — la qualité des descriptions est le premier levier de fiabilité.

Point d’étape — l’outil suivre_commande renvoie un statut pour CMD-2025 et « introuvable » sinon. Testez-le seul avec suivre_commande.invoke({"numero": "CMD-2025"}) avant de le donner à l’agent.

Étape 2 — Brancher une vraie API HTTP

Remplaçons la simulation par un vrai appel réseau. La coopérative expose (par exemple) un petit service de suivi sur son serveur. L’outil fait le GET, lit le JSON, et renvoie une phrase lisible. Le point délicat, c’est de ne jamais laisser une exception réseau remonter brute jusqu’à l’agent : on l’attrape et on renvoie un message exploitable.

import requests
from typing import Annotated
from langchain.tools import tool

BASE = "https://api.teranga.example/commandes"

@tool
def suivre_commande(
    numero: Annotated[str, "Numéro de commande au format CMD-1234"]
) -> str:
    """Donne le statut d'une commande de la coopérative à partir de son numéro."""
    try:
        r = requests.get(f"{BASE}/{numero}", timeout=8)
        if r.status_code == 404:
            return f"Commande {numero} introuvable."
        r.raise_for_status()
        d = r.json()
        return f"Commande {numero} : {d['statut']}, livraison prévue le {d['livraison']}."
    except requests.Timeout:
        return "Le service de suivi met trop de temps à répondre, réessayez dans un instant."
    except requests.RequestException as e:
        return f"Impossible de joindre le service de suivi ({e.__class__.__name__})."

On distingue trois cas : commande absente (404), lenteur (timeout) et panne générale. Chacun renvoie un message clair que l’agent saura reformuler poliment au client. C’est une règle d’or du tool calling : un outil renvoie toujours du texte exploitable, jamais une exception. Sinon, l’agent reçoit une erreur Python qu’il ne sait pas gérer et la conversation déraille.

Point d’étape — couper le réseau et rappeler l’outil doit renvoyer un message poli, pas une trace d’erreur Python. Si vous voyez une stack trace, c’est qu’une exception n’est pas attrapée.

Sous le capot : la boucle de tool calling

Pour bien régler ses outils, il faut comprendre ce qui se passe à chaque appel. Le mécanisme tient en une boucle. Vous envoyez la question ; le modèle, qui connaît la liste des outils et leurs descriptions, ne répond pas directement : il émet une demande d’appel d’outil (« appelle suivre_commande avec numero=CMD-2025 »). LangChain exécute réellement la fonction Python correspondante et renvoie son résultat au modèle sous la forme d’un message spécial de type tool.

Le modèle relit alors la conversation enrichie de ce résultat et décide : soit il a tout ce qu’il faut et rédige la réponse finale, soit il lui manque encore une information et il demande un nouvel appel. La boucle tourne ainsi — appel, résultat, réflexion — jusqu’à ce que le modèle estime pouvoir répondre. C’est exactement ce qui permet d’enchaîner plusieurs outils sans l’avoir programmé : le modèle réévalue la situation après chaque résultat. Comprendre cette boucle change tout pour le débogage : quand un agent boucle sans fin ou s’arrête trop tôt, c’est presque toujours qu’un outil renvoie une réponse ambiguë que le modèle réinterprète mal. Un outil qui répond clairement, c’est une boucle qui se termine vite.

Étape 3 — Plusieurs outils, et l’agent qui les enchaîne

Ajoutons deux outils : vérifier un paiement et ouvrir un ticket. L’intérêt d’un agent éclate ici : face à « j’ai payé la CMD-2025 par Orange Money mais je n’ai rien reçu », il peut vérifier le paiement, constater un souci, puis ouvrir un ticket — trois outils dans une seule réponse, sans qu’on ait codé ce scénario précis.

from langchain.agents import create_agent
from langchain.chat_models import init_chat_model

@tool
def verifier_paiement(
    numero: Annotated[str, "Numéro de commande au format CMD-1234"]
) -> str:
    """Indique si le paiement mobile money d'une commande est confirmé."""
    confirmes = {"CMD-2025"}
    return ("Paiement confirmé." if numero in confirmes
            else f"Aucun paiement confirmé pour {numero}.")

@tool
def ouvrir_ticket(
    numero: Annotated[str, "Numéro de commande concerné"],
    motif: Annotated[str, "Résumé court du problème signalé par le client"]
) -> str:
    """Ouvre un ticket de support pour le gérant et renvoie son identifiant."""
    # en vrai : insertion en base ; ici on simule
    return f"Ticket T-{abs(hash(numero+motif)) % 9000 + 1000} ouvert pour {numero}."

modele = init_chat_model("openai:gpt-4o-mini")
assistant = create_agent(
    model=modele,
    tools=[suivre_commande, verifier_paiement, ouvrir_ticket],
    system_prompt=(
        "Tu es l'assistant de support de la coopérative Teranga. Utilise les outils pour "
        "vérifier les faits avant de répondre. Si un paiement n'est pas confirmé ou si le "
        "client signale un litige, ouvre un ticket avec un motif clair. Réponds en français, "
        "avec courtoisie."),
)

Remarquez qu’ouvrir_ticket prend deux arguments : le modèle doit en remplir un (le numéro) à partir du message et synthétiser l’autre (le motif). C’est là que les descriptions d’arguments paient : sans elles, l’agent confond les champs.

Point d’étape — l’agent dispose de trois outils. Vérifiez la liste tools : un outil oublié ici, et l’agent ne pourra jamais l’appeler.

Étape 4 — Le scénario complet

Lançons le cas litigieux et observons l’agent enchaîner les outils de lui-même.

r = assistant.invoke({"messages": [{"role": "user", "content":
    "Bonjour, j'ai payé la commande CMD-2025 par Orange Money mais je n'ai toujours rien reçu."}]})
print(r["messages"][-1].content)

# Pour voir le raisonnement, parcourez tous les messages :
for m in r["messages"]:
    print(m.type, "->", getattr(m, "content", "")[:80])

La boucle finale est instructive : vous y voyez le message humain, puis les appels d’outils (tool), puis la réponse de l’assistant. Pour CMD-2025, le paiement est confirmé et la commande expédiée : l’agent rassure le client avec la date de livraison. Changez pour une commande au paiement non confirmé : l’agent ouvre alors un ticket et l’annonce. Vous n’avez écrit aucun « if » pour ces enchaînements — c’est l’agent qui décide.

Quand forcer ou restreindre un outil

Laisser l’agent totalement libre n’est pas toujours souhaitable. Deux réglages reviennent souvent. D’abord, restreindre : si un outil est sensible (ouvrir un ticket, déclencher un remboursement), précisez dans la consigne les conditions exactes de son usage — « n’ouvre un ticket que si le client signale un litige avéré ». Ensuite, valider les entrées : un outil qui écrit en base doit vérifier lui-même que le numéro a le bon format avant d’agir, car le modèle peut se tromper. La règle générale : plus une action a de conséquences, plus l’outil et la consigne doivent être stricts. Un outil de lecture peut être permissif ; un outil qui modifie quelque chose doit être verrouillé.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
L’agent appelle l’outil avec un mauvais argument Description d’argument absente ou ambiguë Annotez chaque argument avec Annotated[type, "description"]
Une exception réseau fait planter la conversation L’outil laisse remonter l’erreur brute Attrapez les exceptions et renvoyez un message texte
L’agent n’appelle jamais un outil pourtant utile Docstring vague ou consigne qui ne l’évoque pas Décrivez précisément l’outil et rappelez son usage dans le system
L’agent appelle un outil sensible trop souvent Aucune condition d’usage dans la consigne Encadrez l’usage (« uniquement si… ») et validez dans l’outil

🌍 Adaptation au contexte ouest-africain

Les outils sont le pont entre l’agent et les systèmes déjà en place : une base MySQL de commandes, une API d’un agrégateur mobile money, un Google Sheet partagé. Bonne nouvelle, ce pont est bon marché : un outil qui interroge votre base ne coûte rien en appel de modèle, c’est du Python ordinaire. Pensez aux timeouts courts (ici 8 secondes) : sur une connexion instable, un outil qui attend une API lente bloque toute la réponse. Prévoyez aussi un repli gracieux quand un service tiers est injoignable — fréquent avec les passerelles de paiement régionales. Enfin, journalisez les appels d’outils sensibles : un ticket ouvert, un paiement vérifié, c’est ce que le gérant voudra pouvoir relire.

✅ Récapitulatif

Vous savez désormais écrire des outils robustes : arguments typés et décrits, appel HTTP réel, gestion d’erreurs qui renvoie du texte, et plusieurs outils qu’un agent enchaîne tout seul selon la situation. L’Assistant Teranga ne se contente plus de parler : il interroge le vrai système et agit dans des limites que vous contrôlez. C’est la brique « action » de votre agent. Retenez surtout que la fiabilité d’un agent ne tient pas à la magie du modèle, mais à la qualité de ses outils : un nom clair, des arguments décrits, des erreurs domptées et des garde-fous sur les actions sensibles font plus pour la robustesse qu’un modèle plus gros. Soignez vos outils, et l’agent suivra.

🧾 Aide-mémoire

Élément Rôle
@tool + docstring Déclarer un outil et décrire ce qu’il fait
Annotated[str, "description"] Décrire un argument pour guider le modèle
try / except requests.RequestException Renvoyer du texte au lieu d’une exception
tools=[a, b, c] dans create_agent Donner plusieurs outils à enchaîner
Parcourir r["messages"] Inspecter les appels d’outils et le raisonnement

💪 À vous de jouer

Ajoutez un outil annuler_commande(numero) qui ne s’exécute que si le paiement n’est pas encore confirmé (sinon il refuse poliment). Imposez cette règle à la fois dans la consigne et dans l’outil lui-même.

Voir une solution
@tool
def annuler_commande(
    numero: Annotated[str, "Numéro de commande à annuler"]
) -> str:
    """Annule une commande si elle n'est pas encore payée."""
    if verifier_paiement.invoke({"numero": numero}).startswith("Paiement confirmé"):
        return f"Annulation refusée : {numero} est déjà payée, ouvrez plutôt un ticket."
    return f"Commande {numero} annulée."

# Ajoutez annuler_commande à la liste tools et rappelez la règle dans system_prompt.

La double protection (consigne + vérification dans l’outil) est la bonne pratique : on ne fait jamais confiance au seul jugement du modèle pour une action irréversible.

Le réflexe sécurité avec les outils

Un outil donne à un modèle de langage un accès direct à vos systèmes : c’est puissant, et c’est précisément pourquoi il faut le traiter comme une porte d’entrée à sécuriser. Trois habitudes à prendre dès maintenant. Premièrement, le moindre privilège : si l’outil de suivi n’a besoin que de lire, donnez-lui un accès en lecture seule à la base, jamais un compte qui peut tout modifier. Deuxièmement, ne construisez jamais une requête SQL en collant directement ce que le modèle a produit : passez par des requêtes paramétrées, exactement comme avec une saisie utilisateur, car une valeur d’argument reste une donnée non fiable. Troisièmement, plafonnez les conséquences : un outil d’envoi de SMS qui pourrait spammer doit avoir sa propre limite de débit, indépendamment de l’agent. Le modèle est un assistant compétent, pas un administrateur de confiance — vos outils sont la frontière qui protège la coopérative s’il se trompe ou si on cherche à le manipuler par un message piégé.

Tutoriels frères

Pour aller plus loin

FAQ

Q : Un outil peut-il renvoyer autre chose qu’une chaîne ?
R : Oui, il peut renvoyer des structures (dictionnaires, listes), mais renvoyer une chaîne lisible reste le plus sûr pour que le modèle reformule correctement. Réservez les structures aux cas où un autre outil consomme la sortie.

Q : Comment empêcher l’agent d’inventer un numéro de commande ?
R : Faites valider le format par l’outil (et renvoyez « format invalide » si besoin) et demandez dans la consigne de réclamer le numéro au client quand il manque, plutôt que d’en inventer un.

Q : Combien d’outils un agent peut-il gérer ?
R : Techniquement beaucoup, mais au-delà d’une dizaine le modèle hésite davantage. Si votre agent croule sous les outils, c’est souvent le signe qu’il faut le découper en plusieurs agents spécialisés — un sujet abordé avec LangGraph et CrewAI.

Q : Faut-il un modèle puissant pour le tool calling ?
R : Il faut surtout un modèle qui sache appeler des fonctions de façon fiable. Les modèles hébergés récents le font très bien ; les petits modèles locaux y arrivent pour un ou deux outils simples, mais peinent à enchaîner plusieurs appels. Pour un assistant de support qui jongle avec trois ou quatre outils, un modèle hébergé reste le choix le plus sûr en production.

مشاركة