Pourquoi les sorties structurées sont indispensables en 2026
Les grands modèles de langage sont incroyablement doués pour générer du texte. Mais quand il s'agit de produire des données structurées fiables pour un système de production, c'est une autre histoire.
Que vous cherchiez à extraire des entités d'un document, remplir un formulaire automatiquement ou alimenter un pipeline de données, le JSON brut craché par un LLM sans contrainte pose des problèmes bien réels : champs manquants, types incorrects, formats qui changent d'un appel à l'autre. Bref, un cauchemar pour quiconque a déjà essayé de parser ce genre de sorties en production.
Heureusement, en 2026, trois approches complémentaires permettent d'obtenir des sorties structurées fiables — les API natives des fournisseurs, la bibliothèque Instructor et le framework Pydantic AI. On va les passer en revue ensemble, avec du code fonctionnel pour chacune.
Les 3 niveaux de sorties structurées
Avant de plonger dans le code, prenons un moment pour comprendre les trois niveaux de structuration disponibles. C'est important pour choisir la bonne approche selon votre contexte.
Niveau 1 — Prompt Engineering (peu fiable)
Demander au modèle de retourner du JSON via le prompt. Ça fonctionne 80 à 95 % du temps, mais ça échoue silencieusement sur les cas limites. Aucune garantie de type. Honnêtement, à éviter en production.
Niveau 2 — Function Calling / Tool Use (fiable)
Le schéma est transmis au modèle via l'API de tool calling. On monte à 95-99 % de fiabilité. Le schéma sert de guide fort, mais ce n'est pas encore une contrainte absolue — le modèle peut parfois s'en écarter.
Niveau 3 — Structured Output natif (garanti)
Décodage contraint avec JSON Schema. 100 % de conformité grâce à des machines à états finis qui masquent les tokens invalides lors de la génération. C'est le standard de production en 2026, et franchement, c'est celui que vous devriez viser.
Approche 1 : API natives — OpenAI et Claude
Sorties structurées avec le SDK Anthropic (Claude)
Depuis fin 2025, Claude supporte nativement les sorties structurées. Le SDK Python fournit une méthode .parse() qui accepte un modèle Pydantic et retourne directement un objet typé, validé automatiquement. Pas besoin de parser du JSON à la main.
from pydantic import BaseModel
from anthropic import Anthropic
class InfoContact(BaseModel):
nom: str
email: str
plan_interesse: str
demo_demandee: bool
client = Anthropic()
response = client.messages.parse(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Extrais les informations clés de cet email : "
"Jean Dupont ([email protected]) est intéressé "
"par notre offre Enterprise et souhaite planifier "
"une démo mardi prochain à 14h.",
}
],
output_format=InfoContact,
)
# Accès direct à l'objet Pydantic typé
print(response.parsed_output)
print(response.parsed_output.nom) # "Jean Dupont"
print(response.parsed_output.email) # "[email protected]"
Les modèles supportés incluent Claude Opus 4.6, Sonnet 4.6, Sonnet 4.5, Opus 4.5 et Haiku 4.5. La fonctionnalité est disponible via l'API Anthropic et Amazon Bedrock.
Mode strict pour le tool use
Si vous utilisez le tool use (function calling) de Claude, le mode strict est un vrai game-changer. Il valide les paramètres des outils côté modèle, ce qui garantit que Claude appelle vos fonctions avec des arguments correctement typés. Sans ce mode, le modèle pourrait retourner des types incompatibles ou carrément oublier des champs requis.
from anthropic import Anthropic
client = Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "Quelle météo à Paris ?"}
],
tools=[
{
"name": "obtenir_meteo",
"description": "Obtenir la météo actuelle",
"strict": True,
"input_schema": {
"type": "object",
"properties": {
"ville": {
"type": "string",
"description": "Nom de la ville"
},
"unite": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["ville"],
"additionalProperties": False
}
}
],
)
print(response.content)
Configuration directe via output_config
Pour un contrôle plus fin sans utiliser Pydantic côté serveur, il y a aussi l'option output_config avec un schéma JSON brut. C'est pratique quand vous voulez garder les choses simples ou quand votre schéma est généré dynamiquement.
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Extrais les informations de : Marie Martin "
"([email protected]), offre Pro, pas de démo."
}
],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"nom": {"type": "string"},
"email": {"type": "string"},
"plan": {"type": "string"},
"demo": {"type": "boolean"}
},
"required": ["nom", "email", "plan", "demo"],
"additionalProperties": False
}
}
},
)
# Retourne du JSON valide garanti
print(response.content[0].text)
Sorties structurées avec OpenAI
Côté OpenAI, l'approche est similaire. Le SDK Python propose aussi une méthode .parse() qui accepte un modèle Pydantic, mais cette fois via le paramètre response_format :
from pydantic import BaseModel
from openai import OpenAI
class Evenement(BaseModel):
nom: str
date: str
lieu: str
participants: list[str]
client = OpenAI()
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "Extrais les détails de l'événement."},
{"role": "user", "content": "La conférence IA Paris 2026 se tiendra "
"le 15 mai au Palais des Congrès avec "
"Yann LeCun et Luc Julia."}
],
response_format=Evenement,
)
evenement = completion.choices[0].message.parsed
print(evenement.nom) # "Conférence IA Paris 2026"
print(evenement.lieu) # "Palais des Congrès"
Approche 2 : Instructor — extraction multi-fournisseur
Instructor, c'est un peu la star des bibliothèques d'extraction structurée en Python. Plus de 3 millions de téléchargements mensuels, 11 000 étoiles sur GitHub — elle est devenue la référence pour les développeurs qui veulent des sorties validées sans se prendre la tête.
Et pour cause : elle résout un problème très concret que les SDK natifs ne gèrent pas forcément bien.
Installation et configuration
# Installation avec le support Anthropic
pip install "instructor[anthropic]"
# Ou pour tous les fournisseurs
pip install "instructor[all]"
Extraction structurée basique
Instructor s'appuie sur Pydantic pour définir les schémas et ajoute trois choses essentielles par-dessus : la validation automatique, les retries intelligents et le support multi-fournisseur. Voyons ça en action.
import instructor
from pydantic import BaseModel, Field
from typing import List
class Propriete(BaseModel):
nom: str = Field(description="Nom de la propriété")
valeur: str = Field(description="Valeur de la propriété")
class Utilisateur(BaseModel):
nom: str = Field(description="Nom complet")
age: int = Field(description="Âge en années")
proprietes: List[Propriete] = Field(
description="Liste des propriétés"
)
# Client Claude via Instructor
client = instructor.from_provider(
"anthropic/claude-sonnet-4-5",
mode=instructor.Mode.TOOLS
)
utilisateur = client.create(
max_tokens=1024,
messages=[
{
"role": "user",
"content": "Crée un profil pour Marie, 32 ans, "
"développeuse Python senior à Lyon."
}
],
response_model=Utilisateur,
)
print(utilisateur.model_dump_json(indent=2))
Retries automatiques sur échec de validation
C'est là qu'Instructor brille vraiment. Imaginez que le modèle retourne age: -5 alors que votre modèle Pydantic spécifie ge=0. Au lieu de planter, Instructor intercepte l'erreur de validation, la renvoie au modèle en lui expliquant le problème, et retente automatiquement. Ça a l'air tout bête, mais en production, ça change tout.
from pydantic import BaseModel, Field
class AnalyseSentiment(BaseModel):
texte: str
score: float = Field(ge=-1.0, le=1.0,
description="Score entre -1 (négatif) et 1 (positif)")
label: str = Field(
pattern="^(positif|négatif|neutre)$",
description="Label de sentiment"
)
# max_retries=3 par défaut
resultat = client.create(
messages=[
{
"role": "user",
"content": "Analyse le sentiment : "
"'Ce produit est absolument fantastique !'"
}
],
response_model=AnalyseSentiment,
max_retries=3,
)
print(f"Score : {resultat.score}, Label : {resultat.label}")
Changement de fournisseur sans modification de code
Un des gros atouts d'Instructor, c'est la portabilité. Vous changez de modèle en modifiant littéralement une seule ligne de code. Le reste — vos schémas Pydantic, votre logique métier — reste identique.
# Utiliser Claude
client = instructor.from_provider("anthropic/claude-sonnet-4-5")
# Basculer sur OpenAI — même code, même schéma
client = instructor.from_provider("openai/gpt-4o")
# Ou un modèle local avec Ollama
client = instructor.from_provider("ollama/llama3.1")
# Le reste du code est identique
resultat = client.create(
messages=[{"role": "user", "content": "..."}],
response_model=MonModele,
)
Streaming de sorties partielles
Pour les réponses longues, Instructor supporte le streaming d'objets partiels qui se remplissent au fur et à mesure. C'est particulièrement utile pour afficher une progression côté interface utilisateur.
class ProfilComplet(BaseModel):
nom: str = Field(description="Nom complet")
bio: str = Field(description="Biographie détaillée")
competences: list[str] = Field(description="Compétences clés")
for partiel in client.create_partial(
messages=[
{"role": "user", "content": "Crée un profil détaillé de développeur IA"}
],
response_model=ProfilComplet,
max_tokens=4096,
):
# L'objet se remplit progressivement
print(f"État actuel : {partiel}")
Approche 3 : Pydantic AI — le framework agentique
Pydantic AI est le framework d'agents officiel de l'équipe derrière Pydantic. Avec plus de 15 500 étoiles sur GitHub, il ambitionne d'apporter l'expérience développeur de FastAPI au monde de l'IA générative. Concrètement, ça veut dire : typage fort, injection de dépendances et validation automatique.
Si vous avez déjà travaillé avec FastAPI, vous allez vous sentir en terrain connu.
Agent avec sortie structurée
from pydantic import BaseModel
from pydantic_ai import Agent
class AnalyseCode(BaseModel):
langage: str
complexite: str
problemes: list[str]
suggestions: list[str]
agent = Agent(
"anthropic:claude-sonnet-4-5",
output_type=AnalyseCode,
system_prompt="Tu es un expert en revue de code Python."
)
resultat = agent.run_sync(
"Analyse ce code : def f(x): return x if x > 0 else -x"
)
print(resultat.output)
# AnalyseCode(langage='Python', complexite='faible', ...)
Tools typés avec injection de dépendances
Pydantic AI permet de déclarer des outils que le LLM peut appeler pendant l'exécution, avec un système d'injection de dépendances type-safe. C'est clairement inspiré de FastAPI (et c'est un compliment).
from dataclasses import dataclass
from pydantic_ai import Agent, RunContext
import httpx
@dataclass
class Deps:
client_http: httpx.AsyncClient
cle_api: str
agent = Agent(
"anthropic:claude-sonnet-4-5",
deps_type=Deps,
system_prompt="Tu es un assistant météo."
)
@agent.tool
async def obtenir_meteo(
ctx: RunContext[Deps], ville: str
) -> str:
"""Obtenir la météo actuelle pour une ville."""
response = await ctx.deps.client_http.get(
f"https://api.meteo.fr/v1/current",
params={"city": ville},
headers={"Authorization": f"Bearer {ctx.deps.cle_api}"}
)
return response.text
async def main():
async with httpx.AsyncClient() as http_client:
deps = Deps(
client_http=http_client,
cle_api="votre-cle-api"
)
result = await agent.run(
"Quelle météo fait-il à Lyon ?",
deps=deps
)
print(result.output)
Cas d'usage avancés
Extraction de factures depuis des PDF
Instructor supporte l'extraction multimodale depuis des images et des PDF. C'est un cas d'usage très demandé en entreprise, et pour avoir testé plusieurs approches, celle-ci est de loin la plus ergonomique.
from pydantic import BaseModel
from instructor.processing.multimodal import PDF
class LigneFacture(BaseModel):
description: str
quantite: int
prix_unitaire: float
class Facture(BaseModel):
numero: str
date: str
client: str
lignes: list[LigneFacture]
total_ht: float
tva: float
total_ttc: float
client = instructor.from_provider("anthropic/claude-sonnet-4-5")
facture = client.create(
response_model=Facture,
max_tokens=2000,
messages=[{
"role": "user",
"content": [
"Extrais toutes les données de cette facture",
PDF.from_path("facture_mars_2026.pdf"),
],
}],
)
print(f"Total TTC : {facture.total_ttc} €")
Appels parallèles avec types multiples
Petit truc sympa : Instructor détecte automatiquement le mode parallèle quand votre response_model est un Iterable[Union[...]]. Le modèle peut alors retourner plusieurs types d'objets en un seul appel.
from typing import Iterable, Literal
class RechercheWeb(BaseModel):
requete: str
class Meteo(BaseModel):
ville: str
unite: Literal["celsius", "fahrenheit"]
class Calcul(BaseModel):
expression: str
resultats = client.create(
messages=[
{"role": "system", "content": "Utilise toujours les outils."},
{"role": "user", "content": "Météo à Paris et Lyon, "
"et cherche 'IA en France 2026'"}
],
response_model=Iterable[Meteo | RechercheWeb],
)
for r in resultats:
print(type(r).__name__, r)
Quelle approche choisir ?
Bon, avec trois options sur la table, comment trancher ? Ça dépend vraiment de votre contexte.
| Approche | Idéal pour | Avantages | Limites |
|---|---|---|---|
| SDK natif (.parse()) | Projet mono-fournisseur, schémas simples | Zéro dépendance externe, performance maximale | Pas de retry automatique, verrouillage fournisseur |
| Instructor | Extraction de données, multi-fournisseur | Retries, streaming, 15+ fournisseurs, léger | Pas de gestion d'agents, pas d'injection de dépendances |
| Pydantic AI | Agents complets avec outils et workflows | DI, tools typés, graphes, exécution durable | Plus lourd, courbe d'apprentissage plus raide |
Mon conseil : commencez par le SDK natif pour vos prototypes et preuves de concept. Dès que vous avez besoin de validation robuste ou de portabilité entre fournisseurs, passez à Instructor. Et quand votre application exige des agents avec outils, état conversationnel et injection de dépendances, Pydantic AI devient le choix logique.
Bonnes pratiques pour la production
Validation Pydantic au-delà du schéma JSON
Un point que beaucoup de développeurs négligent : les contraintes JSON Schema (comme minimum, maximum, minLength) sont souvent supprimées par les SDK avant l'envoi au modèle. Pydantic, lui, les applique côté client, créant un filet de sécurité supplémentaire. Utilisez toujours des validateurs Pydantic, même quand le fournisseur garantit la conformité au schéma.
from pydantic import BaseModel, Field, field_validator
class CommandeProduit(BaseModel):
produit: str
quantite: int = Field(ge=1, le=1000)
prix: float = Field(ge=0)
code_promo: str | None = None
@field_validator("code_promo")
@classmethod
def valider_promo(cls, v):
if v and not v.startswith("PROMO-"):
raise ValueError("Code promo invalide")
return v
Gérer les schémas complexes sans exploser les coûts
Les schémas JSON complexes augmentent la consommation de tokens — parfois de manière significative. En production, divisez les grands schémas en appels plus petits et parallélisés. Un schéma à 20 champs imbriqués coûtera nettement plus cher qu'un schéma plat à 5 champs. C'est un compromis à garder en tête dès la conception.
Fallback multi-fournisseur
Aucun fournisseur n'est fiable à 100 %, même en 2026. Pour les chemins critiques de votre application, construisez des chaînes de fallback. Instructor facilite grandement cette approche grâce à son API unifiée — si Claude rencontre un problème, vous basculez sur GPT-4o sans toucher au code métier.
FAQ
Quelle est la différence entre function calling et structured output ?
Le function calling (ou tool use) permet au LLM de générer un appel de fonction structuré que votre code exécute ensuite. Les structured outputs vont plus loin : ils appliquent un décodage contraint qui rend le modèle physiquement incapable de générer du JSON non conforme au schéma. En 2026, les structured outputs natifs sont le standard pour l'extraction de données, tandis que le function calling reste pertinent pour l'interaction avec des APIs externes.
Instructor est-il compatible avec les modèles open source locaux ?
Tout à fait. Instructor supporte plus de 15 fournisseurs, y compris Ollama pour les modèles locaux comme Llama 3.1 et Mistral. La syntaxe reste identique : changez simplement le fournisseur dans from_provider(). À noter que les performances de structured output varient selon le modèle — les modèles plus grands offrent généralement une meilleure conformité au schéma.
Comment gérer les hallucinations dans les sorties structurées ?
C'est une question qui revient souvent. Les sorties structurées garantissent la conformité au format, pas l'exactitude factuelle. Ce sont deux problèmes distincts. Pour combattre les hallucinations, combinez la validation Pydantic (format et plages de valeurs) avec une validation métier qui vérifie les données contre une source de vérité. Les validateurs personnalisés de Pydantic sont parfaits pour appliquer ces règles côté client.
Pydantic AI remplace-t-il Instructor ?
Non, et c'est important de le comprendre : ils sont complémentaires. Instructor excelle dans l'extraction de données structurées — c'est un outil léger et ciblé qui fait très bien son job. Pydantic AI est un framework complet pour construire des agents avec outils, injection de dépendances et état conversationnel. Si vous avez juste besoin d'extraire du JSON validé, Instructor est plus simple. Si vous construisez un agent complexe, Pydantic AI est le meilleur choix.
Le structured output fonctionne-t-il avec le streaming ?
Oui, et c'est plutôt bien fait. Instructor propose create_partial() pour recevoir des objets Pydantic partiels qui se remplissent progressivement. Pydantic AI supporte également le streaming structuré natif. Attention cependant : en mode streaming, évitez de déclarer des validateurs dans le modèle de réponse, car ils interrompraient le processus avant que l'objet ne soit complet.