Aller au contenu principal

Sorties structurées avec les outils

Mis à jour le 29 juillet 2026

Objectifs

  • Comprendre comment utiliser les outils pour forcer une réponse structurée
  • Générer du JSON avec un format garanti grâce au Tool Use

Le concept

Voici un détournement du Tool Use que peu de développeurs anticipent et qui règle un problème très concret. Quand vous demandez du JSON à un modèle en langage naturel, vous obtenez souvent du JSON valide, parfois précédé d’un « Voici le résultat : », parfois enveloppé dans un bloc de code, parfois avec une clé renommée. Votre parseur casse une fois sur vingt, ce qui est intenable dans une chaîne de traitement automatisée.

Or, quand Claude demande à utiliser un outil, il produit déjà une structure parfaitement formée, contrainte par le schéma que vous avez fourni. L’astuce consiste à exploiter ce canal pour ce qu’il est — un générateur de JSON conforme — sans jamais exécuter la fonction derrière. Le procédé tient en quatre gestes : vous définissez un outil dont le schéma correspond exactement au format voulu, vous demandez à Claude de l’utiliser, vous extrayez les données structurées de sa réponse, et vous n’appelez jamais la fonction sous-jacente. L’outil est un prétexte ; le schéma est le vrai produit.

Exemple : analyse de sentiment

Imaginons une chaîne de traitement d’avis clients qui alimente un tableau de bord. Ce que vous voulez récupérer pour chaque avis ressemble à ceci.

{
  "negative_score": 0.6,
  "neutral_score": 0.3,
  "positive_score": 0.1
}

Définir l’outil “fictif”

La définition se rédige exactement comme celle d’un vrai outil : un nom, une description, un schéma. Chaque propriété devient une clé du JSON final, et la mention de la plage de valeurs dans les descriptions guide le modèle vers des scores cohérents entre eux. Le fait que print_sentiment_scores ne corresponde à aucune fonction de votre code n’a aucune importance, Claude n’exécute rien.

tools = [
    {
        "name": "print_sentiment_scores",
        "description": "Affiche les scores de sentiment pour un texte donne.",
        "input_schema": {
            "type": "object",
            "properties": {
                "positive_score": {
                    "type": "number",
                    "description": "Score de sentiment positif (0 a 1)"
                },
                "negative_score": {
                    "type": "number",
                    "description": "Score de sentiment négatif (0 a 1)"
                },
                "neutral_score": {
                    "type": "number",
                    "description": "Score de sentiment neutre (0 a 1)"
                }
            },
            "required": ["positive_score", "negative_score", "neutral_score"]
        }
    }
]

Forcer l’utilisation avec tool_choice

Vous pourriez supplier Claude dans le prompt d’utiliser l’outil, mais ce serait laisser subsister l’incertitude que l’on cherchait justement à éliminer. Le paramètre tool_choice la supprime : en le pointant sur un outil nommé, vous garantissez que la réponse contiendra ce bloc et pas autre chose.

response = client.messages.create(
    model="claude-sonnet-5",
    max_tokens=1000,
    tools=tools,
    tool_choice={"type": "tool", "name": "print_sentiment_scores"},
    messages=[{
        "role": "user",
        "content": "Analyse le sentiment de ce texte : 'Ce produit est absolument horrible, je suis très déçu.'"
    }]
)

Extraire le JSON

Côté extraction, plus de découpage de chaîne ni d’expression régulière : vous cherchez le bloc de type tool_use dans la réponse et vous lisez son attribut input, qui est déjà un dictionnaire Python.

import json

# Trouver le bloc ToolUse dans la réponse
tool_use_block = next(
    block for block in response.content if block.type == "tool_use"
)

# Les données sont directement dans input
sentiment_data = tool_use_block.input
print(json.dumps(sentiment_data, indent=2))

Résultat :

{
  "positive_score": 0.0,
  "negative_score": 0.85,
  "neutral_score": 0.15
}

Version fonction réutilisable

En pratique, on encapsule ce mécanisme une bonne fois pour toutes derrière une fonction qui prend un texte et rend un dictionnaire. Le reste de votre application n’a alors plus conscience qu’un modèle de langage est impliqué : elle appelle analyze_sentiment comme n’importe quelle bibliothèque de NLP.

def analyze_sentiment(text):
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1000,
        tools=tools,
        tool_choice={"type": "tool", "name": "print_sentiment_scores"},
        messages=[{
            "role": "user",
            "content": f"Analyse le sentiment de ce texte : '{text}'"
        }]
    )
    tool_block = next(b for b in response.content if b.type == "tool_use")
    return tool_block.input

# Utilisation
result = analyze_sentiment("J'adore ce restaurant, le service est impeccable !")
print(json.dumps(result, indent=2))

Exemple : extraction d’entités

La même mécanique s’applique à toute tâche d’extraction, et le schéma JSON peut décrire des structures plus riches que trois nombres. Pour repérer les personnes, organisations et lieux cités dans un document, on déclare trois propriétés de type array dont les items sont des chaînes. Claude renverra alors trois listes, éventuellement vides, mais toujours présentes puisqu’elles figurent dans required — ce qui vous évite les tests d’existence de clé dans le code appelant.

entity_tool = {
    "name": "print_entities",
    "description": "Affiche les entités extraites d'un texte.",
    "input_schema": {
        "type": "object",
        "properties": {
            "persons": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Liste des personnes mentionnées"
            },
            "organizations": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Liste des organisations mentionnées"
            },
            "locations": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Liste des lieux mentionnés"
            }
        },
        "required": ["persons", "organizations", "locations"]
    }
}

Transposer la technique : un traducteur multilingue

Appliquez le procédé à un troisième domaine. Écrivez une fonction translate qui, pour une phrase donnée, produit un JSON de traduction multilingue de cette forme :

{
  "english": "how much does this cost",
  "spanish": "cuanto cuesta esto?",
  "french": "combien ca coute ?",
  "japanese": "korehaikuradesuka",
  "arabic": "kam tukallif hadha?"
}

Réfléchissez d’abord au schéma : cinq propriétés de type string, toutes obligatoires, et des descriptions qui nomment la langue attendue. Le reste n’est que la répétition du motif que vous connaissez désormais.

Voir la solution
translate_tool = {
    "name": "print_translation",
    "description": "Affiche la traduction d'une phrase en plusieurs langues.",
    "input_schema": {
        "type": "object",
        "properties": {
            "english": {"type": "string", "description": "La phrase en anglais"},
            "spanish": {"type": "string", "description": "Traduction en espagnol"},
            "french": {"type": "string", "description": "Traduction en francais"},
            "japanese": {"type": "string", "description": "Traduction en japonais"},
            "arabic": {"type": "string", "description": "Traduction en arabe"}
        },
        "required": ["english", "spanish", "french", "japanese", "arabic"]
    }
}

def translate(phrase):
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1000,
        tools=[translate_tool],
        tool_choice={"type": "tool", "name": "print_translation"},
        messages=[{
            "role": "user",
            "content": f"Traduis cette phrase : '{phrase}'"
        }]
    )
    block = next(b for b in response.content if b.type == "tool_use")
    return block.input