- 01Le function calling laisse l'IA demander l'exécution d'une fonction — mais c'est TON code qui l'exécute, toujours.
- 02Le cycle tient en 4 temps : tu décris l'outil, l'IA demande l'appel, tu exécutes, tu renvoies le résultat.
- 03À la fin tu as un script Python complet, compatible OpenAI/Mistral/Ollama, que tu adaptes à tes propres fonctions.
À la fin de ce tuto, tu auras un assistant en ligne de commande qui répond à « Où en est ma commande 4521 ? » en appelant réellement une fonction get_order_status() dans ton code, puis en formulant la réponse en français. Tu comprendras chaque étape du mécanisme, et tu pourras y brancher tes propres fonctions (base de données, API interne, envoi de mail…).
Durée : 20–30 min. Prérequis : Python 3.10+, savoir lancer un script, une clé API (OpenAI ou Mistral — le code est identique, on verra la nuance).
Le modèle mental (lis ça avant de coder)
Le malentendu numéro un : l'IA n'exécute jamais ton code. Elle ne fait que demander « j'aimerais appeler get_order_status avec order_id="4521" ». C'est ton programme qui lance la vraie fonction et lui renvoie le résultat. Tu gardes le contrôle total.
Le cycle complet tient en quatre temps :
1. TU décris les outils disponibles ──► envoyés au modèle avec la question
2. L'IA décide ──► "appelle get_order_status(order_id='4521')"
3. TON code exécute la fonction ──► {"statut": "expédiée", ...}
4. TU renvoies le résultat à l'IA ──► "Votre commande 4521 a été expédiée, livraison le 25/06."
Garde ce schéma en tête : tout le reste n'est que la mise en œuvre de ces quatre flèches.
Étape 0 — Installation
pip install openai
Et ta clé dans l'environnement :
export OPENAI_API_KEY="sk-..." # macOS / Linux
# setx OPENAI_API_KEY "sk-..." # Windows (puis rouvrir le terminal)
Tu préfères Mistral ? Même code, tu changes juste le client :
OpenAI(base_url="https://api.mistral.ai/v1", api_key="...")et le modèle (mistral-small-latest). Avec Ollama en local :base_url="http://localhost:11434/v1".
Étape 1 — Un appel simple, pour voir le problème
Avant les outils, regardons ce qui se passe sans. Crée assistant.py :
from openai import OpenAI
client = OpenAI() # lit OPENAI_API_KEY automatiquement
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Où en est ma commande 4521 ?"}],
)
print(resp.choices[0].message.content)
Lance-le :
python assistant.py
Résultat typique :
Je n'ai pas accès à votre système de commandes. Pour connaître le statut
de la commande 4521, contactez le service client...
Logique : le modèle ne connaît pas tes commandes. C'est exactement ce trou que le function calling va combler.
Étape 2 — Décrire l'outil que l'IA pourra demander
On déclare l'outil dans un format précis (du JSON Schema). Ajoute ceci dans assistant.py :
tools = [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Récupère le statut et la date de livraison d'une commande à partir de son numéro.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Le numéro de commande, ex. '4521'."
}
},
"required": ["order_id"],
},
},
}]
Décortiquons, car chaque champ a un rôle :
name: l'identifiant que l'IA renverra quand elle voudra l'appeler.description: le champ le plus important. C'est uniquement là-dessus que l'IA décide quand utiliser l'outil. Vague = elle l'appelle au mauvais moment ou jamais. Sois explicite sur ce que fait l'outil.parameters: les arguments attendus, en JSON Schema. Ici un seul,order_id, de typestring.required: les arguments obligatoires.
Étape 3 — Premier aller-retour : l'IA demande l'appel
Maintenant on renvoie la question avec les outils, et on observe ce que l'IA répond :
messages = [{"role": "user", "content": "Où en est ma commande 4521 ?"}]
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
msg = resp.choices[0].message
print(msg.tool_calls)
Cette fois, msg.content est vide, mais msg.tool_calls contient :
[ChatCompletionMessageToolCall(
id='call_abc123',
function=Function(
name='get_order_status',
arguments='{"order_id":"4521"}' # ⚠️ une CHAÎNE, pas un dict
),
type='function'
)]
L'IA n'a rien exécuté : elle te dit « voilà la fonction que je veux, avec ces arguments ». Note que arguments est une chaîne JSON — on devra la parser. Le champ id servira à relier le résultat à cette demande précise.
Étape 4 — Exécuter la fonction dans ton code
C'est ici que tu prends la main. La vraie fonction (remplace plus tard par ta base de données) :
import json
ORDERS = {
"4521": {"statut": "expédiée", "livraison_estimée": "2026-06-25"},
"4522": {"statut": "en préparation", "livraison_estimée": "2026-06-27"},
}
def get_order_status(order_id: str) -> dict:
return ORDERS.get(order_id, {"erreur": "commande introuvable"})
Et on exécute ce que l'IA a demandé :
tool_call = msg.tool_calls[0]
args = json.loads(tool_call.function.arguments) # '{"order_id":"4521"}' -> dict
result = get_order_status(**args) # {'statut': 'expédiée', ...}
Étape 5 — Renvoyer le résultat à l'IA pour la réponse finale
L'IA doit maintenant transformer ce dict en phrase. On lui renvoie deux messages : sa propre demande d'appel (pour garder le fil), puis le résultat, relié par tool_call_id :
messages.append(msg) # la demande d'appel de l'IA
messages.append({
"role": "tool",
"tool_call_id": tool_call.id, # relie le résultat à la bonne demande
"content": json.dumps(result, ensure_ascii=False),
})
final = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
print(final.choices[0].message.content)
Résultat :
Votre commande 4521 a bien été expédiée. La livraison est estimée au 25 juin 2026.
Tu viens de boucler les quatre flèches du schéma de départ.
Étape 6 — Le script complet et réutilisable
En pratique, on ne sait pas à l'avance combien d'appels l'IA voudra faire (parfois plusieurs d'affilée). On enveloppe donc tout dans une boucle, avec un garde-fou. Voici le script final, prêt à copier :
import json, os
from openai import OpenAI
client = OpenAI()
# --- 1. Tes vraies fonctions ---
ORDERS = {
"4521": {"statut": "expédiée", "livraison_estimée": "2026-06-25"},
"4522": {"statut": "en préparation", "livraison_estimée": "2026-06-27"},
}
def get_order_status(order_id: str) -> dict:
return ORDERS.get(order_id, {"erreur": "commande introuvable"})
# --- 2. Le registre : nom vu par l'IA -> fonction réelle ---
dispatch = {"get_order_status": get_order_status}
# --- 3. La description vue par l'IA ---
tools = [{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Récupère le statut et la date de livraison d'une commande à partir de son numéro.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "Le numéro de commande, ex. '4521'."}
},
"required": ["order_id"],
},
},
}]
def run(question: str) -> str:
messages = [
{"role": "system", "content": "Tu es l'assistant SAV. Utilise les outils pour répondre précisément."},
{"role": "user", "content": question},
]
for _ in range(5): # garde-fou : 5 tours maximum
resp = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=tools,
)
msg = resp.choices[0].message
if not msg.tool_calls:
return msg.content # plus d'outil à appeler : c'est la réponse finale
messages.append(msg) # on garde la demande dans l'historique
for tc in msg.tool_calls: # l'IA peut demander plusieurs appels d'un coup
args = json.loads(tc.function.arguments)
result = dispatch[tc.function.name](**args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False),
})
return "Trop d'étapes, j'arrête."
if __name__ == "__main__":
print(run("Où en est ma commande 4521 ?"))
print(run("Et la 9999 ?"))
Teste les deux cas : une commande qui existe, une qui n'existe pas. L'IA s'adapte au résultat (erreur: commande introuvable) et le formule poliment.
Ajouter un deuxième outil
Le pattern passe à l'échelle sans effort. Tu écris la fonction, tu l'ajoutes au dispatch et à tools :
def cancel_order(order_id: str) -> dict:
if order_id in ORDERS:
ORDERS[order_id]["statut"] = "annulée"
return {"ok": True, "order_id": order_id}
return {"erreur": "commande introuvable"}
dispatch["cancel_order"] = cancel_order
# + ajoute le bloc correspondant dans `tools`
L'IA choisira seule le bon outil selon la question (« annule la 4521 » → cancel_order). C'est ce mécanisme, mis en boucle, qui fait un agent (sujet des tutos avancés).
Sécurité : la partie non négociable
Comme c'est l'IA qui choisit les arguments, ne lui donne jamais un accès aveugle à des actions sensibles.
- Valide les arguments avant d'exécuter. Un
order_iddoit ressembler à un numéro, pas à"; DROP TABLE…. Ne fais jamais confiance au contenu tel quel. - Moindre privilège. Un outil de lecture ne doit pas pouvoir écrire. Limite chaque fonction au strict nécessaire.
- Confirme l'irréversible. Pour une annulation, un paiement, une suppression : mets une barrière que le modèle ne contrôle pas.
def cancel_order(order_id: str, confirm: bool = False) -> dict:
if not confirm:
return {"action": "confirmation_requise",
"message": f"Confirmer l'annulation de {order_id} ?"}
# ... annulation réelle seulement si confirm=True
Erreurs fréquentes (et comment les régler)
- L'IA invente un argument → ta
descriptionest trop floue, ou il manque une contrainte. Précise, et pour un choix fermé utilise"enum": ["urgent", "normal"]dans le schéma. json.loadsplante → un petit modèle a renvoyé du JSON malformé. Entoure d'untry/exceptet redemande, ou choisis un modèle plus fiable pour cette tâche.- Boucle qui ne s'arrête pas → garde toujours la limite d'itérations (
range(5)ici). - L'outil n'est jamais appelé → améliore la
description, ou force avectool_choice="required". - Tu utilises Claude (API Anthropic) ? Le principe est identique mais le format diffère : les outils se déclarent dans le paramètre
toolsavecinput_schema, et la réponse contient des blocstool_use/tool_result. La logique des quatre temps, elle, ne change pas.
L'adapter à tes besoins
Pour brancher ton propre cas, trois choses à changer seulement :
- Écris ta fonction (
get_stock,creer_facture,chercher_client…) — du Python normal qui retourne un dict. - Ajoute-la au
dispatchet décris-la danstools(soigne ladescription). - Garde le reste tel quel : la boucle
run()fonctionne quel que soit le nombre d'outils.
À retenir
Le function calling laisse l'IA demander l'exécution d'une fonction, mais c'est toujours ton code qui agit. Le cycle tient en quatre temps : tu décris l'outil, l'IA renvoie un tool_call (nom + arguments en chaîne JSON), tu exécutes, tu renvoies le résultat via un message tool relié par tool_call_id. Enveloppe le tout dans une boucle avec garde-fou, soigne tes descriptions, valide les arguments et confirme l'irréversible. Tu tiens là la brique de base de tous les agents.
Articles liés
Construire un chatbot en streaming sur son site avec une API IA
Afficher la réponse d'un LLM token par token (effet « ça s'écrit en direct ») avec une route serveur qui relaie le flux et un client qui le lit.
Brancher une IA à ses outils avec MCP (Model Context Protocol)
Écrire un premier serveur MCP qui expose un outil à un assistant IA, pour lui donner accès à tes données ou actions sans recâbler à chaque intégration.
Obtenir du JSON fiable d'un LLM : structured outputs et validation
Arrêter de parser du texte au petit bonheur : forcer un LLM à répondre en JSON valide, valider la sortie contre un schéma et gérer proprement les cas malformés.