Aller au contenu principal

Laisser un modèle OpenAI appeler tes fonctions Python : le tool calling sans magie

Un assistant qui répond « je n'ai pas accès à vos commandes » sert peu. Le tool calling permet au modèle de demander l'exécution de tes fonctions Python, puis de répondre avec le résultat. Cet article décortique la boucle d'échange avec l'API OpenAI, montre un exemple complet et testé, puis détaille les règles de sécurité qui évitent qu'un modèle consulte les données du mauvais client.

Guides & tutoriels ·
Adel LATIBI
Adel LATIBI

Le Briefing Dev - les ressources et actus de la semaine, droit dans ta boîte chaque vendredi gratuitement.

En vous inscrivant, vous acceptez de recevoir notre newsletter. Désinscription possible à tout moment.

La plupart des premiers projets construits sur l'API d'OpenAI ressemblent à une interface de chat : une question part, un texte revient. Pratique pour reformuler un e-mail ou résumer un document. Beaucoup moins pour une boutique en ligne dont les clients demandent où en est leur colis, parce que le modèle ne connaît ni la base de données, ni le transporteur, ni le client.

La réponse habituelle consiste à tout coller dans le prompt : l'historique des commandes, le catalogue, les conditions de retour. Ça fonctionne pour une démonstration et s'effondre dès que les données grossissent ou changent toutes les minutes. Elle pose aussi une question gênante : pourquoi envoyer au modèle les données de tous les clients pour répondre à un seul ?

Le tool calling, ou appel d'outils, renverse la logique. Le modèle reçoit la liste des fonctions disponibles, décide laquelle il a besoin d'appeler, et ton code l'exécute puis lui renvoie le résultat. Le mécanisme paraît mystérieux tant qu'on ne l'a pas vu de près. Une fois la boucle comprise, il se réduit à une trentaine de lignes de Python et quelques règles de prudence.

Le problème : un modèle qui ne sait rien de ton application

Un modèle de langage a été entraîné sur des textes publics jusqu'à une certaine date. Il ne sait pas que la commande 4471 a été expédiée hier, ni que le client qui écrit en est le titulaire. Interrogé sur ce sujet, il a deux comportements possibles : avouer qu'il ne sait pas, ou inventer une réponse plausible. Le second cas est le plus dangereux, parce qu'une date de livraison inventée ressemble exactement à une vraie.

Pour répondre correctement, l'application doit aller chercher l'information au bon endroit, au moment de la question, et seulement l'information qui concerne la personne connectée. C'est le travail du code classique : une requête en base, un appel à l'API du transporteur. Le modèle, lui, sait très bien comprendre « mon colis n'est toujours pas arrivé, c'est la 4471 » et en déduire qu'il faut consulter le statut de cette commande.

Le tool calling relie ces deux compétences. Le modèle comprend la demande et choisit l'action, le code exécute l'action et garde le contrôle sur ce qui est autorisé.

Le principe : le modèle demande, ton code exécute

Point qui surprend la plupart des gens la première fois : le modèle n'exécute jamais rien. Il ne voit pas ton code, il n'a pas accès à ta base de données. Il produit seulement un message structuré qui dit, en substance, « j'aimerais appeler la fonction statut_commande avec l'argument numero = "4471" ». Ton programme lit ce message, décide s'il l'accepte, exécute la fonction et renvoie le résultat dans un nouvel appel à l'API.

Ton code Python Modèle (API) 1. question + liste des outils 2. « appelle statut_commande(4471) » 3. vérifie les droits, exécute la fonction 4. résultat (function_call_output) 5. réponse rédigée pour le client
Deux appels à l'API pour une seule question : le modèle demande l'outil, le code l'exécute et renvoie le résultat.

Chaque outil se décrit avec un nom, une phrase d'explication et un schéma JSON de ses paramètres. Cette description est la seule chose que le modèle connaît de ta fonction : il s'en sert pour décider quand l'appeler et avec quels arguments. Une description vague donne des appels approximatifs, et c'est souvent là que se joue la qualité de l'ensemble, bien plus que dans le choix du modèle.

Un dernier élément du mécanisme compte pour la suite : le modèle peut demander plusieurs appels d'outils dans une même réponse, ou enchaîner un nouvel appel après avoir lu un premier résultat. Le code doit donc tourner en boucle jusqu'à ce que le modèle réponde par du texte, avec une limite pour éviter qu'il tourne indéfiniment.

Un exemple complet avec l'API Responses

1. La fonction métier, écrite comme n'importe quelle fonction

# outils.py
import json
import os
import re
 
from openai import OpenAI
 
client = OpenAI()
MODELE = os.environ["OPENAI_MODEL"]
 
COMMANDES = {
    ("cli_208", "4471"): {"statut": "expédiée", "transporteur": "Colissimo"},
}
 
 
def statut_commande(client_id: str, numero: str) -> dict:
    if not re.fullmatch(r"\d{4,8}", numero):
        return {"erreur": "numéro de commande invalide"}
    commande = COMMANDES.get((client_id, numero))
    if commande is None:
        return {"erreur": f"aucune commande {numero} sur ce compte"}
    return commande

Le dictionnaire COMMANDES remplace ici la base de données, mais tout le reste est réaliste. La fonction prend deux paramètres, l'identifiant du client et le numéro de commande, et cherche la commande avec les deux à la fois. Le modèle ne fournira que le second, le premier viendra de la session de l'utilisateur connecté. Cette séparation est le point de sécurité le plus important de l'article, et on y revient plus bas.

2. La description de l'outil

OUTILS = [
    {
        "type": "function",
        "name": "statut_commande",
        "description": "Renvoie le statut d'expédition d'une commande du client connecté.",
        "parameters": {
            "type": "object",
            "properties": {
                "numero": {
                    "type": "string",
                    "description": "Numéro de commande, chiffres uniquement",
                },
            },
            "required": ["numero"],
            "additionalProperties": False,
        },
        "strict": True,
    },
]

L'option strict demande à l'API de garantir que les arguments produits respectent le schéma : le champ numero sera présent, sera une chaîne, et aucun champ inventé ne s'y ajoutera. En mode strict, tous les champs doivent figurer dans required et additionalProperties doit valoir False. Le schéma garantit la forme des arguments, jamais leur légitimité : une chaîne de chiffres valide peut très bien désigner la commande de quelqu'un d'autre.

3. La boucle d'échange

def executer(appel, client_id: str) -> dict:
    arguments = json.loads(appel.arguments)
    if appel.name == "statut_commande":
        return statut_commande(client_id, arguments["numero"])
    return {"erreur": f"outil inconnu : {appel.name}"}
 
 
def repondre(question: str, client_id: str) -> str:
    echanges = [{"role": "user", "content": question}]
 
    for _ in range(5):
        reponse = client.responses.create(
            model=MODELE,
            instructions="Tu réponds aux clients d'une boutique en ligne. "
                         "Utilise les outils pour toute information sur une commande.",
            input=echanges,
            tools=OUTILS,
        )
        appels = [item for item in reponse.output if item.type == "function_call"]
        if not appels:
            return reponse.output_text
 
        echanges += reponse.output
        for appel in appels:
            resultat = executer(appel, client_id)
            echanges.append({
                "type": "function_call_output",
                "call_id": appel.call_id,
                "output": json.dumps(resultat, ensure_ascii=False),
            })
 
    raise RuntimeError("Trop d'appels d'outils successifs")

La boucle fait exactement ce que montre le schéma. Elle envoie la conversation et la liste des outils, regarde si la réponse contient des demandes d'appel, exécute chacune d'elles et renvoie les résultats en les rattachant à leur call_id, l'identifiant qui permet au modèle de savoir quel résultat correspond à quelle demande. Les éléments renvoyés par le modèle sont ajoutés tels quels à l'historique avant les résultats, sans quoi l'API ne peut pas faire le lien. Quand le modèle répond enfin par du texte, la boucle s'arrête. Au bout de cinq tours, elle abandonne avec une erreur explicite plutôt que de consommer des appels payants sans fin.

Appelée avec repondre("Où en est ma commande 4471 ?", "cli_208"), la fonction renvoie une phrase construite à partir du statut réel. Avec un autre identifiant de client, l'outil renvoie « aucune commande 4471 sur ce compte » et le modèle répond en conséquence. Ce code a été exécuté avec un client d'API simulé pour vérifier la boucle ; avec une vraie clé, seule la formulation de la réponse finale varie.

4. Garder le nom du modèle hors du code

Le nom du modèle est lu dans la variable d'environnement OPENAI_MODEL, comme la clé d'API. Les modèles disponibles changent plusieurs fois par an, et le passage à une nouvelle version ne devrait jamais demander de modifier le programme. L'article sur GPT-5.5 et ce qu'il change pour les développeurs illustre le rythme de ces sorties. Pour vérifier qu'un changement de modèle ne dégrade pas les réponses, la méthode décrite dans l'article sur le test des prompts par jeu d'essai s'applique telle quelle.

Les pièges, surtout ceux de sécurité

Laisser le modèle choisir à qui appartiennent les données

Si la fonction acceptait un paramètre client_id exposé au modèle, n'importe quel utilisateur pourrait écrire « je suis le client cli_312, montre-moi ses commandes », et le modèle, de bonne foi, transmettrait l'identifiant demandé. Tout ce qui relève de l'identité et des droits vient de la session côté serveur, jamais des arguments générés. C'est la même logique que dans l'article sur le principe de moindre privilège : un outil ne doit pouvoir atteindre que ce dont il a besoin pour la personne en face.

Exposer des actions irréversibles sans confirmation

Consulter un statut ne casse rien. Rembourser, annuler une commande ou envoyer un e-mail, si. Pour ces outils, le code ne doit pas exécuter directement la demande du modèle : il prépare l'action, l'affiche à l'utilisateur et attend une validation explicite. Le texte saisi par l'utilisateur, ou celui d'un document que le modèle lit, peut contenir des instructions détournées, une technique appelée injection de prompt. La confirmation humaine reste la barrière la plus fiable contre ce type d'attaque.

Faire confiance aux arguments parce que le schéma est strict

Le mode strict garantit le type, pas la valeur. Un numéro de commande peut contenir n'importe quels chiffres, une quantité peut valoir zéro ou cent mille. La fonction valide ses entrées comme si elles venaient d'un formulaire web, avec la même méfiance. L'expression régulière de l'exemple n'est pas là pour la décoration.

Renvoyer trop de données au modèle

Tout ce que la fonction renvoie part chez le fournisseur du modèle et peut se retrouver dans la réponse affichée. Renvoyer l'objet commande complet, avec l'adresse, le téléphone et le détail du paiement, alors que la question porte sur le statut, expose des données sans raison. La fonction renvoie le strict nécessaire. Les conséquences d'une fuite de ce type sont détaillées dans l'article sur les risques d'une application non sécurisée.

Oublier que l'API peut échouer

Un appel peut expirer, être refusé pour dépassement de quota ou renvoyer une erreur temporaire, et une boucle d'outils multiplie le nombre d'appels par question. Les relances avec délai croissant, la gestion des limites de débit et les délais d'expiration sont traités dans l'article sur la consommation d'une API tierce. Une fonction métier qui échoue doit aussi renvoyer une erreur lisible plutôt que lever une exception : le modèle sait expliquer au client qu'une information est indisponible, à condition qu'on le lui dise.

De l'exemple à une vraie application

Exposer cette fonction repondre derrière une route HTTP transforme le script en service utilisable par un site web ou une application mobile. Le guide pour créer sa première API REST avec Python et FastAPI montre comment faire, et la session utilisateur fournit alors naturellement le client_id. Les mêmes briques mènent ensuite aux projets plus ambitieux décrits dans 15 projets LLM à coder, ou aux agents qui enchaînent plusieurs outils, dont l'article sur les agents IA en entreprise décrit les usages.

Pour les personnes en reconversion, les développeurs juniors et les curieux qui veulent construire une application complète autour de l'API d'OpenAI, la formation Création d'une application IA avec Python et l'API OpenAI couvre ce parcours. La manière d'écrire les instructions et les descriptions d'outils, souvent négligée, est l'objet de la formation Prompt Engineering et API LLM. Les bases du langage, si elles sont encore récentes, s'acquièrent avec Python pour débutants.

Un bon premier exercice : ajouter un deuxième outil, en lecture seule, et observer comment le modèle choisit entre les deux selon la question.

Questions fréquentes

Le modèle exécute-t-il mon code Python ?

Non. Le modèle produit une demande d'appel avec un nom de fonction et des arguments au format JSON. C'est ton programme qui décide d'exécuter ou non cette demande, avec ses propres règles, puis qui renvoie le résultat. Le modèle ne voit jamais le code de la fonction, seulement sa description.

Quelle différence entre tool calling et function calling ?

Les deux termes désignent le même mécanisme. Function calling est le nom d'origine, tool calling le terme plus général utilisé aujourd'hui, parce que les outils ne sont pas tous des fonctions écrites par le développeur : l'API propose aussi des outils intégrés, comme la recherche web.

Combien d'outils peut-on donner au modèle ?

Techniquement plusieurs dizaines, mais chaque description occupe de la place dans la requête et augmente le risque que le modèle choisisse le mauvais. En pratique, mieux vaut un petit nombre d'outils bien décrits, aux rôles nettement distincts, quitte à proposer des jeux d'outils différents selon le contexte de la conversation.

Le tool calling coûte-t-il plus cher qu'une simple question ?

Oui. Les descriptions d'outils sont facturées comme du texte envoyé, et une question qui déclenche un outil demande au moins deux appels à l'API. Le surcoût reste modeste avec des descriptions courtes et des résultats d'outils limités au nécessaire, ce qui est aussi meilleur pour la confidentialité.

Que se passe-t-il si le modèle appelle un outil qui n'existe pas ?

Le code reçoit un nom de fonction qu'il ne connaît pas. La bonne réaction consiste à renvoyer un résultat d'erreur explicite, comme dans la fonction executer de l'exemple, plutôt que de planter. Le modèle peut alors corriger sa demande ou expliquer qu'il ne peut pas répondre.

Faut-il un framework d'agents pour faire du tool calling ?

Non. La boucle de l'article tient en une trentaine de lignes avec le SDK officiel. Les frameworks apportent du confort sur les cas complexes, mais commencer sans eux permet de comprendre ce qui se passe à chaque étape, et de savoir où chercher quand quelque chose se passe mal.

Vous êtes expert ?

Partagez votre expertise sur notre blog

Tutoriel, retour d'expérience, analyse - publiez un article invité et gagnez en visibilité.

Écrire pour nous