Aller au contenu
Développement medium

Function calling : connecter un LLM à de vrais outils

12 min de lecture

logo python

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.

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

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

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.

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

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.

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].message

La 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}
)

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.

SymptômeCause probableSolution
L'agent reboucle sur le même outilProvider LiteLLM ollama/Utiliser ollama_chat/<modèle>
ValidationError non géréeL'outil appelé sans passer par la validationParser et valider avec Pydantic avant d'exécuter
json.JSONDecodeErrorLe modèle a produit des arguments mal formésCapturer l'erreur, renvoyer un message au modèle
Le schéma et l'outil divergentSchéma JSON écrit à la mainGénérer le schéma depuis le modèle Pydantic
Aucun tool_calls en retourModèle sans support du function callingChoisir un modèle compatible (llama3.2, par ex.)
  • 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/.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn