Sommaire

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({\n  model: 'gpt-4o',\n  messages,\n  response_format: {\n    type: 'json_schema',\n    json_schema: {\n      name: 'invoice_extraction',\n      schema: {\n        type: 'object',\n        properties: {\n          invoice_number: { type: 'string' },\n          total: { type: 'number' },\n          items: { type: 'array', items: { type: 'string' } }\n        },\n        required: ['invoice_number', 'total']\n      },\n      strict: true\n    }\n  }\n});

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: [{\n  name: 'return_analysis',\n  input_schema: {\n    type: 'object',\n    properties: {\n      sentiment: { type: 'string', enum: ['positive', 'neutral', 'negative'] },\n      confidence: { type: 'number' }\n    }\n  }\n}],\ntool_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\nconst InvoiceSchema = z.object({\n  number: z.string(),\n  total: z.number().positive(),\n  items: z.array(z.string())\n});\n\n// 2. Convertir en JSON Schema pour l'API\nconst schema = zodToJsonSchema(InvoiceSchema);\n\n// 3. Appel en mode structured\nconst raw = await callLLM(schema, prompt);\n\n// 4. Validation Zod en defense en profondeur\nconst invoice = InvoiceSchema.parse(raw);

C'est l'assurance d'une excellente fiabilité en production. Besoin d'intégrer ça chez vous ?