Un modèle GPT sait reformuler et expliquer, mais il ne connaît pas l’état de la commande numéro 4827 de votre boutique : cette information vit dans votre base de données, pas dans le modèle. Le function calling (appel de fonctions) résout exactement ce problème. Vous décrivez au modèle les fonctions que votre code sait exécuter ; quand une question le nécessite, le modèle ne répond pas par du texte inventé, il vous demande d’appeler la bonne fonction avec les bons arguments. Votre programme exécute la fonction, renvoie le résultat, et le modèle rédige la réponse finale à partir de données réelles.
Dans cette série, nous bâtissons un assistant de support client. Au tutoriel précédent, il répondait à des questions générales. Ici, nous lui donnons un premier pouvoir concret : consulter l’état d’une commande. C’est la différence entre un assistant qui bavarde et un assistant qui agit.
🎯 Ce que vous allez apprendre
- Décrire une fonction de votre code pour que le modèle sache quand et comment l’appeler.
- Détecter une demande d’appel de fonction dans la réponse du modèle.
- Exécuter la fonction réelle et renvoyer son résultat au modèle.
- Obtenir une réponse finale rédigée à partir de vos données.
- Gérer plusieurs outils et sécuriser les arguments reçus.
🛠️ Ce que vous allez construire
Un assistant qui, face à « Où en est ma commande 4827 ? », appelle une fonction Python get_order_status, récupère le statut dans une table de commandes simulée, et répond en français : « Votre commande 4827 a été expédiée le 12 et arrive demain. » Le mécanisme est identique avec une vraie base de données : seule l’implémentation de la fonction change.
Prérequis
- Avoir suivi le premier appel à l’API OpenAI en Python (clé configurée, SDK installé).
- Savoir écrire une fonction Python et manipuler un dictionnaire.
- Notions de JSON : un objet avec des clés et des valeurs.
- ⏱️ Temps estimé : environ 35 minutes.
Étape 1 — Écrire la fonction métier
Avant de parler au modèle, on écrit la fonction que l’on veut lui rendre accessible. Le modèle ne l’exécute jamais lui-même : il se contente de demander son exécution. C’est votre code, et lui seul, qui touche aux données. On commence donc par une fonction ordinaire, ici adossée à un dictionnaire qui tient lieu de base de commandes.
COMMANDES = {
"4827": {"statut": "expediee", "livraison": "demain", "montant": 45000},
"5012": {"statut": "en preparation", "livraison": "sous 3 jours", "montant": 120000},
}
def get_order_status(numero):
commande = COMMANDES.get(numero)
if commande is None:
return {"erreur": "commande introuvable", "numero": numero}
return {"numero": numero, **commande}
Cette fonction prend un numéro de commande et renvoie un dictionnaire avec son statut, sa date de livraison et son montant — ou une erreur explicite si le numéro est inconnu. Renvoyer un dictionnaire (plutôt qu’une phrase toute faite) est volontaire : on laisse le modèle formuler la réponse, on lui fournit juste les faits bruts. En production, vous remplaceriez l’accès au dictionnaire par une requête SQL ou un appel à votre API interne, sans rien changer au reste.
Étape 2 — Décrire la fonction au modèle
Le modèle a besoin d’une « carte d’identité » de chaque fonction : son nom, ce qu’elle fait, et la liste de ses paramètres avec leur type. Cette description suit le format JSON Schema. La qualité de la description compte énormément : c’est en la lisant que le modèle décide s’il doit appeler la fonction et avec quelles valeurs.
tools = [
{
"type": "function",
"name": "get_order_status",
"description": "Recupere le statut, la date de livraison et le "
"montant d'une commande a partir de son numero.",
"parameters": {
"type": "object",
"properties": {
"numero": {
"type": "string",
"description": "Le numero de la commande, par ex. 4827",
},
},
"required": ["numero"],
"additionalProperties": False,
},
"strict": True,
},
]
Le champ description guide la décision du modèle ; parameters décrit la forme exacte des arguments attendus. Les deux clés additionalProperties: False et strict: True activent le mode strict : le modèle est alors contraint de produire des arguments conformes au schéma, ni plus ni moins. Sans ce mode, il pourrait inventer un champ ou oublier le numéro. Avec lui, vous recevez toujours un objet valide, ce qui simplifie le code en aval.
Étape 3 — Premier appel : le modèle demande l’outil
On envoie la question de l’utilisateur en passant la liste tools. Le modèle analyse la demande et, s’il juge qu’une fonction est nécessaire, il ne renvoie pas de texte : il renvoie un objet de type function_call dans sa sortie, contenant le nom de la fonction et les arguments choisis.
from openai import OpenAI
import json
client = OpenAI()
historique = [
{"role": "system", "content": "Tu es l'assistant de Baobab Informatique. "
"Tu reponds en francais."},
{"role": "user", "content": "Bonjour, ou en est ma commande 4827 ?"},
]
reponse = client.responses.create(
model="gpt-5.4",
input=historique,
tools=tools,
)
for item in reponse.output:
if item.type == "function_call":
print("Le modele veut appeler :", item.name)
print("Avec les arguments :", item.arguments)
La sortie reponse.output est une liste d’éléments. On la parcourt pour repérer ceux de type function_call. Chacun expose name (la fonction demandée), arguments (une chaîne JSON, par exemple {"numero": "4827"}) et call_id (un identifiant unique de cet appel, dont on aura besoin à l’étape suivante). À ce stade, aucune donnée n’a encore été consultée : le modèle a seulement formulé une intention. C’est à votre code de l’honorer.
✅ Point d’étape — Le terminal affiche le nom de la fonction et les arguments extraits de la question. Si à la place vous obtenez du texte (le modèle a répondu sans outil), vérifiez que la liste
toolsest bien passée et que la description de la fonction est assez claire.
Étape 4 — Exécuter la fonction et renvoyer le résultat
Maintenant que l’on connaît la fonction demandée et ses arguments, on l’exécute réellement, puis on renvoie son résultat au modèle pour qu’il rédige la réponse finale. La règle essentielle : on rattache le résultat à l’appel d’origine grâce au call_id, et on réinjecte aussi l’élément function_call dans l’historique. Le modèle voit ainsi la boucle complète — il a demandé, vous avez répondu.
historique += reponse.output # on garde la demande d'appel
for item in reponse.output:
if item.type == "function_call":
args = json.loads(item.arguments)
resultat = get_order_status(args["numero"])
historique.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(resultat),
})
finale = client.responses.create(
model="gpt-5.4",
input=historique,
tools=tools,
)
print(finale.output_text)
On convertit la chaîne d’arguments en dictionnaire avec json.loads, on appelle la vraie fonction, puis on sérialise son résultat avec json.dumps pour le glisser dans un élément function_call_output. Le second appel à responses.create renvoie cette fois du texte : le modèle a lu le statut réel et rédige une phrase naturelle, par exemple « Votre commande 4827 a bien été expédiée et devrait arriver demain. » La donnée vient de votre code, la formulation vient du modèle : chacun fait ce qu’il fait de mieux.
✅ Point d’étape — La réponse finale mentionne le statut exact présent dans votre dictionnaire. Testez avec un numéro inconnu (« commande 9999 ») : le modèle doit relayer poliment l’erreur « commande introuvable » que la fonction a renvoyée.
Étape 5 — Plusieurs outils et boucle générique
Un vrai assistant dispose de plusieurs fonctions : consulter une commande, vérifier un stock, ouvrir un ticket. On regroupe alors les implémentations dans un dictionnaire et on écrit une boucle qui dispatche n’importe quel appel, quel que soit l’outil choisi par le modèle. Cette structure évite de répéter le même if pour chaque fonction.
OUTILS = {
"get_order_status": get_order_status,
# "verifier_stock": verifier_stock, # ajoutez ici vos autres fonctions
}
def traiter_appels(reponse, historique):
historique += reponse.output
appel_effectue = False
for item in reponse.output:
if item.type == "function_call":
appel_effectue = True
fonction = OUTILS[item.name]
args = json.loads(item.arguments)
resultat = fonction(**args)
historique.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(resultat),
})
return appel_effectue
La boucle parcourt tous les appels demandés — le modèle peut en réclamer plusieurs d’un coup — et appelle la bonne fonction via le dictionnaire OUTILS. Le drapeau appel_effectue indique s’il faut relancer le modèle pour obtenir la réponse rédigée. En enveloppant cela dans une boucle « tant qu’il y a des appels, exécute puis relance », vous gérez même les enchaînements où le modèle a besoin de plusieurs outils successifs avant de conclure.
Sécuriser les arguments reçus
Le mode strict garantit la forme des arguments, mais pas leur innocuité. Si une fonction touche une base de données ou un système de fichiers, traitez les arguments du modèle comme une saisie utilisateur quelconque : validez les bornes, échappez les requêtes, n’exécutez jamais directement une chaîne reçue. Un modèle peut, sous l’effet d’une question piégée, demander un appel inattendu — c’est à votre code de refuser une opération dangereuse. Le principe est simple : le modèle propose, votre code dispose, et garde toujours le dernier mot sur ce qui s’exécute réellement.
Comment le modèle décide d’appeler une fonction
Comprendre la logique de décision évite bien des surprises. À chaque requête, le modèle compare l’intention de l’utilisateur aux descriptions des outils disponibles. S’il estime qu’aucune fonction n’est pertinente — par exemple « Bonjour, ça va ? » — il répond directement par du texte. S’il juge qu’une fonction comble un manque d’information, il émet un appel. Cette décision repose presque entièrement sur la qualité des descriptions : un verbe d’action précis et des exemples de valeurs dans le champ description améliorent nettement la justesse des déclenchements. Une description floue produit soit des appels manqués, soit des appels intempestifs.
Vous pouvez aussi forcer la main au modèle avec le paramètre tool_choice. Réglé sur "auto" (la valeur par défaut), il laisse le modèle juger ; sur "required", il l’oblige à utiliser au moins un outil ; en nommant explicitement une fonction, il impose celle-ci. Le réglage "auto" convient à un assistant conversationnel où certaines questions n’ont besoin d’aucun outil ; le mode forcé sert surtout dans des pipelines où l’on sait d’avance qu’une extraction est nécessaire.
Gardez à l’esprit le coût de ce mécanisme. Un échange avec appel de fonction représente deux requêtes facturées : celle qui produit la demande d’appel, puis celle qui rédige la réponse à partir du résultat. La latence ressentie double également, puisqu’il faut deux allers-retours réseau plus le temps d’exécution de votre fonction. Pour une question fréquente et simple, mettre en cache le résultat de la fonction ou court-circuiter l’API quand la réponse est connue d’avance reste souvent plus efficace que de solliciter le modèle à chaque fois.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| Le modèle répond du texte au lieu d’appeler la fonction | Description trop vague ou outils non transmis | Préciser la description et vérifier le paramètre tools |
KeyError sur call_id |
Résultat renvoyé sans rattacher le call_id d’origine |
Recopier exactement item.call_id dans function_call_output |
| Le second appel reboucle sur un nouvel appel de fonction | Historique incomplet (l’élément function_call n’a pas été réinjecté) |
Faire historique += reponse.output avant d’ajouter le résultat |
JSONDecodeError sur les arguments |
Lecture des arguments comme dict au lieu de chaîne | Toujours passer par json.loads(item.arguments) |
| Arguments incomplets ou champ surnuméraire | Mode strict non activé | Ajouter strict: True et additionalProperties: False |
✅ Récapitulatif
Votre assistant ne se contente plus de parler : il consulte des données réelles. Vous savez décrire une fonction au format JSON Schema, détecter une demande d’appel dans la sortie du modèle, exécuter la fonction côté code, et renvoyer le résultat via call_id pour obtenir une réponse rédigée. Vous avez aussi une boucle générique qui orchestre plusieurs outils. Cette mécanique est le cœur de tout agent : un modèle qui raisonne, des fonctions qui agissent.
🧾 Aide-mémoire
| Élément | Rôle |
|---|---|
tools=[{"type":"function", ...}] |
Déclarer les fonctions disponibles |
strict: True + additionalProperties: False |
Forcer des arguments conformes |
item.type == "function_call" |
Repérer une demande d’appel |
json.loads(item.arguments) |
Lire les arguments |
function_call_output + call_id |
Renvoyer le résultat au modèle |
2e responses.create |
Obtenir la réponse rédigée finale |
💪 À vous de jouer
Ajoutez une fonction ouvrir_ticket(sujet, priorite) qui renvoie un numéro de ticket fictif, déclarez-la dans tools et dans OUTILS, puis demandez à l’assistant : « Ma commande 5012 est en retard, ouvrez un ticket urgent. » Observez-le enchaîner la consultation puis l’ouverture du ticket.
Voir une piste
def ouvrir_ticket(sujet, priorite="normale"):
return {"ticket": "TK-" + str(abs(hash(sujet)) % 10000), "priorite": priorite}
# Schema a ajouter dans tools : proprietes sujet (string) et priorite (string),
# required ["sujet", "priorite"], strict True. Puis OUTILS["ouvrir_ticket"] = ouvrir_ticket.
Tutoriels associés
- Sorties structurées JSON fiables avec l’API OpenAI
- Premier appel à l’API OpenAI en Python
- 🔝 Retour au guide principal de la série
- Documentation officielle : OpenAI — Function calling
FAQ
Le modèle exécute-t-il lui-même ma fonction ?
Non. Il indique seulement quelle fonction appeler et avec quels arguments. L’exécution se fait dans votre code, ce qui vous laisse le contrôle total sur ce qui touche vos données.
Function calling et sorties structurées, est-ce la même chose ?
Les deux reposent sur JSON Schema, mais l’objectif diffère : le function calling laisse le modèle déclencher une action de votre code, tandis que les sorties structurées contraignent la réponse finale à un format précis. Le tutoriel suivant traite ce second cas.
Peut-il appeler plusieurs fonctions en une fois ?
Oui. La sortie peut contenir plusieurs éléments function_call ; il suffit de tous les traiter avant de relancer le modèle, comme dans la boucle de l’étape 5.
Que se passe-t-il si la fonction échoue ?
Renvoyez l’erreur dans le champ output (par exemple un objet avec une clé erreur). Le modèle la lira et la relaiera poliment à l’utilisateur, comme pour un numéro de commande inconnu.