
Un agent ne vaut que par ses outils. Le guide précédent a posé la boucle ; celui-ci traite ce qui la rend fiable : le function calling. Comment un LLM, qui ne produit que du texte, en vient-il à appeler proprement une fonction Python ? La réponse tient en un contrat — un schéma JSON — et en deux exigences souvent négligées : valider ce que le modèle renvoie, et gérer les erreurs quand un outil échoue ou que le modèle hallucine ses arguments. Vous construirez un agent météo qui interroge une vraie API — Open-Meteo, sans clé — avec LiteLLM pour l'appel au modèle et Pydantic comme source unique de vérité pour le schéma et la validation. Public visé : développeur Python ayant lu le guide sur les agents.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Le contrat de function calling : le schéma JSON décrit au modèle, l'appel structuré en retour.
- Utiliser Pydantic comme source unique : générer le schéma et valider les arguments.
- Brancher un outil sur une API réelle.
- Gérer les erreurs : JSON cassé, argument manquant, outil qui échoue.
- Appeler le modèle via LiteLLM et son interface unifiée.
Prérequis
Section intitulée « Prérequis »Ce guide prolonge Comprendre les agents IA — la boucle Reason/Act/Observe y est posée. Il vous faut aussi :
- Une instance Ollama avec le modèle
llama3.2. - Les bases de Pydantic et un accès internet pour l'API météo.
- Python 3.10+.
Le contrat : un schéma JSON décrit au modèle
Section intitulée « Le contrat : un schéma JSON décrit au modèle »Un LLM ne sait pas « appeler une fonction ». Il sait produire du texte. Le function calling est un protocole qui transforme cette capacité en appels structurés, et il repose sur un contrat en deux temps.
D'abord, vous décrivez vos outils au modèle : pour chacun, un nom, une description, et un schéma JSON des paramètres attendus. Le modèle ne voit jamais le code — seulement cette description. Ensuite, quand il juge qu'un outil est pertinent, il ne l'exécute pas : il renvoie un appel structuré — le nom de l'outil et un objet JSON d'arguments. C'est votre code qui exécute, puis réinjecte le résultat.
Ce contrat a une conséquence directe sur la fiabilité : le modèle remplit le schéma au mieux, sans garantie. Il peut omettre un champ requis, inverser deux paramètres, passer un nombre sous forme de chaîne. Le schéma dit au modèle ce qu'on attend ; il ne contraint rien. D'où la deuxième moitié du travail — valider.
Pydantic : une seule source de vérité
Section intitulée « Pydantic : une seule source de vérité »La tentation est d'écrire le schéma JSON à la main, dans un gros dictionnaire. C'est une erreur : ce schéma vivrait à côté du code de l'outil, et les deux divergeraient à la première modification.
La bonne approche utilise Pydantic comme source unique. Un modèle Pydantic décrit les arguments de l'outil — une fois. Il sert alors deux fois : il génère le schéma JSON envoyé au LLM, et il valide les arguments que le LLM renvoie.
from pydantic import BaseModel, Field
class MeteoArgs(BaseModel): """Arguments de l'outil météo."""
ville: str = Field(min_length=1, description="Nom de la ville, ex : Paris")Le schéma décrit au modèle se génère directement depuis cette classe :
SCHEMA = [ { "type": "function", "function": { "name": "meteo", "description": "Donne la météo actuelle d'une ville.", "parameters": MeteoArgs.model_json_schema(), }, }]model_json_schema() produit le JSON Schema attendu par le modèle. Le jour où l'outil gagne un paramètre, vous l'ajoutez à la classe Pydantic — et le schéma comme la validation suivent, sans rien dupliquer.
L'outil qui appelle une vraie API
Section intitulée « L'outil qui appelle une vraie API »L'outil du lab interroge Open-Meteo, une API météo publique et sans clé. Le travail se fait en deux requêtes : géocoder la ville en coordonnées, puis récupérer la météo. Le point qui compte n'est pas la météo — c'est la manière d'échouer.
def meteo(ville: str) -> str: """Renvoie la météo actuelle d'une ville via l'API Open-Meteo.""" try: geo = httpx.get( "https://geocoding-api.open-meteo.com/v1/search", params={"name": ville, "count": 1, "language": "fr"}, timeout=10, ).json() except httpx.HTTPError: return "Service de géolocalisation indisponible."
lieux = geo.get("results") if not lieux: # échec « métier », pas technique return f"Ville introuvable : {ville}" lieu = lieux[0] # ... seconde requête pour la météo, même prudence ...Un outil bien conçu ne lève jamais d'exception vers la boucle. Une ville introuvable, une API en panne : il renvoie un message lisible. Pourquoi ? Parce que ce message retourne au modèle. Un agent qui reçoit « Ville introuvable : Pariss » peut corriger la faute et réessayer. Une exception, elle, casse la boucle et l'agent meurt. L'erreur est une donnée, pas un crash.
Gérer les erreurs et valider les arguments
Section intitulée « Gérer les erreurs et valider les arguments »Entre l'appel du modèle et l'exécution de l'outil, une étape filtre tout : elle parse le JSON, le valide avec Pydantic, et n'appelle l'outil qu'ensuite. Chaque échec possible y est transformé en message.
def executer_outil(nom: str, arguments_json: str) -> str: """Valide les arguments du LLM avec Pydantic, puis exécute l'outil.""" try: donnees = json.loads(arguments_json) except json.JSONDecodeError: return "Erreur : arguments JSON invalides."
if nom != "meteo": return f"Erreur : outil inconnu « {nom} »."
try: args = MeteoArgs(**donnees) # validation Pydantic except ValidationError as erreur: detail = erreur.errors()[0] return f"Erreur de validation ({detail['loc']}) : {detail['msg']}."
return meteo(args.ville)Trois pannes sont couvertes, et ce sont les trois pannes réelles du function calling. Le JSON cassé : un petit modèle produit parfois un objet d'arguments mal formé. L'argument halluciné : le modèle appelle meteo sans le champ ville, ou avec une chaîne vide — Pydantic le rejette net. L'outil inconnu : le modèle invente un nom d'outil. Dans les trois cas, la fonction renvoie un texte ; ce texte repart au modèle, qui voit son erreur et se corrige au tour suivant.
La boucle via LiteLLM
Section intitulée « La boucle via LiteLLM »Le guide précédent appelait Ollama à la main. Ici, on passe par LiteLLM : une couche qui expose une seule interface — le format d'OpenAI — quel que soit le fournisseur derrière, Ollama en local comme une API distante. Le schéma des outils, les tool_calls en retour : tout est normalisé.
from litellm import completion
reponse = completion(model=MODELE, messages=messages, tools=SCHEMA)message = reponse.choices[0].messageLa boucle reprend le cycle Reason/Act/Observe, mais lit les tool_calls au format unifié. Les arguments arrivent en chaîne JSON — d'où le passage par executer_outil. Chaque résultat est réinjecté dans un message de rôle tool, rattaché à son appel par tool_call_id.
for appel in message.tool_calls: resultat = executer_outil(appel.function.name, appel.function.arguments) messages.append( {"role": "tool", "tool_call_id": appel.id, "content": resultat} )Faire tourner l'agent
Section intitulée « Faire tourner l'agent »Sur une question météo, l'agent appelle l'outil, observe le résultat réel et répond :
Trace des outils : - meteo({"ville": "Toulouse"}) -> À Toulouse : 17.8 °C, ciel dégagé.
Réponse : Il fait actuellement 17.8 degrés Celsius à Toulouse, sous un ciel dégagé.La suite de tests du lab couvre les trois faces du function calling, et chacune mérite son test. Le contrat : l'outil interrogé en direct renvoie bien une météo lisible (API réelle), et une ville inexistante échoue proprement. La validation : un appel sans le champ ville, ou un JSON cassé, est rejeté sans exception — ces tests sont déterministes, ils ne dépendent d'aucun modèle. L'agent complet : une question météo déclenche l'outil et produit une réponse. Le réel prouve l'intégration ; le déterministe prouve la robustesse.
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
| L'agent reboucle sur le même outil | Provider LiteLLM ollama/ | Utiliser ollama_chat/<modèle> |
ValidationError non gérée | L'outil appelé sans passer par la validation | Parser et valider avec Pydantic avant d'exécuter |
json.JSONDecodeError | Le modèle a produit des arguments mal formés | Capturer l'erreur, renvoyer un message au modèle |
| Le schéma et l'outil divergent | Schéma JSON écrit à la main | Générer le schéma depuis le modèle Pydantic |
Aucun tool_calls en retour | Modèle sans support du function calling | Choisir un modèle compatible (llama3.2, par ex.) |
À retenir
Section intitulée « À retenir »- Le function calling est un contrat : un schéma décrit les outils, le modèle renvoie un appel structuré, votre code exécute.
- Le schéma dit ce qu'on attend ; il ne contraint rien — le modèle peut toujours mal le remplir.
- Pydantic est la source unique : il génère le schéma et valide les arguments. Pas de schéma écrit à la main.
- L'ordre est non négociable : parser, valider, puis exécuter.
- Un outil ne lève jamais d'exception vers la boucle — il renvoie un message ; l'erreur est une donnée que le modèle peut corriger.
- LiteLLM unifie l'appel au modèle ; avec Ollama, utiliser le provider
ollama_chat/.
Prochaines étapes
Section intitulée « Prochaines étapes »Pour aller plus loin
Section intitulée « Pour aller plus loin »- Documentation function calling de LiteLLM — le format unifié des outils.
- API Open-Meteo — l'API météo publique utilisée par le lab.