Changer le thème

Sortie structurée LLM : JSON Schema obligatoire et fiabilité des appels d'outils

Easton editorial illustration: central JSON Schema gate, three incoming provider-output cards, one validated tool-call object

Une alerte en production : échec d’appel d’outil Agent, cinq retries consécutifs, tous des erreurs de format de paramètres. Dans les logs, le champ city devait être "北京", le LLM renvoyait {"name": "北京", "id": null}. Le parseur plante, tout le pipeline de traitement s’arrête.

C’était un gros piège l’année dernière.

Depuis, j’ai étudié systématiquement la sortie structurée des LLM — des Structured Outputs d’OpenAI au Tool Use d’Anthropic, du retry automatique d’Instructor au décodage contraint d’Outlines. Au début, je pensais qu’un « meilleur prompt » suffisait. En réalité, ce n’est pas un problème de prompt, c’est un problème d’architecture de fiabilité.

Cet article présente cette « architecture de fiabilité à trois niveaux » : validation des paramètres, retry en cas d’échec, décodage contraint. En fin de parcours, comparaison transversale OpenAI, Claude et Gemini pour choisir la bonne approche. Quelques modèles de code prêts pour la production sont inclus.

1. Pourquoi la sortie structurée est la base des Agents

Commençons par le « dérive de format » — pas un cas isolé, le cauchemar de tout développeur Agent.

Trois formes de dérive de format

Première forme : champs manquants. Vous demandez un objet utilisateur avec name, age, email ; le LLM renvoie {"name": "张三"} — les deux autres champs ont disparu. Pas à chaque fois, mais parfois. En production, « parfois » veut dire « inévitablement ».

Deuxième forme : erreur de type. La doc est claire : user_id est un entier. Le LLM renvoie "user_id": "12345", une chaîne. La validation Pydantic échoue, toute la chaîne d’appels se brise.

Troisième forme : contenu superflu. La plus insidieuse. Vous voulez du JSON ; le modèle ajoute « Here is the response: » au début et « I hope this helps! » à la fin. Le parseur JSON ne sait plus quoi en faire.

5-10 %
Taux d’échec JSON Mode
<0,1 %
Taux d’échec Structured Outputs

Les chiffres officiels d’OpenAI parlent d’eux-mêmes : JSON Mode (JSON valide uniquement) échoue dans 5-10 % des cas ; Structured Outputs (respect forcé du Schema) échoue à moins de 0,1 %. Deux ordres de grandeur d’écart.

Pourquoi c’est si important

Vous pourriez penser : « Ce n’est qu’un échec de parsing, quelques retries de plus. »

Le retry n’est pas gratuit.

Coût des appels API. Un appel GPT-4 peut coûter quelques centimes ; cinq retries, quelques euros. Si votre Agent traite 100 000 requêtes par jour avec en moyenne deux retries par requête — faites le calcul.

Latence cumulée. Deux secondes par appel, trois retries : l’utilisateur attend plus de six secondes. Inacceptable en dialogue temps réel.

Expérience utilisateur. L’utilisateur demande la météo, l’Agent tourne dix secondes, puis « erreur système ». Il ne reviendra pas.

La sortie structurée n’est donc pas un « plus », c’est la base d’un Agent stable. La suite explique comment y parvenir — pas avec « un meilleur prompt », mais avec une architecture fiable.

2. Architecture de fiabilité à trois niveaux

Cette architecture résume beaucoup d’essais et d’erreurs. Ce n’est pas une solution miracle, mais elle fait passer le taux d’erreur de format de 5-10 % à quasi zéro.

L1 : couche de validation des paramètres — première ligne de défense

Rôle simple : définir la structure attendue avec Pydantic, forcer la conversion de types, filtrer par liste blanche.

from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
from datetime import datetime

class ToolCallParams(BaseModel):
    """Modèle de paramètres d'appel d'outil"""
    city: str = Field(..., min_length=1, max_length=50, description="Nom de la ville")
    date: Optional[datetime] = Field(None, description="Date de la requête")
    units: str = Field("metric", pattern="^(metric|imperial)$")

    @field_validator("city")
    @classmethod
    def validate_city(cls, v: str) -> str:
        # Validation par liste blanche
        allowed_cities = {"北京", "上海", "广州", "深圳", "杭州"}
        if v not in allowed_cities:
            raise ValueError(f"Ville non prise en charge : {v}, prises en charge : {allowed_cities}")
        return v

Pydantic fait trois choses : conversion de type (chaîne "123" → entier 123), détection de champs manquants, validation personnalisée. Couche la plus basique et la plus importante.

L2 : couche de retry en cas d’échec — auto-correction avec retour d’erreur

Quand la validation échoue, ne relancez pas aveuglément : renvoyez l’erreur au LLM pour qu’il corrige. La bibliothèque Instructor encapsule bien cette logique.

import instructor
from openai import OpenAI
from pydantic import ValidationError

client = instructor.patch(OpenAI())

def get_weather_with_retry(user_query: str, max_retries: int = 3):
    """Mécanisme de retry avec retour d'erreur"""
    messages = [{"role": "user", "content": user_query}]

    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="gpt-4o",
                response_model=ToolCallParams,  # Modèle Pydantic
                messages=messages,
                temperature=0.1  # Température basse pour sortie structurée
            )
            return response  # Validation automatique réussie

        except ValidationError as e:
            # Renvoyer l'erreur au LLM pour correction
            error_msg = f"Échec de validation des paramètres : {str(e)}\nCorrigez et renvoyez le JSON au bon format."
            messages.append({"role": "assistant", "content": "Génération des paramètres..."})
            messages.append({"role": "user", "content": error_msg})

            if attempt == max_retries - 1:
                raise Exception(f"Échec après {max_retries} tentatives : {e}")

# Exemple d'utilisation
result = get_weather_with_retry("帮我查一下北京明天的天气")

Principe clé : le LLM ne devine pas au hasard ; il sait ce qui est faux et pourquoi. Avec un retour d’erreur, il corrige. En pratique, le taux de succès au retry passe de 60 % à plus de 95 % avec ce mécanisme.

L3 : couche de décodage contraint — éliminer les erreurs à la source

Les deux premiers niveaux corrigent après coup ; L3 prévient avant.

Principe : à chaque token généré, une machine à états finis (FSM) limite les choix possibles et force une séquence conforme au Schema. Comme un « frein » : le modèle ne peut pas dévier.

Deux options principales :

Outlines (open source, modèles locaux) :

from outlines import models, generate
import json

# Charger le modèle local
model = models.transformers("Qwen/Qwen2.5-7B-Instruct")

# Définir le Schema
schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer"}
    },
    "required": ["name", "age"]
}

# Créer le générateur contraint
generator = generate.json(model, schema)
result = generator("提取用户信息: 张三今年28岁")
# 100 % conforme au Schema, sans retry

guided_json de vLLM (grands modèles déployés) :

from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen2.5-72B-Instruct")
sampling_params = SamplingParams(
    temperature=0.0,
    guided_decoding_backend="outlines",
    guided_json={  # Passer directement le JSON Schema
        "type": "object",
        "properties": {
            "tool_name": {"type": "string"},
            "arguments": {"type": "object"}
        }
    }
)

L3 a un coût de compilation : la FSM doit être construite à partir du Schema. Si le Schema change souvent, chaque reconstruction ajoute de la latence. Pour la plupart des Agents, le Schema est relativement stable — surcoût acceptable.

Comment choisir entre les trois niveaux

ScénarioSolution recommandée
Appels API OpenAIL1 + L2 (Pydantic + Instructor)
Appels API ClaudeL1 + L2 (Claude ne supporte pas Strict Mode)
Modèles locaux déployésL1 + L3 (Outlines / vLLM guided_json)
Fiabilité maximale exigéeL1 + L2 + L3 combinés

3. Comparaison des éditeurs : OpenAI, Claude, Gemini

Différences d’implémentation entre éditeurs. Sans comparaison, on tombe facilement dans le piège — « sortie structurée » ne signifie pas la même chose partout.

OpenAI : Strict Mode, conformité forcée

En août 2024, OpenAI a lancé Structured Outputs — la solution la plus fiable parmi les API commerciales actuelles.

Mécanisme central : le paramètre strict: true. Activé, la sortie est contrainte au JSON Schema défini, conformité garantie à 100 %. Sous le capot : décodage contraint par grammaire, proche du principe d’Outlines.

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "提取用户信息"}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "user_info",
            "strict": True,  # Paramètre clé
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"}
                },
                "required": ["name", "age"]
            }
        }
    }
)
# Sortie 100 % conforme au Schema

OpenAI annonce un taux d’échec Strict Mode inférieur à 0,1 %. En pratique, je n’ai pas vu d’erreur de format — avec une limite : pas de Schema récursif ; certaines structures imbriquées demandent des contournements.

Anthropic Claude : Tool Use, sans garantie de conformité

Claude emprunte une autre voie — Tool Use (appel d’outil).

Vous définissez un outil, Claude l’appelle avec des arguments. Piège : le paramètre strict existe mais la documentation officielle précise qu’il est ignoré. Claude ne garantit pas que les arguments respectent votre Schema.

Citation officielle Anthropic (mise à jour avril 2026) :

“The strict parameter is currently ignored for tool definitions. Claude will make a best effort to provide valid arguments, but does not guarantee schema compliance.”

Traduction : il fera de son mieux, sans garantie. Avec Claude pour les appels d’outils, ajoutez L1 (validation) et L2 (retry).

import anthropic

client = anthropic.Anthropic()

# Définition d'outil Claude
tools = [{
    "name": "get_weather",
    "input_schema": {
        "type": "object",
        "properties": {
            "city": {"type": "string"}
        },
        "required": ["city"]
    }
}]

response = client.messages.create(
    model="claude-3.5-sonnet",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "北京天气"}]
)

# Important : valider manuellement les paramètres tool_use
for block in response.content:
    if block.type == "tool_use":
        # Validation Pydantic ici
        validated_params = ToolCallParams.model_validate(block.input)

Google Gemini : Controlled Generation

Gemini propose Controlled Generation via le paramètre response_schema.

import google.generativeai as genai

model = genai.GenerativeModel('gemini-1.5-pro')

response = model.generate_content(
    "提取用户信息",
    generation_config={
        "response_mime_type": "application/json",
        "response_schema": {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "age": {"type": "integer"}
            },
            "required": ["name", "age"]
        }
    }
)

Fiabilité Gemini entre OpenAI et Claude — contrainte présente, mais sans la force « conformité forcée » d’OpenAI. En test, taux d’échec autour de 1-2 % : mieux que JSON Mode, en deçà de Strict Mode.

Modèles open source : Outlines / vLLM

Les modèles open source (Qwen, Llama, Mistral) ne supportent pas nativement la sortie structurée ; il faut des outils externes — Outlines et guided_json de vLLM.

Point intéressant : modèle open source + Outlines peut être plus fiable que certaines API commerciales — la FSM est une contrainte dure, pas un « meilleur effort sans garantie ».

Tableau de choix rapide

BesoinSolution recommandéeRaison
Appels API purs, stabilitéOpenAI + Structured Outputs0,1 % d’échec, le plus fiable
Raisonnement complexe + outilsClaude + validation L1/L2Fort en raisonnement, validation requise
Modèle privé déployéQwen/Llama + OutlinesCoût maîtrisé, haute fiabilité
Format très strict (finance, santé)OpenAI Strict ou OutlinesÉchec quasi nul
Prototype rapideInstructor + API au choixBien encapsulé, retry automatique

4. Modèles de code prêts pour la production

Quelques modèles validés en production — utilisables tels quels ou adaptés.

Modèle 1 : exemple complet OpenAI Structured Outputs

"""
Exemple complet OpenAI Structured Outputs
Usage : appels d'outils, extraction de données, génération de rapports
"""
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import List, Optional
import json

# 1. Définir le modèle Pydantic
class SearchQuery(BaseModel):
    """Paramètres de requête de recherche"""
    keywords: List[str] = Field(
        ...,
        min_length=1,
        max_length=5,
        description="Liste de mots-clés de recherche"
    )
    filters: Optional[dict] = Field(
        default=None,
        description="Filtres optionnels"
    )
    limit: int = Field(
        default=10,
        ge=1,
        le=100,
        description="Nombre de résultats"
    )

# 2. Modèle Pydantic → JSON Schema
def model_to_schema(model: type[BaseModel]) -> dict:
    """Convertir un modèle Pydantic en JSON Schema"""
    schema = model.model_json_schema()
    # Nettoyer les métadonnées Pydantic
    schema.pop("title", None)
    for prop in schema.get("properties", {}).values():
        prop.pop("title", None)
    return schema

# 3. Appel de sortie structurée
client = OpenAI()

def extract_search_params(user_input: str) -> SearchQuery:
    """Extraire les paramètres de recherche depuis l'entrée utilisateur"""
    schema = model_to_schema(SearchQuery)

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {
                "role": "system",
                "content": "你是一个搜索助手,帮助用户提取搜索参数。"
            },
            {"role": "user", "content": user_input}
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "search_query",
                "strict": True,
                "schema": schema
            }
        },
        temperature=0.1  # Température basse pour sortie structurée
    )

    # 4. Analyser et valider une seconde fois
    raw_content = response.choices[0].message.content
    data = json.loads(raw_content)
    return SearchQuery.model_validate(data)

# Exemple d'utilisation
if __name__ == "__main__":
    query = extract_search_params(
        "我想找一些关于 Python 异步编程的文章,只要最近一个月的,最多 20 条"
    )
    print(query)
    # SearchQuery(keywords=['Python', '异步编程'], filters={'date_range': 'last_month'}, limit=20)

Modèle 2 : retry automatique avec Instructor

"""
Exemple de retry automatique Instructor
Usage : API Claude, OpenAI JSON Mode (non Strict), scénarios tolérants aux erreurs
"""
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field, ValidationError

class AgentAction(BaseModel):
    """Décision d'action de l'Agent"""
    action_type: str = Field(
        ...,
        pattern="^(search|execute|respond|clarify)$"
    )
    parameters: dict = Field(default_factory=dict)
    reasoning: str = Field(..., min_length=10)

# patch du client OpenAI
client = instructor.patch(OpenAI())

def get_agent_decision(
    context: str,
    user_request: str,
    max_retries: int = 3
) -> AgentAction:
    """
    Obtenir la décision d'action de l'Agent avec retry automatique

    Args:
        context: Contexte de conversation actuel
        user_request: Requête utilisateur
        max_retries: Nombre maximal de tentatives

    Returns:
        AgentAction: Décision validée
    """
    messages = [
        {"role": "system", "content": "你是一个智能助手,分析用户需求并决定下一步行动。"},
        {"role": "user", "content": f"上下文: {context}\n\n用户请求: {user_request}"}
    ]

    try:
        response = client.chat.completions.create(
            model="gpt-4o",
            response_model=AgentAction,  # Validation automatique Instructor
            messages=messages,
            max_retries=max_retries,  # Retry intégré
            temperature=0.1
        )
        return response

    except ValidationError as e:
        # Instructor a déjà retenté max_retries fois
        raise Exception(f"Erreur de format non corrigeable, vérifiez la définition du modèle : {e}")

# Exemple d'utilisation
decision = get_agent_decision(
    context="用户正在查询天气信息",
    user_request="帮我查北京明天的天气,要是晴天就推荐户外活动"
)
print(f"Type d'action : {decision.action_type}")
print(f"Paramètres : {decision.parameters}")
print(f"Raisonnement : {decision.reasoning}")

Modèle 3 : sortie structurée avec modèle local Outlines

"""
Exemple Outlines pour modèle local
Usage : déploiement privé, coût sensible, exigences de confidentialité
"""
from outlines import models, generate
from pydantic import BaseModel
from typing import List
import json

# Définir la structure de données
class ProductInfo(BaseModel):
    """Informations produit"""
    name: str
    price: float
    category: str
    tags: List[str]

# Charger le modèle (premier chargement : quelques secondes)
model = models.transformers("Qwen/Qwen2.5-7B-Instruct")

# Créer le générateur structuré
# Note : le schema est compilé en FSM au premier appel (~1-2 s)
schema_str = json.dumps(ProductInfo.model_json_schema())
generator = generate.json(model, schema_str)

def extract_product_info(description: str) -> ProductInfo:
    """
    Extraire les informations structurées depuis une description produit

    Args:
        description: Texte de description produit

    Returns:
        ProductInfo: Informations produit structurées
    """
    prompt = f"从以下商品描述中提取关键信息,以 JSON 格式返回:\n{description}"

    # Résultat 100 % conforme au Schema
    result = generator(prompt)

    # Conversion en modèle Pydantic (double validation)
    return ProductInfo.model_validate(result)

# Exemple d'utilisation
description = """
这款蓝牙耳机采用最新的降噪技术,价格 299 元,
属于数码配件类,适合运动、通勤等场景使用。
"""
product = extract_product_info(description)
print(product)
# ProductInfo(name='蓝牙耳机', price=299.0, category='数码配件', tags=['运动', '通勤'])

Modèle 4 : flux complet d’appel d’outil

"""
Flux complet de validation des paramètres d'appel d'outil
Inclut : définition Schema → appel LLM → validation → retry → exécution outil
"""
from openai import OpenAI
from pydantic import BaseModel, Field, field_validator, ValidationError
from typing import Callable, Dict, Any
import json

# 1. Modèle de paramètres d'outil
class WeatherQueryParams(BaseModel):
    """Paramètres de l'outil météo"""
    city: str = Field(..., min_length=1, max_length=50)
    date_offset: int = Field(default=0, ge=-7, le=7, description="Décalage de date, 0 = aujourd'hui")

    @field_validator("city")
    @classmethod
    def validate_city(cls, v: str) -> str:
        allowed = {"北京", "上海", "广州", "深圳", "杭州", "成都", "武汉"}
        if v not in allowed:
            raise ValueError(f"Ville non prise en charge, options : {allowed}")
        return v

# 2. Gestionnaire d'appels d'outils
class ToolCallManager:
    """Gère le flux complet d'appel d'outil"""

    def __init__(self):
        self.client = OpenAI()
        self.tools: Dict[str, Callable] = {}

    def register_tool(self, name: str, func: Callable, param_model: type[BaseModel]):
        """Enregistrer un outil"""
        self.tools[name] = {
            "function": func,
            "param_model": param_model
        }

    def execute_with_retry(
        self,
        tool_name: str,
        user_request: str,
        max_retries: int = 3
    ) -> Any:
        """Exécuter un appel d'outil avec retry"""

        tool_config = self.tools[tool_name]
        param_model = tool_config["param_model"]
        schema = param_model.model_json_schema()

        messages = [
            {"role": "system", "content": f"Extraire les paramètres d'appel de l'outil '{tool_name}'"},
            {"role": "user", "content": user_request}
        ]

        for attempt in range(max_retries):
            try:
                # Appeler le LLM pour obtenir les paramètres
                response = self.client.chat.completions.create(
                    model="gpt-4o",
                    messages=messages,
                    response_format={
                        "type": "json_schema",
                        "json_schema": {
                            "name": tool_name,
                            "strict": True,
                            "schema": schema
                        }
                    },
                    temperature=0.1
                )

                # Valider les paramètres
                params = param_model.model_validate_json(
                    response.choices[0].message.content
                )

                # Exécuter l'outil
                return tool_config["function"](params)

            except ValidationError as e:
                # Retour d'erreur pour correction
                messages.append({
                    "role": "user",
                    "content": f"Échec de validation des paramètres : {e}\nCorrigez le format."
                })
                continue

        raise Exception(f"Échec d'appel d'outil après {max_retries} tentatives de validation")

# 3. Exemple d'utilisation
def get_weather(params: WeatherQueryParams) -> str:
    """Simulation de requête météo"""
    # Logique d'appel API réelle ici
    return f"{params.city} 未来 {params.date_offset} 天天气晴朗"

manager = ToolCallManager()
manager.register_tool("get_weather", get_weather, WeatherQueryParams)

result = manager.execute_with_retry(
    "get_weather",
    "帮我查一下北京明天的天气"
)
print(result)  # 北京未来 1 天天气晴朗

Ces modèles couvrent les scénarios les plus courants. Combinez et adaptez selon vos besoins.

5. Bonnes pratiques en production

Le code est prêt, mais la production impose d’autres détails. Quelques pièges rencontrés et leurs solutions.

Temperature : ne montez pas trop

En sortie structurée, réglez Temperature entre 0,0 et 0,2 — plage recommandée par OpenAI, la plus stable en pratique.

Problème d’une température élevée ? Le LLM « diverge », la sortie devient aléatoire. L’aléatoire est l’ennemi de la structure — vous voulez de la déterminisme, pas de la créativité. À 0,7, j’avais 15 % d’erreurs de format ; à 0,1, le problème a quasi disparu.

Stratégie de retry : tout ne mérite pas un retry

Avant de retenter, classifiez l’erreur :

Type d’erreurRetry ?Raison
Erreur de format (champ manquant, mauvais type)Oui + retour d’erreurLe LLM peut corriger
Erreur de service API (429, 500)Oui + backoffProblème temporaire serveur
Échec validation métier (ville hors liste)Non, erreur directeConfirmation utilisateur
Échec exécution outil (résultat vide)Non, fallbackProblème de l’outil

J’ai vu des retries illimités sur une ville hors liste blanche : dix tentatives, timeout. Classifiez les erreurs pour traiter efficacement.

Comparaison du surcoût de performance

SolutionLatence ajoutéeCoût ajoutéFiabilité
Contrainte par prompt (sans paramètre spécial)+0 ms+0 %5-10 % d’échec
JSON Mode (OpenAI uniquement)+50 ms+0 %2-5 % d’échec
Structured Outputs (Strict)+100 ms+0 %<0,1 % d’échec
Retry Instructor+200-500 ms/tentativecoût × retriesproche de 0 %
Outlines FSM+1-2 s (première compilation)+0 %100 % conforme

Arbitrage : stabilité maximale → Structured Outputs ou Outlines ; prototype rapide → Instructor ; budget serré → JSON Mode + validation manuelle.

Métriques à surveiller : trois incontournables

Après mise en ligne, surveillez :

  1. Taux d’échec de format : part des requêtes en échec de validation. Au-delà de 1 %, investiguer.
  2. Nombre moyen de retries : normalement entre 0,5 et 1,5. Au-delà de 2, problème de modèle ou de Schema.
  3. Latence moyenne : +50-200 ms vs sortie classique, à garder acceptable.

Avec Prometheus + Grafana, je consulte un rapport hebdomadaire. Un jour, les retries sont passés de 0,8 à 2,5 — le Schema avait changé sans mise à jour du code. Le monitoring a évité pire.

Conclusion

Un seul message central : en 2026, la sortie structurée n’est plus un problème insoluble — avec la bonne méthode.

L’architecture à trois niveaux (validation + retry + décodage contraint) couvre le passage du « ça tourne » au « ça tourne de façon stable ». Pour les éditeurs : OpenAI Strict Mode est le plus stable, Claude exige une validation manuelle, modèles open source + Outlines offrent une fiabilité remarquable.

Les modèles de code sont au chapitre 4 — adaptez-les. Si vous débutez en développement Agent, commencez par Instructor : bonne encapsulation, retry et retour d’erreur intégrés ; passez à Outlines pour une conformité à 100 % une fois à l’aise.

Des questions ? Commentez ou contactez-moi. Contenu un peu dense — j’espère qu’il vous évitera quelques pièges.

Implémenter le flux complet OpenAI Structured Outputs

Étapes complètes de la définition du modèle Pydantic à l'appel de sortie structurée

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Définir le modèle de données Pydantic

    Créez une classe de modèle Pydantic avec Field pour les contraintes :

    • `Field(..., min_length=1, max_length=50)` pour la longueur des chaînes
    • `Field(default=10, ge=1, le=100)` pour les plages numériques
    • `@field_validator` pour la logique de validation personnalisée (ex. filtre par liste blanche)
    • `Optional[T]` pour les champs optionnels
  2. 2

    Step 2: Convertir le modèle Pydantic en JSON Schema

    Utilisez la méthode `model.model_json_schema()` :

    ```python
    schema = SearchQuery.model_json_schema()
    schema.pop("title", None) # Nettoyer les métadonnées Pydantic
    ```

    Vérifiez que le Schema respecte les exigences OpenAI Structured Outputs.
  3. 3

    Step 3: Appeler l'API OpenAI avec Strict Mode activé

    Définissez le paramètre `response_format` dans la requête API :

    • `type: "json_schema"` — type de sortie structurée
    • `strict: True` — mode de conformité forcée
    • `json_schema.name` — nom du Schema (personnalisé)
    • `json_schema.schema` — JSON Schema converti à l'étape précédente
  4. 4

    Step 4: Analyser la réponse et valider une seconde fois

    Même si Strict Mode garantit une conformité à 100 %, une double validation reste recommandée :

    • `json.loads()` pour analyser la chaîne de réponse
    • `model.model_validate(data)` pour la validation Pydantic
    • Capturer `ValidationError` et gérer les cas limites
  5. 5

    Step 5: Configurer le paramètre Temperature

    Utilisez une température basse pour la sortie structurée :

    ```python
    temperature=0.1 # Recommandé : 0.0-0.2
    ```

    Évitez une température élevée qui augmente l'aléatoire et déstabilise le format.

FAQ

Que faire si le LLM renvoie un JSON mal formaté ?
Adoptez une architecture de fiabilité à trois niveaux :

• Niveau L1 — validation des paramètres : modèle Pydantic, conversion de types et validation des champs
• Niveau L2 — retry en cas d'échec : bibliothèque Instructor qui renvoie l'erreur au LLM pour auto-correction
• Niveau L3 — décodage contraint : Outlines ou guided_json de vLLM pour garantir la conformité à la source
Quelle est la différence entre la sortie structurée OpenAI et Claude ?
OpenAI Structured Outputs en mode strict garantit une conformité de format à 100 %, taux d'échec &lt;0,1 % ; Claude Tool Use ne garantit pas la conformité, le paramètre strict est ignoré par l'éditeur — ajoutez les niveaux L1/L2. Pour une stabilité maximale, choisissez OpenAI ; pour un raisonnement complexe, Claude + validation manuelle est préférable.
Comment choisir la bonne solution de sortie structurée ?
Selon le scénario et le besoin :

• **Appels API OpenAI** : Structured Outputs + Strict Mode (le plus stable)
• **Appels API Claude** : validation Pydantic + retry Instructor (validation manuelle requise)
• **Déploiement de modèles locaux** : Outlines ou vLLM guided_json (coût maîtrisé, haute fiabilité)
• **Prototype rapide** : bibliothèque Instructor (encapsulation prête à l'emploi)
• **Secteurs exigeants (finance, santé)** : OpenAI Strict ou Outlines (échec quasi nul)
Comment régler le paramètre Temperature ?
En sortie structurée, réglez Temperature entre 0,0 et 0,2. Plage recommandée par OpenAI, la plus stable en pratique. Une température élevée augmente l'aléatoire et le taux d'erreurs de format. En test, à 0,7 le taux d'erreur atteignait 15 % ; à 0,1, les problèmes de format ont quasi disparu.
Quel surcoût de performance pour la sortie structurée ?
Les écarts selon la solution sont importants :

• **Contrainte par prompt** : +0 ms de latence, 5-10 % d'échecs
• **JSON Mode** : +50 ms, 2-5 % d'échecs
• **Structured Outputs** : +100 ms, &lt;0,1 % d'échecs
• **Retry Instructor** : +200-500 ms par retry, taux d'échec proche de 0 %
• **Outlines FSM** : +1-2 s à la première compilation, 100 % de conformité

Arbitrez selon la fiabilité et le budget.
Quelles erreurs faut-il retenter ? Lesquelles non ?
Distinguez le type d'erreur :

**À retenter** :
• Erreur de format des paramètres (champ manquant, mauvais type) — le LLM peut corriger
• Erreur de service API (429, 500) — problème temporaire côté serveur

**À ne pas retenter** :
• Échec de validation métier (ville hors liste blanche) — confirmation utilisateur requise
• Échec d'exécution de l'outil (résultat vide) — problème de l'outil, passer en fallback

Les retries illimités provoquent des timeouts ; seule une classification fine des erreurs est efficace.

15 min de lecture · Publié le: 6 mai 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog