En 2023 on priait pour que le LLM sorte du JSON propre. En 2026, on le force. Voici les 3 techniques qui marchent.
1. Schema-guided generation (la meilleure)
OpenAI et Anthropic supportent maintenant nativement le structured output : vous fournissez un JSON Schema, et le modèle est contraint au niveau du décodage à produire une sortie qui match. Pas un parsing ensuite, une contrainte pendant la génération.
response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
response_format: {
type: 'json_schema',
json_schema: {
name: 'invoice_extraction',
schema: {
type: 'object',
properties: {
invoice_number: { type: 'string' },
total: { type: 'number' },
items: { type: 'array', items: { type: 'string' } }
},
required: ['invoice_number', 'total']
},
strict: true
}
}
});Avec strict: true, c'est 100 % garanti. Plus de try/catch sur JSON.parse.
2. Tool use comme surrogate
Claude supporte les structured outputs via le tool use : vous définissez un "tool" dont le schéma d'input est votre sortie désirée, et vous forcez son appel. Le modèle vous retourne l'objet typé parfaitement.
tools: [{
name: 'return_analysis',
input_schema: {
type: 'object',
properties: {
sentiment: { type: 'string', enum: ['positive', 'neutral', 'negative'] },
confidence: { type: 'number' }
}
}
}],
tool_choice: { type: 'tool', name: 'return_analysis' }3. Instructor / constrained decoding (open source)
Instructor (Python) ou Ollama en local permettent du constrained decoding contre un schéma Pydantic/Zod. Valider en post, si invalide, retry avec l'erreur en prompt. Fonctionne avec tout modèle, utile pour les modèles open source.
Les pièges
- Énumérations mal pensées : trop de valeurs possibles = le modèle dérive. Gardez < 20 valeurs par enum.
- Schémas trop imbriqués : au-delà de 3 niveaux, la qualité chute même en mode strict.
- Descriptions manquantes : chaque champ devrait avoir une description claire, le modèle l'utilise pour décider quoi mettre.
Mon pattern en 2026
// 1. Définir le schéma Zod côté TS
const InvoiceSchema = z.object({
number: z.string(),
total: z.number().positive(),
items: z.array(z.string())
});
// 2. Convertir en JSON Schema pour l'API
const schema = zodToJsonSchema(InvoiceSchema);
// 3. Appel en mode structured
const raw = await callLLM(schema, prompt);
// 4. Validation Zod en defense en profondeur
const invoice = InvoiceSchema.parse(raw);99,9 % de fiabilité sur mes apps en prod. Besoin d'intégrer ça chez vous ?