Sorties structurées et appels de fonctions : les schémas adaptés à la production
Avancé13 min de lectureIA pour les entreprises

Sorties structurées et appels de fonctions : les schémas adaptés à la production

Les sorties structurées et les appels de fonctions permettent de passer d’un « LLM qui génère du texte » à un « système qui exécute des tâches ». En production, les schémas, la gestion des erreurs, l’idempotence et la dégradation progressive comptent davantage que le seul mode JSON.

Ce que vous saurez faire

Des sorties structurées et des appels de fonctions adaptés à la production exigent davantage que le mode JSON : schémas stricts, sémantique d’erreur explicite, idempotence, gestion progressive des défaillances et boucles d’analyse des résultats d’outils qui détectent les erreurs du modèle avant leur propagation.

AI Expert TeamPublié: 15 mai 2026
Enregistré uniquement dans ce navigateur.
Dans cet article

Le passage d’un « chatbot LLM » à un « système fondé sur un LLM qui exécute des tâches » repose sur les sorties structurées et les appels de fonctions. Le LLM cesse alors de produire uniquement du texte : il génère des données, prend des décisions et s’intègre au reste de votre infrastructure.

En production, une « sortie structurée » ne signifie pas que « le mode JSON a fonctionné une fois pendant les tests ». Elle suppose une chaîne de traitement robuste qui gère la variabilité du modèle, les résultats malformés, les défaillances partielles, l’évolution des schémas et le fait que les LLM ne suivent pas toujours les instructions.

Cet article présente les schémas de conception qui résistent réellement aux conditions de production. Nous supposons que vous connaissez les bases — tool_choice d’OpenAI, l’utilisation d’outils d’Anthropic et les contraintes de schéma JSON — et approfondissons les mécanismes qui rendent ces systèmes fiables.

Les deux modes

Deux capacités liées mais distinctes :

Sortie structurée : le LLM produit un résultat conforme à un schéma, généralement JSON. Utilisez-la lorsque la réponse doit être exploitable par un programme.

Appel de fonction ou d’outil : le LLM reçoit un ensemble de fonctions disponibles, décide laquelle appeler si nécessaire et génère les paramètres de l’appel. Le système hôte exécute la fonction et renvoie le résultat. Le LLM peut ensuite appeler d’autres fonctions ou produire une réponse finale.

Les API de modèles exposent généralement ces fonctionnalités via :

  • Un paramètre response_format, ou son équivalent, qui reçoit un schéma JSON pour les sorties structurées simples — les « Structured Outputs » d’OpenAI, responseSchema de Google Gemini, etc.
  • Un tableau tools qui décrit les fonctions disponibles, accompagné d’une réponse d’appel d’outil lorsque l’une d’elles est invoquée. L’API d’utilisation d’outils d’Anthropic permet également de contraindre la sortie à un schéma : vous définissez un seul outil avec le schéma voulu et forcez le modèle à l’appeler.

Les deux mécanismes fonctionnent et sont liés. Un « appel de fonction » est essentiellement une sortie structurée dont le schéma correspond à la signature de la fonction.

Schéma 1 : Des schémas stricts et explicites

Le principal gain de fiabilité provient de vos schémas.

Un schéma permissif :

{
  "type": "object",
  "properties": {
    "category": { "type": "string" },
    "priority": { "type": "string" }
  }
}

Un schéma strict :

{
  "type": "object",
  "properties": {
    "category": {
      "type": "string",
      "enum": ["billing", "technical", "account", "feature_request", "complaint"],
      "description": "The ticket category. Use 'technical' for product bugs and 'account' for login/password issues."
    },
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high", "urgent"],
      "description": "Use 'urgent' only for outages or business-critical impact. 'high' for blocking issues on important customers. 'medium' for standard impact. 'low' for nice-to-haves."
    }
  },
  "required": ["category", "priority"],
  "additionalProperties": false
}

Le schéma strict :

  • Restreint les valeurs aux ensembles d’énumérations connus (pas de dérive libre).
  • Inclut des descriptions qui agissent comme des prompts intégrés (le modèle les utilise).
  • Exige des champs (vous n’obtenez pas de réponses partielles).
  • Interdit les champs supplémentaires et donc les clés inventées.

En production, chaque champ doit être décrit, chaque énumération explicitée et chaque champ obligatoire signalé. C’est le principe du « schéma comme prompt » : le schéma participe directement à l’ingénierie des prompts.

Schéma 2 : Génération contrainte

Les grands fournisseurs prennent désormais en charge la génération contrainte : le décodage oblige le modèle à ne produire que des résultats valides.

  • OpenAI : response_format: { type: "json_schema", json_schema: { ..., strict: true } }
  • Anthropic : Outils avec des schémas stricts.
  • Open source : outlines, lm-format-enforcer, jsonformer et le décodage fondé sur la grammaire de vLLM.

Utilisez toujours ces mécanismes. Ils éliminent toute une catégorie d’échecs : JSON malformé, champs inventés ou champs obligatoires manquants. Leur impact sur les performances est négligeable.

Lorsque la génération contrainte n’est pas disponible pour certains modèles open source ou certaines configurations, la validation suivie d’une nouvelle tentative constitue l’alternative (voir le schéma 4).

Schéma 3 : Gestion des versions de schémas

Les schémas évoluent. Vous ajoutez des champs. Vous dépréciez des champs. Vous modifiez des énumérations.

Un changement de schéma est un changement de code. Il doit être :

  • Versionné. Marquez chaque schéma avec un numéro de version.
  • Testé. L’ensemble de tests s’exécute contre le nouveau schéma avant le déploiement.
  • Communiqué. Les consommateurs en aval de la sortie connaissent le changement.
  • Rétrocompatible lorsque cela est possible. Ajoutez de nouveaux champs facultatifs ; ne supprimez pas les champs obligatoires.

Une méthode efficace consiste à stocker les schémas sous forme de types TypeScript ou de modèles Pydantic, à les gérer dans le système de contrôle de version et à générer les schémas JSON à partir de ces définitions. Les types servent alors à la fois à l’API du modèle et au code de l’application.

class TicketClassificationV2(BaseModel):
    category: Literal["billing", "technical", "account", "feature_request", "complaint"]
    priority: Literal["low", "medium", "high", "urgent"]
    confidence: float = Field(ge=0, le=1, description="Confidence in this classification, 0-1")
    needs_human_review: bool = Field(description="True if any field has low confidence or unusual signal")
    reasoning: str = Field(description="Brief reasoning for the classification, especially for non-obvious cases")

Un modèle Pydantic définit le schéma, valide le résultat et sert de type dans le code Python : une source de référence unique.

Schéma 4 : Validation et nouvelle tentative

Même avec la génération contrainte, validez le résultat avant de l’utiliser :

from pydantic import ValidationError

def call_with_validation(prompt, schema, max_retries=2):
    for attempt in range(max_retries + 1):
        response = llm_call(prompt, response_format=schema)
        try:
            parsed = schema.model_validate_json(response.content)
            return parsed
        except ValidationError as e:
            if attempt < max_retries:
                prompt = build_retry_prompt(prompt, response.content, e)
                continue
            raise

Le prompt de nouvelle tentative doit contenir les instructions initiales, le résultat précédent du modèle et une description précise de l’erreur :

Your previous response had a validation error:
{error message}

Your previous output:
{previous output}

Please correct the issue and produce a valid response.

Les nouvelles tentatives fonctionnent étonnamment bien : une seule suffit généralement à corriger une erreur du modèle.

Limites : n’effectuez pas de tentatives indéfiniment — deux ou trois au maximum. Ne relancez pas le traitement pour des erreurs étrangères à la validation, telles qu’une limitation de débit ou un filtre de contenu. Journalisez les tentatives afin d’en surveiller le taux ; une hausse signale une dérive du modèle ou des problèmes de prompt.

Schéma 5 : Analyse des résultats

Pour les appels de fonctions à haut risque, faites en sorte que le modèle réfléchisse aux résultats des outils avant de les utiliser.

Boucle simple :

1. Call LLM with tools available.
2. Model decides to call tool X.
3. Execute X.
4. Pass result back to model.
5. Model produces final response.

Boucle de réflexion :

1. Call LLM with tools available.
2. Model decides to call tool X.
3. Execute X.
4. Pass result back to model.
5. Model evaluates: does this result match what I expected? Should I act on it?
6. If yes, model produces final response. If no, model calls another tool or asks for clarification.

Cette boucle permet de détecter des cas tels que :

  • L’outil a retourné 0 résultats alors qu’il devrait avoir retourné des données → le modèle reconnaît le cas vide.
  • L’outil a retourné une erreur → le modèle la gère explicitement plutôt que de l’ignorer.
  • L’outil a retourné des données inattendues → le modèle les remarque et s’adapte.

Pour l’implémenter, demandez au modèle d’évaluer explicitement les résultats des outils, éventuellement au moyen d’un schéma structuré « évaluer, puis agir ».

Cette étape augmente la latence et le nombre de jetons consommés. Elle se justifie pour les actions à haut risque — envoi d’e-mails, traitement de paiements ou modification de dossiers — mais pas pour une simple recherche d’informations à faible risque.

Schéma 6 : Idempotence

Les LLM peuvent parfois appeler le même outil deux fois ou réessayer des appels qui ont déjà réussi. Sans idempotence, vous obtenez des doublons : deux remboursements, deux e-mails envoyés, deux dossiers créés.

Schémas d’idempotence :

Clés d’idempotence. Chaque appel d’outil reçoit une clé unique, générée côté client et incluse dans l’appel. L’API en aval ou l’adaptateur de l’outil utilise cette clé pour détecter les doublons et renvoyer le résultat existant.

Sémantique de recherche ou de création. Les outils qui créent des enregistrements commencent par une recherche. L’action « créer un client avec l’adresse e-mail X » vérifie d’abord si ce client existe ; le cas échéant, elle renvoie l’enregistrement existant au lieu de créer un doublon.

Journalisation des opérations. Les outils consignent chaque opération. L’adaptateur vérifie le journal avant toute exécution et, si l’opération a déjà eu lieu, renvoie le résultat mis en cache.

Conception prudente des outils. Les outils qui exécutent des actions lourdes de conséquences doivent exiger une confirmation explicite ou une approbation humaine. Le LLM ne peut ainsi pas les déclencher accidentellement au cours d’une boucle rapide.

Pour tout outil qui a des effets secondaires, concevez pour l’idempotence. Ignorer cela est une source majeure d’erreurs en production.

Schéma 7 : Observabilité des appels d’outils

Vous devez savoir ce qui se passe avec les appels d’outils. Pour chaque appel, journalisez :

  • Horodatage.
  • Nom de l’outil et arguments.
  • Résultat (ou erreur).
  • Durée.
  • Utilisateur ou session auquel l’appel appartient.
  • La chaîne d’appels dans cette étape (cet appel d’outil faisait-il partie d’une chaîne plus longue ?).

Construisez des tableaux de bord à partir de ces données. Vues courantes :

  • Volume des appels d’outils par outil.
  • Taux d’erreur par outil.
  • Durée moyenne par outil.
  • Modèles de séquences d’outils (« quels outils tendent à être appelés ensemble ? »).
  • Appels d’outils inexistants que le LLM a tenté d’invoquer.

Ces données révèlent où votre système échoue et où il est coûteux.

Schéma 8 : Arguments inventés

Les LLM inventent parfois des valeurs pour les paramètres des outils. Ils peuvent appeler search_customers(email="...") avec une adresse qui ne correspond pas à la demande réelle de l’utilisateur, ou book_meeting(date="...") avec une date qui n’a jamais été mentionnée.

Méthodes d’atténuation :

Schéma strict avec descriptions. « L’identifiant utilisateur doit être l’un mentionné plus tôt dans la conversation. Ne pas inventer d’identifiants. »

Validation dans l’adaptateur de l’outil. Si une valeur n’est pas plausible — identifiant utilisateur inexistant ou date passée, par exemple — l’outil renvoie une erreur structurée et le modèle réévalue son choix.

Réflexion. « Avant d’appeler cet outil, confirmez que les valeurs que vous utilisez sont ancrées dans la conversation. »

Descriptions d’outils restreintes. Les outils qui opèrent sur des entités spécifiques ne montrent que les identifiants d’entités récupérés plus tôt dans la conversation. Ne pas exposer la recherche brute.

Journalisation d’audit. Détectez les motifs récurrents d’arguments inventés et ajustez les prompts ou les schémas.

Schéma 9 : Dégradation progressive

Les outils échouent, les API deviennent indisponibles et les limites de débit sont atteintes. La bonne réponse consiste rarement à annoncer à l’utilisateur que rien ne fonctionne.

Solutions :

Données en cache ou anciennes. Si la source en temps réel est indisponible, renvoyez les données en cache en indiquant clairement leur ancienneté.

Complétion partielle. Si 3 des 5 sous-tâches réussissent, signalez ce qui a été fait et ce qui n’a pas été fait.

Solutions de secours. Si l’outil principal échoue, le modèle connaît une autre voie. Par exemple, si « search_documents » échoue, utilisez « search_web » en appliquant les précautions appropriées.

États d’erreur visibles par l’utilisateur. Si un outil ne peut réellement pas terminer son traitement, le modèle produit un message d’erreur clair au lieu de prétendre que l’action a réussi.

Le modèle doit connaître ces modèles. Documentez-les dans le prompt système :

If a tool returns an error:
- Try the alternate tool if one exists.
- Report partial results clearly if the user has already provided information.
- Never claim success when a tool returned an error.

Schéma 10 : Diffusion des sorties structurées

Pour l’expérience utilisateur, la diffusion progressive de sorties structurées partielles est excellente : l’utilisateur voit les résultats se former en temps réel au lieu d’attendre la réponse complète.

Implémentation :

  • La plupart des API de modèles modernes diffusent le JSON jeton par jeton.
  • Analysez le JSON partiel de manière incrémentielle avec une bibliothèque telle que partial-json-parser ou un petit analyseur de flux.
  • Mettez à jour l’interface utilisateur à mesure que les champs arrivent.

Cette méthode convient particulièrement aux résultats composés de plusieurs sections : longue description de produit, analyse comportant plusieurs enseignements ou revue de code avec plusieurs constatations. Leur affichage progressif paraît beaucoup plus réactif.

Avertissement : ne prenez aucune décision à partir d’un résultat partiel. Diffusez-le uniquement pour l’affichage et attendez la fin avant d’agir sur le résultat structuré.

Schéma 11 : Appel de fonction ou étape de décision explicite

L’appel de fonction natif est pratique : le modèle « décide » quand utiliser un outil. Pour certains flux de travail, une étape de décision explicite est toutefois plus fiable.

Exemple : un flux de travail d’assistance client dans lequel le modèle doit choisir entre plusieurs actions.

Approche avec appel de fonction natif : proposez au modèle cinq outils — rembourser, envoyer un article, transmettre le dossier à une personne, poser une question de clarification et fermer la demande — puis laissez-le décider.

Approche avec décision explicite : appelez d’abord le modèle avec un seul outil, decide_action, qui reçoit l’action comme unique paramètre. Selon la décision obtenue, appelez de nouveau le modèle en ne lui proposant que l’outil pertinent.

L’approche explicite est plus lente et plus verbeuse, mais plus fiable. Le modèle est plus concentré à chaque étape. Le système hôte a plus de contrôle sur le flux de travail.

Pour les flux de travail à haut risque, l’approche explicite gagne souvent. Pour les flux de travail exploratoires ou simples, l’appel natif de fonction est acceptable.

Schéma 12 : Formatage des résultats d’outils

La manière dont vous renvoyez les résultats d’outil importe. Le modèle lit le résultat ; le format importe.

Mauvais :

{"id": "cus_123", "n": "John", "p": "12345"}

Meilleur :

{
  "customer_id": "cus_123",
  "name": "John Doe",
  "phone": "+1-555-0123",
  "tier": "premium",
  "open_tickets": 0
}

Meilleur encore (pour certains cas) :

Customer found:
- ID: cus_123
- Name: John Doe
- Tier: Premium
- Phone: +1-555-0123
- Open tickets: 0

This customer is in the premium tier and has no open tickets.

La dernière forme est lisible par une personne, fournit du contexte et facilite la génération suivante. La forme JSON précédente est plus structurée et exploitable par une machine. Utilisez celle que le modèle traite le mieux pour les étapes suivantes et vérifiez ce choix par des tests.

Pour certains outils, il est efficace de renvoyer à la fois une sortie structurée et un texte explicatif : « Voici le résultat : [texte]. Données brutes : [JSON] ».

Schéma 13 : Nouvelles tentatives adaptées aux erreurs de schéma

Certaines erreurs de validation sont irrécupérables, car le modèle a fondamentalement mal compris la tâche. D’autres se corrigent facilement.

Une méthode utile consiste à classer l’erreur et à réagir en conséquence.

def handle_validation_error(error):
    if "missing required field" in str(error):
        return retry_with_message("You omitted required field X. Please include it.")
    elif "value not in enum" in str(error):
        return retry_with_message("Value X is not in the allowed set. Choose from: ...")
    elif "type mismatch" in str(error):
        return retry_with_message("Field X must be a number, not a string.")
    else:
        # Unknown error — single generic retry
        return retry_with_message("There was an error in your response. Please try again.")

Les nouvelles tentatives ciblées réussissent plus souvent que les tentatives génériques.

Schéma 14 : Composabilité

Les outils doivent se composer. Des outils petits et axés sur une seule tâche peuvent être combinés par le modèle en flux de travail complexes.

Un outil monolithique process_customer_request(query) qui fait tout est une boîte noire. Le modèle ne peut pas observer ou guider la logique interne.

Un ensemble d’outils ciblés — search_customer(email), get_recent_orders(customer_id), check_subscription_status(customer_id) et escalate_to_human(reason) — peut être composé par le modèle afin de former un flux adapté à chaque situation.

Concevez les outils à la granularité appropriée. Chaque outil fait une seule chose. Les outils se composent en flux de travail.

Schéma 15 : Représenter explicitement « je ne sais pas »

Voici une pratique subtile : représentez explicitement l’incertitude dans le schéma.

class CustomerInfo(BaseModel):
    name: str
    name_confidence: Literal["high", "medium", "low"]
    needs_clarification: bool
    clarification_question: Optional[str] = None

Le modèle peut renvoyer un faible niveau de confiance accompagné d’une question de clarification au lieu d’inventer une réponse.

Cette approche est nettement préférable à un modèle qui remplit systématiquement les champs avec assurance, parfois à l’aide de données inventées.

Exemple concret : traitement de facture

Pour illustrer, voici un système de traitement de facture de qualité de production.

Entrées : Facture PDF jointe à un e-mail. Objectif : extraire des données structurées et les transmettre au système comptable.

Schéma :

class LineItem(BaseModel):
    description: str
    quantity: float
    unit_price: float
    total: float
    confidence: Literal["high", "medium", "low"]

class Invoice(BaseModel):
    vendor_name: str
    vendor_id: Optional[str] = None  # null if not found in our records
    invoice_number: str
    invoice_date: date
    due_date: Optional[date] = None
    line_items: List[LineItem]
    subtotal: float
    tax: float
    total: float
    currency: str  # ISO 4217
    confidence: Literal["high", "medium", "low"]
    needs_review: bool
    review_reasons: List[str]  # Specific reasons review is needed

Flux de travail :

  1. Étape OCR : un modèle de vision extrait le texte du PDF.
  2. Étape d’extraction : appel du LLM avec le schéma ci-dessus et la génération contrainte activée.
  3. Étape de validation : Pydantic valide le résultat. Toute erreur déclenche une nouvelle tentative accompagnée du détail de l’erreur.
  4. Étape de vérification croisée : appel d’un outil pour rechercher le fournisseur dans les dossiers. Si vendor_name correspond à un fournisseur connu, associez vendor_id. Sinon, définissez needs_review=true.
  5. Étape de vérification mathématique : Vérifiez que sum(line_items.total) ≈ subtotal et subtotal + tax ≈ total. Si non, définissez needs_review=true.
  6. Étape de vérification du niveau de confiance : si confidence est faible ou si un poste de facture présente un faible niveau de confiance, définissez needs_review=true.
  7. Étape de routage : si needs_review=true, envoyez le dossier dans la file de vérification humaine. Sinon, transmettez-le au système comptable.
  8. Étape de journalisation : Chaque étape est journalisée avec l’entrée, la sortie, la durée, les erreurs.

Modes d’échec gérés :

  • JSON malformé : la génération contrainte l’empêche ; une nouvelle tentative gère les cas limites.
  • Champs inventés : le schéma strict les interdit.
  • Erreurs mathématiques : validées.
  • Fournisseurs inconnus : signalés.
  • Faible confiance : signalés.
  • Erreurs d’outil : gérées explicitement.

Performances en production : environ 95 % des factures sont traitées directement et 5 % sont signalées pour vérification. Parmi les factures traitées automatiquement, le taux d’erreur est inférieur à 0,5 %, donc largement acceptable. Dans la file de vérification, environ 80 % sont confirmées comme correctes et 20 % nécessitent des corrections.

Voilà à quoi ressemble une sortie structurée adaptée à la production : non pas un mode JSON qui a fonctionné une fois, mais une chaîne de traitement qui gère les modes de défaillance réels.

Erreurs courantes

Voici quelques erreurs que nous observons régulièrement :

Erreur 1 : aucune validation. Avec Pydantic, Zod ou tout autre outil, validez systématiquement. Ne faites pas confiance au modèle.

Erreur 2 : Descriptions vagues. « category: string » n’aide pas le modèle. « category: un des billing, technical, account_access, où billing couvre… » le fait.

Erreur 3 : trop d’outils. Avec 30 outils disponibles, le modèle choisit les mauvais. Limitez chaque appel à moins de dix outils pertinents.

Erreur 4 : aucune nouvelle tentative après un échec de validation. Un seul résultat malformé interrompt tout le flux. Relancez une fois le traitement en fournissant le détail de l’erreur.

Erreur 5 : Aucune observabilité. Lorsque les appels d’outils échouent en production, vous ne pouvez pas diagnostiquer sans traces.

Erreur 6 : aucune idempotence pour les outils à effets secondaires. Les remboursements ou e-mails en double constituent une anomalie prévisible.

Erreur 7 : faire confiance aux arguments choisis par le LLM sans validation. Identifiants utilisateur ou dates inventés : validez les arguments de l’outil avant son exécution.

Erreur 8 : ignorer la version des schémas. Les changements de schéma rompent les consommateurs en aval. Gérez les versions.

De la démonstration à un système de production

Les sorties structurées et les appels de fonctions permettent de passer d’un « LLM qui parle » à un « LLM qui exécute des tâches ». Bien conçus, ils rendent possible l’IA en production. Mal conçus, ils échouent de manière complexe et coûteuse.

Les schémas qui comptent sont les suivants : schémas JSON stricts, génération contrainte, validation avec nouvelle tentative, analyse des résultats d’outils, idempotence, dégradation progressive, gestion des erreurs adaptée aux schémas et observabilité de bout en bout.

Chacun de ces éléments distingue une démonstration d’un système de production. Intégrez-les dès le départ.

À lire ensuite

Continuez sur le même parcours d'apprentissage avec les prochains articles pratiques.