Outils et function calling : brancher le modèle sur tes données
Le modèle ne peut pas lire ta base. Mais tu peux lui donner un outil pour la lire, et il saura quand s'en servir. Tout se joue dans la description de l'outil.
Temps de lecture : 15 min | Niveau : Avancé
Ce que tu sauras faire après
- Expliquer la boucle appel / exécution / résultat sans te tromper d'acteur.
- Écrire une définition d'outil complète :
name,description,input_schema. - Reconnaître une description d'outil trop vague et la réparer.
- Choisir entre laisser le modèle décider et forcer un appel d'outil.
- Renvoyer une erreur d'outil proprement, sans casser la conversation.
1. Le principe, sans jargon
Tool use (aussi appelé function calling) veut dire : tu décris à l'avance des fonctions que le modèle peut demander à exécuter.
Point crucial et souvent mal compris : le modèle n'exécute rien. Il te renvoie une demande structurée. C'est ton code qui exécute et qui renvoie le résultat.
Définition de la doc : "Tool use (also called function calling) lets Claude call functions that you define or that Anthropic provides. Claude determines when to call a tool based on the user's request and the tool's description. It then returns a structured call that your application executes (client tools) or that Anthropic executes (server tools)."
Deux familles :
- Client tools : ton code les exécute. Ta base de données, ton API interne, ton système de fichiers.
- Server tools : exécutés par Anthropic. Recherche web, exécution de code en bac à sable, récupération de page web.
Dans ton métier, ce sont les client tools qui t'intéressent : query_database, run_dbt_model, get_airflow_dag_status, read_schema.
2. La boucle appel / résultat
Quatre temps. Toujours les mêmes.
1. TOI -> API : la question + la liste des outils disponibles
2. API -> TOI : stop_reason = "tool_use", avec un bloc tool_use
{ name: "query_database", input: {...}, id: "toolu_01..." }
3. TOI : ton code execute vraiment la requete
4. TOI -> API : la conversation + un bloc tool_result portant le meme tool_use_id
5. API -> TOI : la reponse finale en texteEn Python, ça donne ça. Note bien : deux appels API, pas un.
import anthropic
client = anthropic.Anthropic()
outils = [
{
"name": "get_weather",
"description": "Get the current weather for a given location.",
"input_schema": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City and state, e.g. San Francisco, CA"}
},
"required": ["location"],
},
}
]
messages = [{"role": "user", "content": "Quel temps fait-il a San Francisco ?"}]
# 1er appel : le modele demande l'outil
reponse = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=outils,
messages=messages,
)
appel = next(b for b in reponse.content if b.type == "tool_use")
# ton code execute reellement
resultat = ma_fonction_meteo(appel.input["location"])
# 2e appel : tu renvoies le resultat
messages += [
{"role": "assistant", "content": reponse.content},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": appel.id, "content": resultat}
]},
]
finale = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
tools=outils,
messages=messages,
)Trois détails à ne pas rater :
- Le
tool_resultpart dans un message de rôleuser. C'est contre-intuitif, mais c'est la structure de l'API. - Le
tool_use_iddoit correspondre exactement à l'idreçu. - Tu dois repasser la liste des outils à chaque appel.
Si tu ne veux pas écrire cette boucle toi-même, les SDK proposent un Tool Runner qui exécute tes fonctions et renvoie les résultats automatiquement.
3. L'anatomie d'une définition d'outil
Quatre champs, dont un optionnel.
| Champ | Rôle | Contrainte |
|---|---|---|
name | Le nom de la fonction | Doit matcher ^[a-zA-Z0-9_-]{1,128}$ |
description | Le champ qui décide de tout | Texte libre, détaillé |
input_schema | Les paramètres, en JSON Schema | Objet JSON Schema valide |
input_examples | (Optionnel) exemples d'entrées valides | Chaque exemple doit valider le schéma |
Exemple minimal officiel :
{
"name": "get_weather",
"description": "Get the current weather in a given location",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g. San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The unit of temperature, either 'celsius' or 'fahrenheit'"
}
},
"required": ["location"]
}
}4. La description : le facteur numéro un
La doc ne laisse aucune ambiguïté : "Provide extremely detailed descriptions. This is by far the most important factor in tool performance."
Elle liste ce que la description doit contenir :
- Ce que l'outil fait.
- Quand il doit être utilisé, et quand il ne doit pas l'être.
- Ce que chaque paramètre veut dire et comment il change le comportement.
- Les limites et pièges, y compris ce que l'outil ne renvoie pas.
Et elle donne une cible chiffrée : "Aim for at least 3–4 sentences for each tool description, more if the tool is complex."
L'article d'ingénierie d'Anthropic va plus loin et dit que des retouches précises sur les descriptions d'outils ont suffi à faire passer un modèle en tête d'un benchmark de code. La description d'outil est un prompt. Traite-la comme tel.
Exemple officiel : bonne description
{
"name": "get_stock_price",
"description": "Retrieves the current stock price for a given ticker symbol. The ticker symbol must be a valid symbol for a publicly traded company on a major US stock exchange like NYSE or NASDAQ. The tool will return the latest trade price in USD. It should be used when the user asks about the current or most recent price of a specific stock. It will not provide any other information about the stock or company.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {
"type": "string",
"description": "The stock ticker symbol, e.g. AAPL for Apple Inc."
}
},
"required": ["ticker"]
}
}Exemple officiel : mauvaise description
{
"name": "get_stock_price",
"description": "Gets the stock price for a ticker.",
"input_schema": {
"type": "object",
"properties": {
"ticker": {"type": "string"}
},
"required": ["ticker"]
}
}La doc commente : la bonne description dit quoi, quand, ce qui est retourné et ce que signifie le paramètre. La mauvaise laisse le modèle avec des questions ouvertes.
5. Cas concret : query_database
C'est l'outil que tu vas écrire en premier. On le fait deux fois.
AVANT - description trop vague
{
"name": "query_database",
"description": "Interroge la base de donnees.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
}Ce qui va se passer, dans l'ordre :
- Le modèle ne sait pas quel dialecte écrire. Il te sort du T-SQL sur BigQuery.
- Il ne sait pas quelles tables existent. Il invente
dim_customersalors que la table s'appelledim_client. - Il ne sait pas s'il a le droit d'écrire. Il tente un
DELETE. - Il ne sait pas combien de lignes reviennent. Il lance un
SELECT *sur 4 To. - Il ne sait pas quand ne pas utiliser l'outil. Il l'appelle pour répondre à "c'est quoi une CTE ?".
Cinq problèmes, une seule cause : la description.
APRÈS - description correcte
{
"name": "query_database",
"description": "Execute une requete SELECT en lecture seule sur l'entrepot BigQuery de production (projet 'acme-dwh', dataset 'marts') et renvoie les lignes au format JSON. Utilise cet outil des que la reponse depend de donnees reelles : chiffres, volumes, dates, listes de valeurs existantes. N'utilise PAS cet outil pour des questions generales sur le SQL, pour expliquer un concept, ou quand la reponse est deja dans la conversation. Le dialecte est GoogleSQL (BigQuery standard), pas Legacy SQL et pas T-SQL. Seules les instructions SELECT et WITH sont autorisees : toute requete contenant INSERT, UPDATE, DELETE, MERGE, CREATE ou DROP est rejetee et renvoie une erreur. La requete doit etre qualifiee en `dataset.table`. Chaque appel est limite a 1000 lignes et a 50 Go scannes ; au-dela, l'outil renvoie une erreur et tu dois ajouter un filtre sur la colonne de partition. L'outil ne renvoie PAS le schema des tables : appelle d'abord 'describe_table' si tu n'es pas sur des colonnes disponibles.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "La requete GoogleSQL complete. Tables qualifiees en dataset.table, par exemple 'marts.fct_commandes'. Inclure toujours un filtre sur la colonne de partition (date_commande) quand la table est partitionnee."
},
"max_rows": {
"type": "integer",
"description": "Nombre maximum de lignes a renvoyer, entre 1 et 1000. Par defaut 100. Mets une valeur basse quand tu veux seulement verifier la forme du resultat.",
"minimum": 1,
"maximum": 1000
},
"dry_run": {
"type": "boolean",
"description": "Si true, la requete n'est pas executee : l'outil renvoie seulement le volume estime de donnees scannees et la validite syntaxique. Utilise dry_run=true avant toute requete que tu juges couteuse."
}
},
"required": ["query"]
},
"input_examples": [
{"query": "SELECT COUNT(*) AS n FROM marts.fct_commandes WHERE date_commande >= '2026-09-01'", "max_rows": 1},
{"query": "SELECT canal, SUM(montant_ht) AS ca FROM marts.fct_commandes WHERE date_commande BETWEEN '2026-09-01' AND '2026-09-30' GROUP BY canal", "max_rows": 20},
{"query": "SELECT * FROM marts.fct_commandes WHERE date_commande >= '2026-01-01'", "dry_run": true}
]
}Pourquoi c'est mieux, point par point
| Ce qu'on a ajouté | Le problème que ça résout |
|---|---|
| Le dialecte explicite | Plus de T-SQL sur BigQuery |
| Le projet et le dataset | Le modèle qualifie correctement |
| "N'utilise PAS pour..." | Plus d'appel inutile sur une question de cours |
| La liste des verbes interdits | Plus de tentative d'écriture |
| La limite de 50 Go + partition | Plus de scan complet |
| "Ne renvoie PAS le schéma" | Le modèle appelle describe_table avant |
dry_run | Le modèle peut estimer avant d'exécuter |
input_examples | Le modèle voit la forme attendue |
Le paragraphe le plus rentable est celui qui dit ce que l'outil ne fait pas. C'est exactement ce que recommande la doc : "Any important caveats or limitations, such as what information the tool does not return."
6. Les cinq erreurs classiques
Erreur 1 : description trop courte
Tu viens de voir le cas. Trois à quatre phrases minimum, plus si l'outil est complexe.
Erreur 2 : trop d'outils granulaires
Conseil de la doc : "Consolidate related operations into fewer tools. Rather than creating a separate tool for every action (create_pr, review_pr, merge_pr), group them into a single tool with an action parameter."
Mauvais : get_dag_status, get_dag_last_run, get_dag_failures, get_dag_schedule. Bon : un outil airflow_dag_info avec un paramètre info_type en enum.
Erreur 3 : pas de namespace
Dès que tes outils touchent plusieurs systèmes, préfixe par le service : bigquery_query, airflow_trigger_dag, dbt_run_model. La doc le recommande, et l'article d'ingénierie aussi : "namespacing tools by service (e.g., asana_search, jira_search) ... can help agents select the right tools at the right time."
Erreur 4 : réponses d'outil trop bavardes
Conseil de la doc : "Design tool responses to return only high-signal information. Return semantic, stable identifiers (for example, slugs or UUIDs) rather than opaque internal references, and include only the fields Claude needs."
Concrètement : ne renvoie pas le dump JSON complet de l'API Airflow. Renvoie l'état, la date, et le message d'erreur. Le reste remplit le contexte pour rien.
Erreur 5 : oublier de décrire les paramètres
Chaque propriété du input_schema mérite sa propre description. {"ticker": {"type": "string"}} ne dit rien. Utilise aussi enum, minimum, maximum : ce sont des contraintes que le modèle lit.
7. Contrôler le déclenchement
Par le prompt
La doc dit que la frontière est pilotable : une consigne légère comme "Use the tools to investigate before responding." augmente l'usage ; "Always call a tool first before responding." pousse plus fort ; "Use your judgment about whether to call a tool or respond directly." le rend plus conservateur.
Autre point utile : les modèles récents suivent les instructions au pied de la lettre. "Can you suggest some changes" produit des suggestions, pas des modifications. Si tu veux une action, écris "Change this function...".
Par le paramètre tool_choice
Quatre valeurs :
auto: le modèle décide. Défaut quand des outils sont fournis.any: il doit appeler un outil, lequel est son choix.tool: il doit appeler cet outil précis.none: aucun outil. Défaut quand aucun outil n'est fourni.
Restrictions importantes en 2026 :
- Avec l'ancien extended thinking manuel (
thinking: {type: "enabled"}),anyettoolrenvoient une erreur. - Sur Claude Opus 5.5, Fable 5.1 et Mythos 5.1,
anyettoolrenvoient une erreur 400 dans tous les cas. Il faut utiliserautoavec strict tool use (strict: true) ou les sorties structurées.
Note complémentaire de la doc : quand tool_choice vaut any ou tool, l'API force le début de réponse, donc le modèle n'écrit pas de phrase d'introduction avant le bloc tool_use, même si tu le lui demandes.
Appels parallèles
Les modèles récents appellent plusieurs outils en parallèle quand les appels sont indépendants. Tu peux fiabiliser ce comportement avec une consigne. Extrait du prompt officiel :
<use_parallel_tool_calls>
If you intend to call multiple tools and there are no dependencies between the tool
calls, make all of the independent tool calls in parallel. ... However, if some tool
calls depend on previous calls to inform dependent values like the parameters, do NOT
call these tools in parallel and instead call them sequentially. Never use placeholders
or guess missing parameters in tool calls.
</use_parallel_tool_calls>Utile quand tu veux lire quatre tables d'un coup. À l'inverse, si tes appels parallèles saturent ta base, demande une exécution séquentielle.
8. Gérer les erreurs
Quand ton outil échoue, ne renvoie pas une exception brute. Renvoie un tool_result avec is_error et un message que le modèle peut utiliser pour se corriger.
try:
lignes = executer_bigquery(appel.input["query"], appel.input.get("max_rows", 100))
contenu, erreur = json.dumps(lignes, ensure_ascii=False), False
except VolumeTropGrand as e:
contenu = (
f"Erreur : la requete scannerait {e.go} Go, la limite est 50 Go. "
f"Ajoute un filtre sur la colonne de partition date_commande, "
f"ou relance avec dry_run=true pour estimer avant."
)
erreur = True
except ColonneInconnue as e:
contenu = (
f"Erreur : la colonne '{e.colonne}' n'existe pas dans {e.table}. "
f"Colonnes disponibles : {', '.join(e.colonnes_reelles)}."
)
erreur = True
bloc_resultat = {
"type": "tool_result",
"tool_use_id": appel.id,
"content": contenu,
"is_error": erreur,
}Regarde le deuxième cas : tu renvoies la liste des colonnes réelles. Le modèle corrige tout seul au tour suivant. Un message d'erreur d'outil est un prompt déguisé.
9. Ton template prêt à copier
Template A - Définition d'outil complète
{
"name": "[SERVICE]_[ACTION]",
"description": "[PHRASE_1 : ce que fait l'outil, sur quel systeme precis]. [PHRASE_2 : quand l'utiliser - 'Utilise cet outil des que ...']. [PHRASE_3 : quand NE PAS l'utiliser - 'N'utilise PAS cet outil pour ...']. [PHRASE_4 : ce qui est renvoye et dans quel format]. [PHRASE_5 : limites, quotas, ce que l'outil ne renvoie PAS, et quel autre outil appeler dans ce cas].",
"input_schema": {
"type": "object",
"properties": {
"[PARAM_1]": {
"type": "[string|integer|boolean|array|object]",
"description": "[CE_QUE_C_EST + FORMAT_ATTENDU + EXEMPLE_CONCRET]"
},
"[PARAM_2]": {
"type": "string",
"enum": ["[VALEUR_1]", "[VALEUR_2]"],
"description": "[CE_QUE_CHAQUE_VALEUR_CHANGE]"
}
},
"required": ["[PARAM_OBLIGATOIRE]"]
},
"input_examples": [
{"[PARAM_1]": "[CAS_SIMPLE]"},
{"[PARAM_1]": "[CAS_COMPLET]", "[PARAM_2]": "[VALEUR_1]"}
]
}Template B - Prompt système qui encadre les outils
Tu es [ROLE] et tu travailles sur [SYSTEME].
Regles d'utilisation des outils :
- Utilise les outils pour investiguer avant de repondre. Ne devine jamais une
valeur qui existe dans [LA_SOURCE_DE_VERITE].
- Avant d'ecrire une requete sur une table que tu ne connais pas, appelle
[OUTIL_DE_SCHEMA].
- Si un outil renvoie une erreur, lis le message, corrige, et reessaie une fois.
Si la deuxieme tentative echoue, explique le blocage a l'utilisateur.
- [ACTION_DANGEREUSE, ex: declencher un DAG en production] : demande toujours
confirmation avant.Template C - Auditer un outil existant
Voici la definition d'un de mes outils. Audite-la.
<definition>
[COLLE_TON_JSON]
</definition>
<contexte>
Systeme reel : [DECRIS_LE_SYSTEME]
Ce que l'outil fait vraiment : [DECRIS_LE_COMPORTEMENT_REEL]
Limites reelles : [QUOTAS, DROITS, VOLUMES]
</contexte>
Pour chaque point, dis si c'est present et suffisant :
1. Ce que l'outil fait
2. Quand l'utiliser
3. Quand NE PAS l'utiliser
4. Ce qui est renvoye, dans quel format
5. Ce qui n'est PAS renvoye
6. Description de chaque parametre
7. Contraintes machines (enum, minimum, maximum)
Puis reecris la definition complete corrigee.À retenir
- Le modèle n'exécute jamais rien : il demande, ton code exécute, tu renvoies un
tool_result. - La
descriptionest de loin le facteur numéro un. Vise au minimum 3 à 4 phrases. - Dis toujours quand ne pas utiliser l'outil et ce qu'il ne renvoie pas.
- Regroupe les opérations proches dans un outil unique avec un paramètre
action, et préfixe par service. tool_choicevautauto,any,toolounone;anyettoolsont refusés sur Opus 5.5, Fable 5.1 et Mythos 5.1.- Un message d'erreur d'outil est un prompt : donne au modèle ce qu'il faut pour se corriger.
Sources
- Tool use with Claude - Anthropic
- Define tools - Anthropic
- Writing effective tools for AI agents - Anthropic Engineering
- Prompting best practices - Anthropic
- Thinking - Anthropic
- Structured outputs - Anthropic
- anthropics/claude-cookbooks - code d'exemple sur les définitions d'outils et la boucle appel / résultat :
tool_use/calculator_tool.ipynb,tool_use/tool_choice.ipynb,tool_use/parallel_tools.ipynb - anthropics/courses - le cours tool use en six notebooks, de l'outil unique au chatbot multi-outils :
tool_use/(branchemaster, dépôt archivé depuis le 15 septembre 2026)