Command Palette

Search for a command to run...

JSON vs appel d'outils programmatique avec Claude

🇬🇧EN

Les trois approches pour connecter des outils à Claude : JSON schema avec boucle manuelle, Tool Runner SDK, et appel programmatique. Comparaison, code, et retour d'expérience.

11 min de lecture
claude-apitool-callingsdkanthropicagentstypescriptpython
Schéma comparatif des trois approches d'appel d'outils Claude : JSON schema, Tool Runner SDK, et appel programmatique

Quand on construit une application avec l’API Claude, connecter des outils est la première étape vers un agent fonctionnel. Le modèle peut appeler des fonctions, interroger des bases de données, envoyer des messages. Mais la façon dont on branche ces outils change radicalement les performances, le coût et la complexité du code.

En 2026, l’API Claude propose trois approches distinctes. Cet article les compare avec du code, des chiffres, et le retour d’expérience de quelqu’un qui les utilise toutes les trois.

Le problème : les allers-retours

L’approche classique du tool calling fonctionne en boucle : le modèle demande un outil, votre serveur l’exécute, renvoie le résultat, le modèle raisonne sur le résultat, demande un autre outil, et ainsi de suite. Chaque itération est un aller-retour complet avec le modèle.

Votre serveurboucle de dispatchmessagestool_useClaude APIinférence complètex10 outils = x10 inférencestokens cumulés dans le contextelatence proportionnelleChaque appel d’outil nécessite une inférence complète du modèle

Le problème devient concret dès que le nombre d’outils augmente. Vérifier les dépenses de 20 employés ? 20 allers-retours. Chaque résultat intermédiaire s’empile dans le contexte. D’après les mesures d’Anthropic, les définitions d’outils seules peuvent consommer plus de 134 000 tokens avant toute conversation dans un setup multi-serveurs.

Approche 1 : JSON schema + boucle manuelle

C’est l’approche historique. On définit chaque outil avec un schéma JSON, on envoie la requête à l’API, on parse la réponse tool_use, on exécute l’outil, et on renvoie le résultat.

import anthropic, json

client = anthropic.Anthropic()

tools = [{
    "name": "query_database",
    "description": "Exécute une requête SQL. Renvoie les lignes en JSON.",
    "input_schema": {
        "type": "object",
        "properties": {
            "sql": {"type": "string", "description": "Requête SQL"}
        },
        "required": ["sql"]
    }
}]

messages = [{"role": "user", "content": "Quel est le CA par région ?"}]

while True:
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4096,
        tools=tools,
        messages=messages,
    )

    if response.stop_reason == "end_turn":
        break

    for block in response.content:
        if block.type == "tool_use":
            result = execute_sql(block.input["sql"])
            messages.append({"role": "assistant", "content": response.content})
            messages.append({
                "role": "user",
                "content": [{
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result)
                }]
            })

La boucle while True est la partie critique. C’est à vous de gérer le cycle requête/réponse, le parsing des blocs tool_use, l’accumulation des messages, et les cas d’erreur.

Avantages : contrôle total sur chaque étape. On peut logger, filtrer les résultats, injecter de la logique métier entre chaque appel.

Inconvénients : beaucoup de boilerplate. Chaque résultat d’outil transite par le modèle, même si c’est juste un intermédiaire. Sur 10 appels d’outils, ça fait 10 inférences complètes.

Approche 2 : Tool Runner SDK

Le Tool Runner est un helper des SDK officiels Anthropic (Python, TypeScript, Go, Java, C#, PHP, Ruby). Il automatise la boucle agentique : définition des outils, exécution, gestion de l’état conversationnel, et validation des types.

En Python, le décorateur @beta_tool génère le schéma JSON à partir des type hints et du docstring :

from anthropic import Anthropic, beta_tool

client = Anthropic()

@beta_tool
def query_database(sql: str) -> str:
    """Exécute une requête SQL sur la base de ventes.

    Args:
        sql: Requête SQL à exécuter
    """
    rows = db.execute(sql)
    return json.dumps(rows)

runner = client.beta.messages.tool_runner(
    model="claude-sonnet-5",
    max_tokens=4096,
    tools=[query_database],
    messages=[{"role": "user", "content": "Quel est le CA par région ?"}],
)

for message in runner:
    print(message)

En TypeScript, deux options : betaZodTool avec validation Zod (recommandé), ou betaTool avec un schéma JSON classique :

import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const client = new Anthropic();

const queryDatabase = betaZodTool({
  name: "query_database",
  description: "Exécute une requête SQL sur la base de ventes",
  inputSchema: z.object({
    sql: z.string().describe("Requête SQL à exécuter"),
  }),
  run: async (input) => {
    const rows = await db.execute(input.sql);
    return JSON.stringify(rows);
  },
});

const result = await client.beta.messages.toolRunner({
  model: "claude-sonnet-5",
  max_tokens: 4096,
  tools: [queryDatabase],
  messages: [{ role: "user", content: "Quel est le CA par région ?" }],
});

Le Tool Runner gère automatiquement la boucle : quand Claude demande un outil, le runner l’exécute et renvoie le résultat. Plus de while True, plus de parsing manuel des blocs tool_use.

Avantages : moins de code, type safety avec Zod ou les type hints Python, gestion d’erreur intégrée.

Inconvénients : même modèle d’exécution que l’approche manuelle sous le capot. Chaque appel d’outil reste un aller-retour avec le modèle. Le gain est en DX, pas en performance.

Approche 3 : appel programmatique

L’appel programmatique (programmatic tool calling) change le modèle d’exécution. Au lieu de demander un outil à la fois via l’API, Claude écrit du code Python qui appelle vos outils directement dans un conteneur d’exécution de code. Les résultats intermédiaires restent dans le conteneur et ne remontent dans le contexte du modèle que lorsque Claude les y envoie explicitement.

Votre serveurtool_resultClaude API + conteneur codecode Pythonboucle + filtragevos outilsasync Python1 seule inférence - Claude écrit le codeN appels d’outils dans le conteneurseul le résultat final rejoint le contexte

Pour activer cette approche, il faut deux choses : inclure l’outil code_execution dans la requête, et ajouter allowed_callers sur les outils que Claude peut appeler depuis le code :

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=4096,
    messages=[{
        "role": "user",
        "content": "Compare le CA des régions Ouest, Est et Centre"
    }],
    tools=[
        {"type": "code_execution_20260120", "name": "code_execution"},
        {
            "name": "query_database",
            "description": "Exécute une requête SQL. Renvoie des lignes JSON.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "sql": {"type": "string", "description": "Requête SQL"}
                },
                "required": ["sql"]
            },
            "allowed_callers": ["code_execution_20260120"]
        }
    ],
)

Claude génère alors un script Python qui appelle query_database en boucle ou en parallèle via asyncio.gather, filtre les résultats, et ne remonte que le résumé :

import json, asyncio

results = {}
for region in ["Ouest", "Est", "Centre"]:
    rows = json.loads(await query_database({
        "sql": f"SELECT SUM(revenue) as total FROM sales WHERE region = '{region}'"
    }))
    results[region] = rows[0]["total"]

best = max(results, key=results.get)
print(f"Région avec le plus haut CA : {best} ({results[best]} EUR)")
print(f"Détail : {json.dumps(results)}")

Au lieu de 3 allers-retours modèle, une seule inférence a produit le code, et les 3 appels d’outil s’exécutent dans le conteneur. Le modèle ne reçoit que le print() final.

Les outils sont exposés comme des fonctions asynchrones Python. Le champ allowed_callers contrôle qui peut appeler chaque outil :

ValeurComportement
["direct"]Appel classique via l’API (défaut)
["code_execution_20260120"]Appel uniquement depuis le code
["direct", "code_execution_20260120"]Les deux modes

Anthropic recommande de choisir un seul mode par outil pour éviter l’ambiguïté.

Avantages : réduction massive des tokens et de la latence. Les benchmarks BrowseComp et DeepSearchQA montrent +11% de performance et -24% de tokens en entrée. Sur des tâches de recherche complexes, la consommation moyenne passe de 43 588 à 27 297 tokens, soit une réduction de 37%.

Inconvénients : nécessite l’outil code_execution (beta). Le conteneur a une durée de vie limitée (~5 minutes d’inactivité). Pas disponible sur Amazon Bedrock ni Google Cloud. L’outil n’est pas un verrou de sécurité : allowed_callers guide Claude mais ne bloque pas strictement les appels directs.

Comparaison

CritèreJSON + boucleTool Runner SDKProgrammatique
Inférences par N outilsNN1 + retours
Tokens intermédiairesDans le contexteDans le contexteDans le conteneur
Complexité du codeHauteBasseMoyenne
Contrôle par étapeTotalLimitéVia code généré
LatenceN x inférenceN x inférence1 inférence + exec
Type safetyManuelleZod / type hintsN/A (code généré)
DisponibilitéGA, tous cloudsBeta, tous cloudsBeta, API directe

Quand utiliser quoi

JSON + boucle manuelle quand vous avez besoin d’un contrôle fin entre chaque appel d’outil. Par exemple : un workflow avec validation humaine entre les étapes, du logging détaillé, ou de la logique métier conditionnelle. C’est aussi le seul choix si vous ciblez Bedrock ou Google Cloud sans le Tool Runner.

Tool Runner SDK pour la majorité des cas. Le code est plus propre, la validation des types est automatique, et le comportement est identique à la boucle manuelle sous le capot. C’est l’approche que j’utilise par défaut dans mes projets TypeScript et Python.

Appel programmatique quand le nombre d’outils par requête est élevé ou que les résultats intermédiaires sont volumineux. L’exemple typique : agréger des données de 20 sources, filtrer, et ne remonter que le résumé. C’est aussi l’approche la plus efficace pour les tâches de recherche où Claude doit explorer et trier avant de conclure.

On peut combiner les approches. Un outil marqué ["direct"] sera appelé via la boucle classique, tandis qu’un autre marqué ["code_execution_20260120"] sera orchestré par le code. Sur le même projet.

En pratique

Dans CodeRift, j’utilise le Tool Runner pour les agents de review : chaque agent a 2-3 outils (lire un fichier, chercher des symboles, poster un commentaire) et le nombre d’appels est prévisible. Le Tool Runner élimine le boilerplate sans sacrifier la visibilité.

Pour les tâches d’exploration de données dans mes workflows IronFlow, l’appel programmatique serait le bon choix : un agent qui doit scanner 50 projets GitLab, filtrer les MR ouvertes, et agréger les stats. Le gros du travail se fait dans le conteneur, et seul le résumé arrive dans le contexte.

L’approche JSON manuelle, je la garde pour les cas où le Tool Runner n’est pas disponible (SDK non supporté, ou intégration avec un framework tiers qui gère sa propre boucle).

Le choix n’est pas définitif. Commencer avec le Tool Runner, migrer vers le programmatique quand la facture tokens le justifie.