Ce que fait réellement le function calling dans un LLM
Demandez à un modèle « quel temps fait-il à Austin en ce moment » sans aucune connexion au monde extérieur, et il n’a que deux options : dire qu’il ne sait pas, ou inventer quelque chose. Le function calling lui offre une troisième option. Il décrit ce dont il a besoin, transmet cette description à votre code et attend une vraie réponse avant de répondre à son tour.
Le nom est un peu trompeur. Le modèle n’appelle rien. Il émet un bloc de données structurées indiquant quel tool il souhaite et avec quels arguments, et votre application décide si elle l’exécute et ce qu’elle fait du résultat. « Tool use » est le terme le plus exact, celui qu’utilise la documentation d’Anthropic ; OpenAI et Google parlent le plus souvent de function calling. Le mécanisme est le même sous les deux noms. Le modèle ne produit jamais que la demande, jamais l’effet de bord.
Pourquoi le modèle demande des tools mais ne les exécute jamais
Imaginez un client au comptoir qui peut désigner des plats sur le menu et dire combien il en veut, mais qui ne passe jamais derrière le comptoir. Le modèle est ce client : il lit le menu que vous lui avez remis (les définitions des tools), choisit un plat et précise combien il en veut (les arguments). Votre code est la personne derrière le comptoir qui prépare la commande, et qui peut refuser une commande qui n’a pas de sens.
Cette séparation est tout le modèle de sécurité. Un appel de tool est une suggestion que votre code peut valider ou rejeter, exactement comme n’importe quelle autre entrée venant d’une source non fiable, car c’est exactement ce qu’elle est. Même un modèle piégé par une injection de prompt via une page web malveillante ne peut rien faire de plus que demander un appel de tool : il ne peut pas contourner votre validation pour atteindre directement la base de données.
Comment définir un tool avec JSON Schema
Chaque fournisseur attend les mêmes trois informations pour chaque tool : un nom, une description que le modèle utilise pour décider quand le tool s’applique, et un JSON Schema pour les arguments. Voici un tool météo pour l’API OpenAI Chat Completions :
| |
Passez plus de temps sur la description que sur le schéma. Le modèle choisit un tool en comparant la demande de l’utilisateur aux descriptions de la liste que vous lui avez envoyée. Une description vague (« des trucs sur la météo ») se retrouve éclipsée par un tool mieux décrit qui fait autre chose, ou se déclenche sur des demandes qu’elle ne devrait pas traiter. Rédigez-la comme si vous deviez expliquer la fonction à un collègue qui ne lira qu’une seule phrase à son sujet.
Comment fonctionne le cycle requête-réponse du function calling
Un appel de tool nécessite au moins deux requêtes. Voici l’aller-retour complet avec l’API OpenAI :
| |
La première réponse ne répond pas à la question. Elle arrive avec
finish_reason sur tool_calls et aucun contenu textuel, car le modèle a
décidé qu’il avait besoin de get_weather avant de pouvoir dire quoi que ce
soit d’utile. call.function.arguments arrive sous forme de chaîne JSON,
pas d’objet déjà parsé, donc json.loads n’est pas optionnel. Vous exécutez
vous-même get_weather (c’est une fonction Python ordinaire ; le modèle n’a
aucun moyen de l’exécuter), puis vous ajoutez le résultat sous forme de
message avec role: "tool" et le tool_call_id correspondant.
Seule la seconde requête, qui transporte désormais ce résultat, vous renvoie
une vraie réponse. Si vous ajoutez le message de l’assistant avec son
tool_calls mais que vous omettez le résultat du tool correspondant, l’API
rejette la requête suivante au lieu de deviner.
En quoi le tool use d’Anthropic diffère du function calling d’OpenAI
Même mécanisme, noms de champs différents. Si vous changez de fournisseur ou que vous en prenez en charge les deux, c’est de là que viendront vos bugs :
| Concept | OpenAI | Anthropic |
|---|---|---|
| Champ listant les tools | tools, chaque tool enveloppé dans {"type": "function", "function": {...}} | tools, chaque tool à plat : {"name", "description", "input_schema"} |
| Clé du schéma des arguments | parameters | input_schema |
| Demande du modèle | tableau tool_calls sur le message, arguments en chaîne JSON | bloc de contenu tool_use, input déjà sous forme d’objet parsé |
| Envoi du résultat | nouveau message, role: "tool", tool_call_id | nouveau message utilisateur, bloc de contenu type: "tool_result", tool_use_id |
| Signaler un appel en échec | pas de champ dédié : placez l’erreur dans content | is_error: true sur le bloc tool_result |
Le piège se trouve dans le format des arguments. Anthropic vous donne
input déjà parsé en dict Python ou en objet JS ; OpenAI vous donne une
chaîne que vous devez vous-même passer à json.loads. Du code écrit pour
l’un puis branché sur l’autre démarre sans erreur et échoue seulement au
premier appel de tool. Si vous prenez en charge les deux, écrivez une seule
fonction de normalisation : les deux chemins y passent avant que vos
implémentations de tools ne voient les arguments.
Comment fonctionnent les appels parallèles et les boucles multi-étapes
Les deux API peuvent renvoyer plusieurs appels de tool dans une seule
réponse. Demandez la météo de trois villes et vous obtenez trois entrées
dans tool_calls, pas trois allers-retours séparés. Exécutez-les (en
parallèle, si elles sont indépendantes) et ajoutez les trois résultats
avant la requête suivante. Chaque ID d’appel émis par le modèle a besoin
d’un résultat ; n’en renvoyez qu’un sur trois et la requête échoue.
Certaines tâches nécessitent plusieurs tours : chercher un utilisateur, puis son historique de commandes, puis signaler la commande en retard. Écrivez-le comme une boucle qui continue d’appeler le modèle et d’exécuter ce qu’il demande jusqu’à ce qu’une réponse arrive sans appel de tool. Puis plafonnez-la. Rien dans l’API n’empêche un modèle de demander neuf fois de suite le même tool quand les résultats ne le rapprochent pas d’une réponse. Une limite fixe, disons 10 ou 20 itérations, transforme cela en une erreur claire plutôt qu’en une facture qui s’envole.
Erreurs courantes du function calling
Traiter les arguments comme une entrée fiable. args["city"] sort d’un
modèle, ce qui veut dire qu’il sort de tout texte ayant alimenté la
conversation, y compris une page web scrapée ou un document que le modèle
devait résumer. Validez-le avant qu’il n’atteigne une requête en base de
données ou une commande shell. Un tool qui construit du SQL en interpolant
les arguments du modèle a la même surface d’injection qu’un tool qui
construit du SQL à partir d’un formulaire web.
Faire confiance au schéma comme s’il s’agissait d’une garantie. Le
modèle respecte bien required et enum, mais pas parfaitement. Il finira
tôt ou tard par envoyer "unit": "F" au lieu de "fahrenheit", ou par
omettre un champ obligatoire. Validez, et renvoyez un message d’erreur clair
comme résultat du tool plutôt que de planter. Dites au modèle ce qui
n’allait pas et il pourra souvent réessayer avec des arguments corrigés au
tour suivant.
Laisser le modèle choisir seul des tools destructeurs. Si un tool
supprime des données ou envoie un e-mail, laisser tool_choice en
automatique est le mauvais réglage par défaut. Gardez les tools destructeurs
hors de la liste jusqu’à ce qu’une personne ait approuvé l’action précise,
ou rendez l’effet réversible (un brouillon plutôt qu’un e-mail envoyé) et
laissez une étape séparée le valider.
Quand ne pas utiliser le function calling
Chaque appel de tool ajoute un aller-retour vers le modèle, plus la latence propre au tool. Si le « tool » ne devait jamais renvoyer qu’un fait statique, placez ce fait dans le prompt ou dans le contexte récupéré et évitez l’aller-retour ; c’est à cela que sert le retrieval du RAG. Et si la décision est en réalité une classification avec un ensemble réduit et fixe de résultats possibles (router ce ticket vers l’une de quatre files), un appel typé et non génératif répond plus vite et moins cher que de demander à un modèle de chat généraliste de choisir un tool. C’est exactement ce pour quoi sont conçus les modèles system one.
Le function calling rentabilise son coût quand l’action dépend de données d’exécution que le modèle ne peut pas avoir dans son prompt, et que l’ensemble des actions possibles est trop ouvert pour être codifié en machine à états. C’est aussi la boucle qui se cache derrière chaque agent CLI de code : lire un fichier, exécuter un test, décider de la suite, appeler un autre tool.
Commencez par un seul tool, une fonction Python codée en dur derrière, et une boucle qui s’arrête après un seul aller-retour. Réglez bien la validation des arguments et le chemin d’erreur avant d’ajouter un second tool. La plupart des bugs d’une configuration à plusieurs tools se révèlent être le premier tool qui gère mal des arguments qu’il n’attendait pas.