Validation des sorties structurées des LLM en Python qui tient la route

Arrêtez d’interpréter des vibes. Validez les contrats.

Sommaire

La plupart des tutoriels sur les « sorties structurées » des LLM manquent de sérieux. Ils vous apprennent à demander du JSON poliment, puis à espérer que le modèle se comporte correctement. Ce n’est pas de la validation. C’est de l’optimisme entre accolades.

La documentation d’OpenAI elle-même fait la distinction de manière explicite. Le mode JSON vous donne un JSON valide, tandis que les Sorties Structurées (Structured Outputs) imposent le respect du schéma, et OpenAI recommande d’utiliser les Sorties Structurées plutôt que le mode JSON lorsque cela est possible.

infographie de validation des sorties structurées

Cela ne rend pas pour autant la charge utile fiable. Le JSON Schema définit la structure et les valeurs autorisées, Pydantic vous offre une validation typée en Python, et OpenAI note explicitement qu’une réponse conforme au schéma peut encore contenir des valeurs incorrectes. Par-dessus le marché, les refus et les sorties incomplètes peuvent contourner la forme attendue. En production, la validation des sorties structurées est un pipeline, pas un simple interrupteur. Cette même frontière doit également s’inscrire dans le contexte plus large du débit, des tentatives de reprise et des limites du planificateur sur le hub d’ingénierie des performances LLM.

La validation des sorties structurées est un contrat

La validation des sorties structurées pour les LLM signifie que vous définissez la forme de la réponse à l’avance, contraignez le modèle à produire cette forme lorsqu’il est possible, puis validez le résultat à nouveau avant que votre application ne lui fasse confiance. En pratique, cela signifie vérifier les champs obligatoires, les types, les énumérations, les formes d’objets fermés et les règles métier avant que la charge utile n’atteigne votre base de données, votre interface utilisateur, votre file d’attente ou vos services en aval. Le JSON Schema existe précisément pour ce type de validation structurelle, Pydantic est conçu pour valider les données non fiables par rapport aux indices de type Python, et la bibliothèque jsonschema de Python vous offre un moyen direct de valider une instance par rapport à un schéma.

Il existe également une séparation claire entre deux cas d’utilisation courants. Si le modèle est censé répondre à l’utilisateur dans un format structuré, utilisez un format de réponse structuré. Si le modèle est censé appeler les outils ou fonctions de votre application, utilisez l’appel de fonction. La documentation d’OpenAI expose cette distinction, et pour l’appel de fonction, ils recommandent d’activer strict: true afin que les arguments respectent fidèlement le schéma de la fonction.

Mon avis tranché est simple. Traitez chaque réponse structurée d’un LLM comme une frontière d’API. Une fois que vous commencez à penser en termes de contrats plutôt que de prompts, l’architecture devient plus propre, les bugs moins coûteux, et le problème du « pourquoi le modèle a inventé un nouveau champ en production » disparaît presque entièrement. C’est la vraie réponse à « qu’est-ce que la validation des sorties structurées pour les LLM » et c’est une bien meilleure réponse que « demander gentiment au modèle de fournir du JSON ».

Le mode JSON n’est pas une validation

Si vous ne retenez qu’une seule chose de cet article, que ce soit celle-ci. Le mode JSON n’est pas une validation de schéma. Le Centre d’aide d’OpenAI indique que le mode JSON ne garantit pas que la sortie correspondra à un schéma spécifique, seulement qu’il s’agit d’un JSON valide et qu’il se parse sans erreurs. Le guide des Sorties Structurées dit la même chose, mais de manière plus claire. Le mode JSON et les Sorties Structurées peuvent tous deux produire du JSON valide, mais seules les Sorties Structurées imposent le respect du schéma.

Cette différence a plus d’importance que les gens ne l’admettent. Dans son article de lancement des Sorties Structurées, OpenAI a rapporté que gpt-4o-2024-08-06 avec les Sorties Structurées avait obtenu 100 % dans ses évaluations de schémas JSON complexes, tandis que gpt-4-0613 avait obtenu moins de 40 %. Vous n’avez pas besoin de traiter ces chiffres comme une vérité universelle pour voir le point plus large. L’application du schéma change la surface d’échec de « tout peut arriver » à « le contrat est beaucoup plus strict ».

Il reste des cas limites, et prétendre le contraire est la façon dont les démos de jouets deviennent des alertes de service. OpenAI documente que le modèle peut refuser une demande non sécurisée, et ces refus sont exposés en dehors de votre chemin de schéma normal. Il documente également les réponses incomplètes, y compris des cas tels que l’atteinte de max_output_tokens ou une interruption par un filtre de contenu. La FAQ « le mode JSON est-il suffisant pour une sortie LLM fiable » a donc une réponse courte et une réponse longue. La réponse courte est non. La réponse longue est que même une sortie structurée stricte nécessite une gestion explicite des échecs.

Où la sortie structurée échoue encore

L’application du schéma réduit le problème. Elle ne l’efface pas. Dans le trafic réel, vous voyez encore des charges utiles brisées ou surprenantes pour des raisons qui ont peu à voir avec la formulation de votre prompt.

Des formes d’échec à concevoir

Les modèles et les clients ne sont pas d’accord sur les détails. Vous pouvez obtenir du prose supplémentaire avant ou après le JSON, des blocs clôturés Markdown autour de la charge utile, ou un appel d’outil dont le nom est valide mais dont les arguments sont un JSON qui ne correspond pas à votre modèle Pydantic. Le streaming aggrave les choses car vous pourriez valider un tampon terminé à moitié. Le code défensif devrait supposer « chaîne en entrée, peut-être du JSON à l’intérieur » plutôt que « les octets sur le fil correspondent déjà à mon modèle ».

Différences entre fournisseurs et API

Tous les hôtes n’exposent pas la même surface de sortie structurée. Une pile peut vous donner une complétion liée à un schéma de première classe, une autre peut ne garantir que la syntaxe JSON, et les plates-formes locales peuvent être en retard par rapport aux API hébergées. C’est l’une des raisons pour lesquelles la FAQ « comment validez-vous le JSON LLM en Python » commence par l’application du fournisseur lorsque cela existe et se termine par la validation côté Python. Pour une vue plus large de la comparaison des fournisseurs, consultez la comparaison des sorties structurées entre les principaux fournisseurs LLM. Si vous exécutez des modèles localement, le même pipeline de validation s’applique après avoir normalisé le format du fil, par exemple après l’extraction avec Ollama comme dans sortie LLM structurée avec Ollama en Python et Go. Lorsqu’un runtime enveloppe encore le JSON avec des préfixes étranges ou des traces de raisonnement, attendez-vous à la même classe d’échecs de analyseur décrite dans problèmes de sortie structurée Ollama GPT-OSS.

La pile Python qui fonctionne vraiment

Ma recommandation est ennuyeuse par intention. D’abord, laissez le fournisseur du modèle appliquer le contrat structurel lorsqu’il le peut. Deuxièmement, validez la charge utile retournée en Python avec Pydantic. Troisièmement, utilisez une validation explicite des règles métier pour les faits qu’un schéma seul ne peut pas prouver. Quatrièmement, testez le contrat avec des fixtures et des exemples adverses au lieu de faire un signe de la main vers une capture d’écran de terrain de jeu et de considérer que c’est terminé. La documentation des Sorties Structurées d’OpenAI, le modèle de validateur de Pydantic, les outils jsonschema de Python et les exemples d’évaluation de sortie structurée d’OpenAI pointent tous dans cette direction.

Pydantic est le centre de gravité idéal pour Python. Il vous permet de modéliser la sortie comme des types Python normaux, de générer un JSON Schema avec model_json_schema(), et de valider du JSON brut avec model_validate_json(). La documentation de Pydantic note également que model_validate_json() est généralement le meilleur chemin plutôt que de faire json.loads(...) d’abord puis de valider, car cette route en deux étapes ajoute du travail d’analyse supplémentaire en Python.

Si vous conservez des fichiers de schéma autonomes dans votre dépôt, ou si vous voulez que CI valide les charges utiles des fixtures indépendamment du code du modèle, le package jsonschema de Python vous offre la vérification de contrat la plus simple avec jsonschema.validate(...). Si vous voulez cela dans pre-commit, check-jsonschema existe spécifiquement en tant qu’outil CLI et crochet pre-commit basé sur jsonschema. C’est un très bon ajustement pour les équipes qui veulent que les modifications de schéma soient révisées comme des modifications de code.

Les frameworks peuvent réduire la plomberie, mais ils ne suppriment pas le besoin de validation réelle. LangChain sélectionne désormais automatiquement la sortie structurée native du fournisseur lorsque le fournisseur le prend en charge et utilise une stratégie d’outil par défaut sinon. Instructor superpose des modèles de réponse Pydantic, la validation, les tentatives de reprise et la prise en charge multi-fournisseurs sur les appels de modèle. Guardrails se concentre sur les validateurs et les couches de garde d’entrée-sortie. Des outils utiles, tous les deux. Mais le schéma et les règles métier vous appartiennent toujours. Si vous choisissez entre des bibliothèques de haut niveau, la comparaison BAML vs Instructor pour Python est un complément utile à cet article.

Un exemple minimal OpenAI et Pydantic

L’exemple le plus petit digne de la production a quelques incontournables. Utilisez un ensemble fermé de valeurs semblables à des énumérations lorsque c’est possible. Interdisez les clés supplémentaires. Ajoutez des descriptions de champ pour que le schéma soit compréhensible par les humains et plus lisible par le modèle. Gardez l’objet racine explicite et ennuyeux. OpenAI recommande des noms clairs ainsi que des titres et descriptions pour les clés importantes, le JSON Schema utilise enum pour restreindre les valeurs, et Pydantic peut fermer la forme de l’objet avec extra="forbid".

from typing import Literal

from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field


class TicketClassification(BaseModel):
    model_config = ConfigDict(extra="forbid")

    category: Literal["billing", "bug", "how_to", "abuse"] = Field(
        description="Catégorie du ticket de support."
    )
    priority: Literal["low", "medium", "high"] = Field(
        description="Urgence opérationnelle."
    )
    needs_human: bool = Field(
        description="Si un humain doit examiner le cas."
    )
    summary: str = Field(
        description="Un résumé en une phrase du problème."
    )


client = OpenAI()

response = client.responses.parse(
    model="gpt-4o-2024-08-06",
    input=[
        {
            "role": "system",
            "content": "Classifiez les tickets de support. Retournez uniquement le résultat structuré.",
        },
        {
            "role": "user",
            "content": "Le client signale des facturations en double après avoir rafraîchi la page de paiement.",
        },
    ],
    text_format=TicketClassification,
)

result = response.output_parsed
print(result.model_dump())

Deux détails dans cet exemple sont faciles à manquer et absolument dignes d’intérêt. extra="forbid" côté Pydantic reflète l’idée du JSON Schema additionalProperties: false, qui est également une exigence pour les schémas d’outils stricts dans la documentation d’appel de fonction d’OpenAI. Et les énumérations ne sont pas cosmétiques. Elles sont l’un des moyens les plus simples d’empêcher le modèle d’inventer une valeur que votre code ne comprend pas.

Le SDK Python d’OpenAI prend en charge client.responses.parse(...) avec un modèle Pydantic fourni comme text_format, et l’objet analysé est retourné sur response.output_parsed. Le même SDK prend également en charge client.chat.completions.parse(...), où l’objet analysé se trouve sur message.parsed. Si vous voulez une extraction de données structurées directe avec un collage minimal, ces assistants sont le point de départ le plus propre.

Analyser, normaliser, puis valider

Les Sorties Structurées et model_validate_json éliminent beaucoup de douleurs d’analyse lorsque la pile est alignée de bout en bout. Le moment où vous prenez en charge un fournisseur qui retourne du texte de chat brut, un modèle qui enveloppe le JSON dans des clôturations, ou un chemin de journalisation qui stocke la chaîne de complétion brute, vous voulez un point de passage unique qui transforme le texte en dictionnaire avant que Pydantic ne s’exécute.

import json


def parse_json_from_llm_text(text: str) -> dict:
    cleaned = text.strip()
    if cleaned.startswith("```"):
        cleaned = cleaned.split("\n", 1)[1]
        cleaned = cleaned.rsplit("```", 1)[0].strip()

    # Préfixe courant "Bien sûr, voici le JSON :" avant l'objet.
    if not cleaned.startswith("{") and "{" in cleaned and "}" in cleaned:
        start = cleaned.find("{")
        end = cleaned.rfind("}")
        if end > start:
            cleaned = cleaned[start : end + 1]

    return json.loads(cleaned)


ticket_dict = parse_json_from_llm_text(raw_completion_text)
ticket = TicketClassification.model_validate(ticket_dict)

Cet assistant est intentionnellement ennuyeux. Il gère les blocs clôturés « json ... » et un préambule en langage naturel lorsque la charge utile est toujours un objet de niveau supérieur unique. Ce n’est pas un extracteur JSON complet. Si le modèle imbrique des accolades à l’intérieur de valeurs de chaîne, le tranchage naïf peut casser, et la bonne correction est généralement un prompt plus strict, des complétions liées au schéma, ou une bibliothèque d’analyse dédiée.

Complétions en streaming

Si vous stream des jetons de chat, n’exécutez pas json.loads ou model_validate_json sur chaque delta. Tamponnez jusqu’à ce que l’API signale un message terminé (vérifiez votre client pour la terminaison du flux ou finish_reason), concaténez le texte, puis analysez une seule fois. La même règle s’applique lorsque les arguments d’appel d’outil arrivent par fragments. Vous ne validez qu’après que la chaîne d’arguments soit complète.

chunks: list[str] = []
for chunk in completion_stream:
    delta = chunk.choices[0].delta.content or ""
    chunks.append(delta)
raw_completion_text = "".join(chunks)
ticket = TicketClassification.model_validate_json(raw_completion_text)

Vous pouvez toujours passer raw_completion_text à travers parse_json_from_llm_text d’abord lorsque vous attendez des clôtures ou des bavardages autour du JSON.

Une fois que vous possédez l’analyse de chaîne brute, la contrainte suivante est souvent non pas Python mais le dialecte JSON Schema du fournisseur et ce que l’API distante accepte réellement.

Limites du schéma du fournisseur (avant de devenir astucieux en Python)

Ne déversez pas aveuglément la sortie de tout générateur de schéma dans une API et ne supposez pas que chaque fonctionnalité JSON Schema est prise en charge. OpenAI prend en charge un sous-ensemble de JSON Schema, exige que tous les champs soient obligatoires pour les Sorties Structurées, exige que la racine soit un objet plutôt qu’un anyOf de niveau supérieur, et documente les limites de profondeur d’imbrication et le nombre total de propriétés. Gardez le schéma facing fournisseur simple. Ce n’est pas une concession. C’est une bonne ingénierie.

Si vous avez besoin d’un chemin de validation agnostique du fournisseur, ou si vous voulez valider les fixtures et mocks stockés, Pydantic plus jsonschema est toujours une excellente combinaison.

from jsonschema import validate as validate_json

schema = TicketClassification.model_json_schema()

payload = {
    "category": "bug",
    "priority": "high",
    "needs_human": True,
    "summary": "Le paiement duplique les charges après le rafraîchissement.",
}

validate_json(instance=payload, schema=schema)
ticket = TicketClassification.model_validate(payload)
print(ticket)

Ce modèle est particulièrement utile dans les tests, les fixtures de contrat et les intégrations où le fournisseur du modèle n’offre pas d’application native de sortie structurée. Rappelez-vous simplement qu’un schéma généré localement peut être plus large que le sous-ensemble pris en charge par un fournisseur donné, donc « valide localement » ne signifie pas automatiquement « accepté par chaque API LLM ». Notez également que certains fournisseurs prétraitent et mettent en cache les artefacts de schéma, donc la première demande pour un nouveau schéma peut être plus lente que les demandes chaudes.

Les appels d’outil sont un second contrat

L’appel de fonction ou d’outil est l’autre forme majeure de sortie structurée. Le modèle choisit un nom et passe des arguments qui devraient correspondre à un JSON Schema que vous contrôlez. OpenAI recommande strict: true sur les définitions d’outil afin que les arguments restent alignés avec ce schéma. Dans les piles lourdes en agents, un mauvais échantillonnage se transforme rapidement en JSON d’outil invalide ; gardez les paramètres d’échantillonnage alignés avec le travail multi-étapes en utilisant la référence des paramètres d’inférence agencique pour Qwen et Gemma.

Les extraits ci-dessous supposent que vous avez déjà mappé l’objet d’appel d’outil du fournisseur en une chaîne name et un dictionnaire arguments, par exemple en analysant tool_calls[].function sur les complétions de chat (les arguments de chaîne JSON deviennent json.loads d’abord). dispatch_tool est l’étape après cette normalisation.

Deux règles pratiques aident en Python. Premièrement, validez le nom de l’outil contre une liste blanche explicite avant de router l’exécution. Deuxièmement, validez le dictionnaire d’arguments avec le même modèle Pydantic que vous utilisez dans les tests, pas avec un accès de clé ad hoc. Le mode d’échec que vous évitez est « arguments JSON valides, forme incorrecte pour l’outil qui a déclenché », qui passe par les vérifications de chaîne.

from typing import Any, Callable

from pydantic import BaseModel

ToolHandler = Callable[[dict[str, Any]], str]


def dispatch_tool(
    *,
    name: str,
    arguments: dict[str, Any],
    handlers: dict[str, tuple[type[BaseModel], ToolHandler]],
) -> str:
    if name not in handlers:
        raise ValueError(f"outil non supporté {name}")
    model_cls, handler = handlers[name]
    validated = model_cls.model_validate(arguments)
    return handler(validated.model_dump())


handlers: dict[str, tuple[type[BaseModel], ToolHandler]] = {
    "classify_ticket": (
        TicketClassification,
        lambda data: f"mis en file comme {data['category']}",
    ),
}

Ce modèle garde le routage et la validation en un seul endroit. Vos gestionnaires réels seront plus riches, mais la séparation devrait rester la même : noms autorisés, arguments typés, puis effets secondaires.

La validation du schéma a toujours besoin de règles métier

Un objet valide n’est pas la même chose qu’un objet correct. OpenAI le dit directement. Les Sorties Structurées n’empêchent pas les erreurs à l’intérieur des valeurs de l’objet JSON. C’est pourquoi la FAQ « pourquoi la validation du schéma et la validation des règles métier sont-elles toutes deux importantes » a une réponse brutale. Parce qu’une réponse peut correspondre parfaitement au schéma et être quand même fausse d’une manière qui nuit à l’entreprise.

Voici un exemple réaliste. La structure peut être valide, mais la logique de tarification peut quand même être absurde.

from decimal import Decimal
from typing import Literal
from typing_extensions import Self

from pydantic import BaseModel, ConfigDict, Field, model_validator


class Offer(BaseModel):
    model_config = ConfigDict(extra="forbid")

    currency: Literal["USD", "EUR", "GBP"]
    amount: Decimal = Field(gt=0)
    original_amount: Decimal | None
    discounted: bool

    @model_validator(mode="after")
    def check_discount_logic(self) -> Self:
        if self.discounted:
            if self.original_amount is None:
                raise ValueError(
                    "original_amount est requis lorsque discounted est vrai"
                )
            if self.original_amount <= self.amount:
                raise ValueError(
                    "original_amount doit être supérieur à amount"
                )
        return self

Ce validateur fait quelque chose que les schémas seuls font souvent mal dans les systèmes réels. Il vérifie la sémantique entre les champs après que le modèle entier a été analysé. model_validator de Pydantic existe précisément pour ce type de validation d’objet entier. Remarquez le champ Decimal | None sans valeur par défaut. Cela garde le champ présent tout en permettant null, ce qui correspond au motif documenté d’OpenAI pour les valeurs semblables à optionnelles sous des Sorties Structurées strictes.

Si vous voulez que les échecs de validation soient renvoyés automatiquement au modèle, Instructor est une couche pratique sur Pydantic. Sa documentation décrit une boucle de reprise où les erreurs de validation sont capturées, formatées comme retour d’information, et utilisées pour demander au modèle d’essayer à nouveau.

import instructor

retrying_client = instructor.from_provider("openai/gpt-4o", max_retries=2)

offer = retrying_client.create(
    response_model=Offer,
    messages=[
        {
            "role": "user",
            "content": (
                "Extrayez l'offre de ce texte. "
                "49.00 USD, maintenant 19.00 USD."
            ),
        }
    ],
)

C’est l’une des rares commodités que je recommanderai volontiers. Les reprises automatiques liées à de véritables erreurs de validation sont utiles. La coercition silencieuse ne l’est pas. La couche de modèle d’Instructor, les docs de reprise et les docs de validation penchent tous vers cette même idée, et ils ont raison de le faire.

Vous pouvez implémenter la même idée sans framework. La boucle est petite. Demandez au modèle, validez avec Pydantic, et si la validation échoue, renvoyez les détails de l’erreur dans un message utilisateur de suivi et demandez uniquement le JSON corrigé. Limitez les tentatives, journalisez l’échec final, et exposez une erreur contrôlée aux appelants. Lorsque vous vous fiez déjà à responses.parse ou à d’autres assistants liés au schéma, vous exercerez rarement ce chemin. Il reste important pour le mode JSON, les points de terminaison de chat plus anciens, ou toute passerelle qui vous remet une chaîne brute.

from openai import OpenAI
from pydantic import ValidationError

client = OpenAI()

messages = [
    {"role": "system", "content": "Retournez uniquement le JSON qui correspond au schéma du ticket."},
    {"role": "user", "content": "Le client signale des facturations en double après avoir rafraîchi la page de paiement."},
]

ticket: TicketClassification | None = None
for attempt in range(2):
    completion = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=messages,
        response_format={"type": "json_object"},
    )
    raw_text = completion.choices[0].message.content or ""
    try:
        ticket = TicketClassification.model_validate_json(raw_text)
        break
    except ValidationError as exc:
        messages.append(
            {
                "role": "user",
                "content": f"La validation a échoué avec {exc.errors()}. Retournez uniquement le JSON corrigé.",
            }
        )
else:
    raise RuntimeError("tentatives de sortie structurée épuisées")

assert ticket is not None

Dans les services réels, vous attacheriez des ID de traçage, masqueriez le texte client dans les journaux, et distingueriez les erreurs de validation récupérables des refus ou des réponses incomplètes. La partie importante est que la reprise est entraînée par la sortie réelle du validateur, pas par un message générique « essayez à nouveau ».

Tester, relancer et échouer en mode fermé

Que devrait-il se passer lorsque la validation LLM échoue ? Pas un haussement d’épaules. Rejetez la charge utile, journalisez l’échec, relancez avec des tentatives bornées si la tâche vaut la peine d’être relancée, et échouez en mode fermé plutôt que de normaliser des ordures en quelque chose qui n’a l’air acceptable. C’est aussi là que beaucoup d’équipes oublient de gérer les refus et les sorties incomplètes explicitement, bien que la documentation du fournisseur leur dise que ces chemins existent.

Pour l’API Responses d’OpenAI, la gestion des échecs devrait être du code de première classe, pas une pensée après coup. La variable est response de client.responses.create ou parse, pas completion du streaming de chat ailleurs dans cet article.

if response.status == "incomplete":
    raise RuntimeError(response.incomplete_details.reason)

content = response.output[0].content[0]

if content.type == "refusal":
    raise RuntimeError(content.refusal)

Ce n’est pas une sur-ingénierie défensive. C’est directement aligné avec les modes d’échec documentés. Si le modèle refuse, vous ne tenez pas une charge utile valide selon le schéma. Si la réponse est incomplète, vous ne tenez pas une charge utile valide selon le schéma. Traitez les deux comme des branches explicites dans votre flux de contrôle.

Vous devriez également tester le contrat en dehors de l’appel du modèle lui-même.

import pytest
from jsonschema import validate as validate_json
from pydantic import ValidationError


def test_ticket_fixture_matches_schema():
    payload = {
        "category": "bug",
        "priority": "high",
        "needs_human": True,
        "summary": "Le paiement duplique les charges après le rafraîchissement.",
    }
    validate_json(instance=payload, schema=TicketClassification.model_json_schema())


def test_discount_logic_rejects_broken_offer():
    with pytest.raises(ValidationError):
        Offer.model_validate(
            {
                "currency": "USD",
                "amount": "19.00",
                "original_amount": "10.00",
                "discounted": True,
            }
        )


def test_ticket_rejects_unknown_category_string():
    with pytest.raises(ValidationError):
        TicketClassification.model_validate(
            {
                "category": "refund",
                "priority": "high",
                "needs_human": True,
                "summary": "Le client veut un remboursement.",
            }
        )


def test_ticket_rejects_extra_keys():
    with pytest.raises(ValidationError):
        TicketClassification.model_validate(
            {
                "category": "bug",
                "priority": "high",
                "needs_human": True,
                "summary": "Flux brisé.",
                "severity": "critical",
            }
        )

C’est la bonne forme de stratégie de test pour la validation de sortie LLM en Python. Validez les fixtures dorées avec jsonschema pour que chaque champ du contrat soit exercé. Validez la sémantique avec Pydantic, puis ajoutez des cas adverses tels que des chaînes d’énumération illégales, des clés supplémentaires interdites et des contradictions entre champs qui vous importent. Si vous prenez des instantanés des sorties réelles du modèle, masquez les données personnelles et traitez-les comme des fixtures de régression.

Si votre équipe vit dans la pile OpenAI, l’API Evals inclut également des recettes d’évaluation de sortie structurée spécifiquement pour tester et itérer sur des tâches qui dépendent de formats lisibles par machine. Et si vous conservez des fichiers de schéma bruts dans le dépôt, connectez check-jsonschema à CI ou pre-commit. Expédiez des contrats, pas des vibes.

Vérifications de production qui vous sauvent plus tard

Lorsque la validation échoue, la réponse de la FAQ est brutale. Rejetez la charge utile, journalisez pourquoi, relancez avec un retour d’information ciblé lorsque la tâche vaut une autre tentative, et échouez en mode fermé plutôt que de coercer de mauvaises données dans une file d’attente.

Une courte liste de contrôle opérationnelle aide les équipes à éviter les incidents répétés.

  • Journalisez la version du schéma ou un hachage du JSON Schema que vous avez envoyé au fournisseur afin que vous puissiez rejouer les échecs avec précision.
  • Masquez les entrées et sorties du modèle dans les journaux. Les journaux structurés sont inutiles s’ils fuient le texte client.
  • Émettez des compteurs ou des métriques pour le taux de refus, le taux de réponse incomplète, le taux d’échec de validation et le taux de réussite de réparation. Les pics là-bas battent le devinette quand un modèle ou une modification de prompt est déployée.

Les conseils plus larges sur l’observabilité pour les systèmes LLM aident à connecter ces signaux aux tableaux de bord, aux traces et aux revues SLO une fois que les compteurs existent.

La meilleure pratique n’est pas compliquée. Utilisez les Sorties Structurées côté fournisseur ou des schémas d’outils stricts lorsque vous le pouvez. Normalisez le texte brut lorsque vous devez. Miroir le contrat en Python avec Pydantic. Ajoutez la validation des règles métier pour ce que le schéma ne peut pas prouver. Gérez les refus et les réponses incomplètes comme des branches normales. Testez le contrat jusqu’à ce qu’il cesse d’être une démo et commence à être un logiciel. Tout le reste n’est que du cosplay d’ingénierie de prompt.

S'abonner

Recevez de nouveaux articles sur les systèmes, l'infrastructure et l'ingénierie IA.