Aller au contenu

Tool Calling (appel de fonctions)

Tool Calling (également appelé Function Calling) permet aux modèles d’aller au-delà de la génération de texte en retournant une « demande d’appel de fonction » structurée lorsque nécessaire. Votre programme exécute la logique réelle (vérifier la météo, interroger la base de données, passer commande), renvoie le résultat au modèle, et le modèle génère une réponse finale basée sur celui-ci. RouteAPI prend en charge Tool Calling sur /v1/chat/completions (format OpenAI) et /v1/messages (format Claude).

Tool Calling est un échange multi-tours :

  1. Vous déclarez un ensemble d’« outils » dans la requête (chaque outil a un nom, une description et un schéma de paramètres).
  2. Le modèle détermine si un outil est nécessaire ; si oui, il retourne une demande d’appel avec des paramètres au lieu d’une réponse directe.
  3. Votre code analyse les paramètres, exécute la logique réelle et obtient le résultat.
  4. Vous renvoyez le résultat au modèle, qui génère une réponse en langage naturel pour l’utilisateur.

Le modèle lui-même n’exécute aucun code ; il décide uniquement « quel outil appeler » et « quels paramètres passer ». L’exécution réelle se produit toujours de votre côté, ce qui signifie que les limites de sécurité (vérifications de permissions, filtrage des paramètres) sont sous votre contrôle.

ScénarioDescription
Requêtes de données en temps réelMétéo, taux de change, cours des actions, inventaire—informations absentes des données d’entraînement du modèle
Intégration système interneInterroger des bases de données, appeler des API internes, lire l’état des commandes
Exécuter des actionsPasser des commandes, envoyer des e-mails, créer des tickets—opérations avec effets secondaires
Extraction structuréeForcer le modèle à produire selon un schéma fixe, équivalent à une sortie structurée
Orchestration d’agentsLes frameworks d’agents utilisent Tool Calling pour piloter des tâches multi-étapes

La capacité Tool Calling dépend du modèle sélectionné. La plupart des modèles grand public (séries OpenAI GPT, Claude, Gemini, etc.) la prennent en charge, mais les détails comme le nombre maximum d’outils et la prise en charge des appels parallèles varient. L’endpoint de liste des modèles ne retourne aucun champ indiquant la capacité Tool Calling : consultez la documentation du fournisseur du modèle, ou envoyez directement une requête réelle avec tools sur le modèle cible pour le vérifier, voir Models.

Dans /v1/chat/completions, les outils sont déclarés via un tableau tools de niveau supérieur, où chaque outil est un objet imbriqué avec type: "function".

{
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Interroger la météo actuelle d'une ville spécifiée. Utiliser le nom complet de la ville en chinois.",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "Nom de la ville, par ex. Beijing" },
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Unité de température, par défaut celsius"
}
},
"required": ["city"]
}
}
}
]
}
ChampTypeRequisDescription
typestringOuiFixé à "function"
function.namestringOuiNom de l’outil, ne peut contenir que lettres, chiffres, underscores et tirets
function.descriptionstringRecommandéDescription de l’objectif de l’outil, le modèle l’utilise pour décider d’appeler
function.parametersobjectNonDéfinition des paramètres, JSON Schema standard

La qualité de description détermine directement si le modèle choisira correctement l’outil. Décrire clairement l’objectif, la signification de chaque paramètre, la plage de valeurs et les conditions limites est plus efficace que l’ajustement de tout paramètre d’échantillonnage.

parameters utilise JSON Schema standard pour décrire la structure des paramètres :

  • type : Généralement "object".
  • properties : Type, description, valeurs énumérées pour chaque paramètre.
  • required : Liste des noms de paramètres requis.
  • enum : Restreindre la plage de valeurs, réduit considérablement la probabilité que le modèle passe des valeurs incorrectes.

Les outils sans paramètres doivent toujours fournir un schéma vide : "parameters": { "type": "object", "properties": {} }.

Dans /v1/messages, les définitions d’outils sont des structures plates sans enveloppes externes type et function, et le champ de paramètre s’appelle input_schema.

{
"tools": [
{
"name": "get_weather",
"description": "Interroger la météo actuelle d'une ville spécifiée. Utiliser le nom complet de la ville en chinois.",
"input_schema": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "Nom de la ville, par ex. Beijing" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
]
}

La structure interne de input_schema est identique à celle de parameters d’OpenAI, tous deux JSON Schema standard. La différence n’est que dans l’enveloppe externe.

ÉlémentOpenAI (/v1/chat/completions)Claude (/v1/messages)
Enveloppe externe{ "type": "function", "function": {...} }Structure plate directe, pas d’enveloppe
Champ nom d’outilfunction.namename
Champ descriptionfunction.descriptiondescription
Champ paramètrefunction.parametersinput_schema
Schéma paramètreJSON Schema standardJSON Schema standard (identique)
Retour du modèleTableau message.tool_callsBloc tool_use dans content
Rôle de passage de résultatMessage role: "tool" indépendantBloc tool_result dans message user
Champ d’association de résultattool_call_idtool_use_id

Pour les détails complets du protocole Claude (incluant is_error, streaming input_json_delta, etc.), voir Protocole Claude Messages. Les exemples suivants sur cette page utilisent par défaut le format OpenAI.

tool_choice contrôle comment le modèle sélectionne les outils.

ValeurComportement
"auto"Le modèle décide d’appeler ou non un outil et lequel. Valeur par défaut lorsque tools est présent
"none"Interdire l’appel de tout outil, le modèle ne produit que du texte
"required"Doit appeler au moins un outil, mais le modèle choisit lequel
{ "type": "function", "function": { "name": "get_weather" } }Forcer l’appel de l’outil spécifié
{ "tool_choice": "auto" }

Le plus courant. Adapté aux scénarios où « les questions des utilisateurs nécessitent parfois des outils, parfois des réponses directes ».

{ "tool_choice": "none" }

Désactiver temporairement les outils tout en conservant les définitions. Souvent utilisé dans la phase de clôture « laisser le modèle résumer, ne pas rappeler les outils ».

{ "tool_choice": "required" }

Forcer le modèle à emprunter le chemin d’outil. Adapté aux scénarios comme l’extraction structurée qui « doivent produire des résultats structurés ».

{
"tool_choice": {
"type": "function",
"function": { "name": "get_weather" }
}
}

L’équivalent au format Claude est { "type": "tool", "name": "get_weather" }, required correspond à { "type": "any" }, none correspond à { "type": "none" }.

Un appel d’outil complet implique au moins deux tours de requêtes.

Envoyer une requête avec tools :

Fenêtre de terminal
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{ "role": "user", "content": "Quel temps fait-il à Beijing maintenant ?" }
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Interroger la météo actuelle d une ville spécifiée.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
]
}'

Lorsque le modèle détermine qu’un outil est nécessaire, finish_reason est tool_calls, message.content est null, et tool_calls contient la demande d’appel :

{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Beijing\"}"
}
}
]
},
"finish_reason": "tool_calls"
}
]
}

Notez que arguments est une chaîne JSON, pas un objet ; vous devez le parser vous-même avec JSON.parse / json.loads. Le modèle peut occasionnellement générer du JSON invalide, donc l’analyse doit être dans un try/catch.

Dispatcher par function.name vers votre logique réelle, exécuter avec les paramètres analysés :

import json
args = json.loads(tool_call["function"]["arguments"])
result = get_weather(**args) # "Beijing, ensoleillé, 23 celsius"

Ajouter le message assistant du premier tour (avec tool_calls) tel quel à messages, puis ajouter un message role: "tool" portant le résultat. tool_call_id doit correspondre exactement à l’id du premier tour :

{
"model": "gpt-5.5",
"messages": [
{ "role": "user", "content": "Quel temps fait-il à Beijing maintenant ?" },
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\": \"Beijing\"}" }
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "Beijing, ensoleillé, température 23 celsius, humidité 45%."
}
],
"tools": []
}

La requête du deuxième tour retourne un résultat en langage naturel, finish_reason revient à stop :

{
"choices": [
{
"message": {
"role": "assistant",
"content": "Beijing est actuellement ensoleillé avec une température de 23 celsius et une humidité de 45%, assez confortable."
},
"finish_reason": "stop"
}
]
}

Le modèle peut nécessiter plusieurs tours d’appels d’outils pour terminer une tâche : d’abord vérifier le numéro de commande, puis utiliser le numéro de commande pour vérifier la logistique. Chaque tour suit la boucle « le modèle retourne tool_calls → exécuter → renvoyer le résultat » jusqu’à ce que finish_reason revienne à stop. Le code de production doit être écrit comme une boucle avec une limite de tours maximum pour éviter les boucles infinies :

MAX_TURNS = 5
for _ in range(MAX_TURNS):
resp = call_model(messages)
msg = resp["choices"][0]["message"]
messages.append(msg)
if not msg.get("tool_calls"):
break # Le modèle donne la réponse finale
for tc in msg["tool_calls"]:
result = dispatch(tc) # Exécuter et retourner une chaîne
messages.append({
"role": "tool",
"tool_call_id": tc["id"],
"content": result,
})

En un tour, le modèle peut simultanément demander plusieurs outils indépendants (par exemple, vérifier la météo de Beijing et Shanghai), résultant en un tableau tool_calls multi-éléments. Vous devez ajouter un message role: "tool" correspondant pour chaque tool_call, avec tool_call_id aligné un à un ; en manquer un causera une erreur au prochain tour.

Pour désactiver le parallélisme et forcer le modèle à appeler un seul outil à la fois, ajoutez "parallel_tool_calls": false en format OpenAI, ou ajoutez "disable_parallel_tool_use": true dans tool_choice pour le format Claude.

Lorsque stream: true est défini, les paramètres d’appel d’outil sont retournés incrémentalement par morceaux.

Le delta.tool_calls de chaque chunk SSE contient index (identifie quel appel d’outil), et function.arguments est un fragment du JSON des paramètres :

data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_abc123","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]}}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"Beijing\"}"}}]}}]}
data: {"choices":[{"finish_reason":"tool_calls"}]}
data: [DONE]

Accumuler par groupes index : id et name n’apparaissent généralement que dans le premier fragment, arguments nécessite que tous les fragments soient concaténés avant l’analyse. Ne pas tenter d’analyser les états intermédiaires pendant la concaténation (c’est du JSON incomplet).

buffers = {} # index -> {"id", "name", "args"}
for chunk in stream:
for tc in chunk["choices"][0]["delta"].get("tool_calls", []):
i = tc["index"]
buf = buffers.setdefault(i, {"id": "", "name": "", "args": ""})
if tc.get("id"):
buf["id"] = tc["id"]
fn = tc.get("function", {})
if fn.get("name"):
buf["name"] = fn["name"]
if fn.get("arguments"):
buf["args"] += fn["arguments"]
# Analyser après la fin du flux
for buf in buffers.values():
args = json.loads(buf["args"])

Les paramètres d’outil en streaming au format Claude sont retournés incrémentalement via le partial_json de l’événement input_json_delta, accumulés par index puis analysés ; détails dans Protocole Claude Messages.

La plupart des modèles grand public agrégés par RouteAPI prennent en charge Tool Calling, y compris les séries OpenAI GPT, Claude, Gemini, etc. Commencez par vérifier via l’endpoint de liste des modèles que le modèle est disponible dans votre compte :

Fenêtre de terminal
curl https://api.routeapi.ai/v1/models \
-H "Authorization: Bearer $ROUTEAPI_KEY"

Cet endpoint retourne uniquement la disponibilité du modèle et les endpoints utilisables, pas la capacité Tool Calling. Pour savoir si Tool Calling est pris en charge, référez-vous à la documentation du fournisseur ou à une requête réelle avec tools.

Différents modèles varient dans les dimensions suivantes ; vérifiez dans un environnement de test avant de passer en production :

DimensionDescription
Nombre maximum d’outilsLa limite supérieure d’outils pouvant être déclarés dans une seule requête varie selon le modèle
Appel parallèleCertains modèles ne prennent pas en charge le retour de plusieurs tool_calls en un tour
Prise en charge de tool_choiceTous les modèles ne prennent pas en charge required / spécification d’outils spécifiques
Complexité des paramètresLes JSON Schema profondément imbriqués ou très volumineux peuvent être tronqués ou ignorés par certains modèles
Granularité d’incrément en streamingLa fragmentation de arguments varie entre les modèles ; doit accumuler par index

Exemple d’outil de requête météo (de bout en bout)

Section intitulée « Exemple d’outil de requête météo (de bout en bout) »

Les trois extraits de code suivants sont fonctionnellement équivalents : définir l’outil → premier tour obtient tool_calls → exécuter → deuxième tour renvoie le résultat → obtient la réponse finale.

curl (compléter manuellement deux tours) :

Fenêtre de terminal
# Premier tour : envoyer une requête avec des outils
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [{ "role": "user", "content": "Quel temps fait-il à Beijing maintenant ?" }],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Interroger la météo actuelle d une ville spécifiée.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}]
}'
# Deuxième tour : renvoyer le résultat de l'outil (tool_call_id utilise la valeur du premier tour)
curl https://api.routeapi.ai/v1/chat/completions \
-H "Authorization: Bearer $ROUTEAPI_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.5",
"messages": [
{ "role": "user", "content": "Quel temps fait-il à Beijing maintenant ?" },
{
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\": \"Beijing\"}" }
}]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "Beijing, ensoleillé, température 23 celsius, humidité 45%."
}
]
}'

Python (SDK OpenAI, complète automatiquement deux tours) :

import json
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["ROUTEAPI_KEY"],
base_url="https://api.routeapi.ai/v1",
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Interroger la météo actuelle d'une ville spécifiée. Utiliser le nom complet de la ville en chinois.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "Nom de la ville, par ex. Beijing"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
},
},
}
]
def get_weather(city: str, unit: str = "celsius") -> str:
# Remplacer par un appel réel au service météo
return f"{city}, ensoleillé, température 23 celsius, humidité 45%."
messages = [{"role": "user", "content": "Quel temps fait-il à Beijing maintenant ?"}]
# Premier tour
resp = client.chat.completions.create(
model="gpt-5.5", messages=messages, tools=tools
)
msg = resp.choices[0].message
if msg.tool_calls:
# Le message assistant doit être ajouté tel quel, sinon tool_call_id ne peut pas s'aligner
messages.append(msg)
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = get_weather(**args)
messages.append(
{"role": "tool", "tool_call_id": tc.id, "content": result}
)
# Deuxième tour
resp = client.chat.completions.create(
model="gpt-5.5", messages=messages, tools=tools
)
print(resp.choices[0].message.content)

Node.js (package openai, complète automatiquement deux tours) :

import OpenAI from 'openai';
const client = new OpenAI({
apiKey: process.env.ROUTEAPI_KEY,
baseURL: 'https://api.routeapi.ai/v1',
});
const tools = [
{
type: 'function',
function: {
name: 'get_weather',
description: 'Interroger la météo actuelle d une ville spécifiée. Utiliser le nom complet de la ville en chinois.',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: 'Nom de la ville, par ex. Beijing' },
unit: { type: 'string', enum: ['celsius', 'fahrenheit'] },
},
required: ['city'],
},
},
},
];
function getWeather(city, unit = 'celsius') {
// Remplacer par un appel réel au service météo
return `${city}, ensoleillé, température 23 celsius, humidité 45%.`;
}
const messages = [{ role: 'user', content: 'Quel temps fait-il à Beijing maintenant ?' }];
// Premier tour
let resp = await client.chat.completions.create({
model: 'gpt-5.5',
messages,
tools,
});
const msg = resp.choices[0].message;
if (msg.tool_calls) {
messages.push(msg); // Ajouter le message assistant tel quel
for (const tc of msg.tool_calls) {
const args = JSON.parse(tc.function.arguments);
const result = getWeather(args.city, args.unit);
messages.push({ role: 'tool', tool_call_id: tc.id, content: result });
}
// Deuxième tour
resp = await client.chat.completions.create({
model: 'gpt-5.5',
messages,
tools,
});
}
console.log(resp.choices[0].message.content);

Utiliser les outils comme enveloppes sécurisées pour les systèmes internes. Clé : SQL ne doit pas être généré directement par le modèle ; au lieu de cela, le modèle sélectionne les paramètres, et votre code construit des requêtes paramétrées pour éviter l’injection.

tools = [
{
"type": "function",
"function": {
"name": "query_order",
"description": "Interroger l'état et le montant de la commande par numéro de commande.",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "Numéro de commande, comme ORD-20260917-001",
}
},
"required": ["order_id"],
},
},
}
]
def query_order(order_id: str) -> str:
# Utiliser une requête paramétrée, ne jamais concaténer la sortie du modèle directement dans SQL
row = db.execute(
"SELECT status, amount FROM orders WHERE order_id = %s",
(order_id,),
).fetchone()
if row is None:
return json.dumps({"found": False})
return json.dumps({"found": True, "status": row[0], "amount": row[1]})

Les résultats d’outils sont recommandés d’être renvoyés sous forme de chaînes JSON ; les modèles peuvent analyser plus fiablement les champs structurés.

Déclarer plusieurs outils simultanément ; le modèle sélectionne selon les besoins ou appelle même en parallèle. Ici, en utilisant météo + taux de change deux outils :

tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Interroger la météo actuelle d'une ville spécifiée.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
},
{
"type": "function",
"function": {
"name": "get_exchange_rate",
"description": "Interroger le taux de change entre deux devises.",
"parameters": {
"type": "object",
"properties": {
"from_currency": {"type": "string", "description": "Comme USD"},
"to_currency": {"type": "string", "description": "Comme CNY"},
},
"required": ["from_currency", "to_currency"],
},
},
},
]
dispatch = {
"get_weather": lambda city: f"{city}, ensoleillé, 23 celsius.",
"get_exchange_rate": lambda from_currency, to_currency: f"1 {from_currency} = 7.2 {to_currency}",
}
messages = [{"role": "user", "content": "Quel temps fait-il à Beijing ? Aussi, dites-moi le taux de change USD vers CNY."}]
# Boucle pour gérer les appels d'outils multi-tours / parallèles, définir une limite pour éviter les boucles infinies
for _ in range(5):
resp = client.chat.completions.create(
model="gpt-5.5", messages=messages, tools=tools
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
break
# Lors d'appels parallèles, tool_calls est un tableau multi-éléments ; chacun doit être renvoyé
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = dispatch[tc.function.name](**args)
messages.append(
{"role": "tool", "tool_call_id": tc.id, "content": result}
)
print(messages[-1]["content"])

Tool Calling introduit trois points d’erreur potentiels : côté modèle, côté de votre code et côté service en amont.

Le modèle peut retourner du JSON invalide, ou manquer des paramètres requis, ou passer des valeurs hors plage enum. Assurez-vous de :

  • Envelopper JSON.parse / json.loads dans try/catch.
  • Après l’analyse, valider les champs requis et les plages de valeurs selon le schéma.
  • Lorsque la validation échoue, renvoyer le message d’erreur comme résultat d’outil pour laisser le modèle s’auto-corriger, plutôt que de lancer directement une exception pour terminer la conversation :
try:
args = json.loads(tc.function.arguments)
city = args["city"] # Valider le champ requis
except (json.JSONDecodeError, KeyError) as exc:
result = f"Échec de l'analyse des paramètres : {exc}. Veuillez fournir à nouveau un paramètre city valide."
else:
result = get_weather(city)
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})

Les outils sont soutenus par de vrais services qui peuvent expirer ou être indisponibles. Définir une limite de timeout pour chaque appel d’outil ; si expiration, renvoyer le résultat comme une description d’erreur claire pour laisser le modèle basculer vers une autre stratégie :

try:
result = call_service(args, timeout=5)
except TimeoutError:
result = "Timeout du service, données non récupérées, veuillez réessayer plus tard ou utiliser une autre méthode."

Ne pas effectuer de nouvelles tentatives automatiques sur les outils avec effets secondaires (passer des commandes, envoyer des e-mails), car ils peuvent s’exécuter à répétition. La conception idempotente ou vérifier avant d’écrire est plus sûr.

Les erreurs de niveau métier (commande introuvable, pas de permission) doivent également être renvoyées au modèle, plutôt que de retourner silencieusement des valeurs vides. Renvoyer des informations d’erreur structurées ; le modèle peut fournir des explications raisonnables aux utilisateurs :

messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps({"error": "order_not_found", "order_id": order_id}),
})

Le format Claude définit correspondamment "is_error": true sur le bloc tool_result, voir Protocole Claude Messages.

  • La capacité Tool Calling, le nombre maximum d’outils, la prise en charge des appels parallèles dépendent du modèle sélectionné ; vérifiez dans l’environnement de test avant de passer en production.
  • arguments est toujours une chaîne JSON, pas un objet ; doit analyser explicitement.
  • Dans les scénarios de streaming, doit accumuler les fragments arguments par index, analyser après concaténation complète.
  • Les paramètres optionnels explicitement passés comme 0 ou false sont considérés comme définis par l’utilisateur, non traités comme des valeurs par défaut et écartés.
  • Enregistrer l’ID de requête, l’ID de modèle, le code d’état et l’utilisation de jetons pour chaque requête pour faciliter le dépannage. Détails de la structure d’erreur dans Erreurs et débogage.