Nehos Groupe

Ce qu'il faut retenir

Les LLM produisent nativement du texte libre — obtenir du JSON valide et prévisible en production nécessite une stratégie explicite : JSON mode, structured outputs (OpenAI), tool use (Anthropic) ou function calling.

La librairie Instructor (Python et TypeScript) associe un schema Pydantic ou Zod au prompt, valide la sortie automatiquement et gère les retries avec self-repair — c'est l'approche la plus robuste pour la majorité des cas d'usage.

Les structured outputs ajoutent entre 5 et 25 % de latence et un surcoût token marginal, mais éliminent les erreurs de parsing en production — un échange largement rentable pour des pipelines métiers critiques.

Extraction de factures, qualification de leads, analyse de contrats, classification d’emails : ce sont les quatre cas d’usage B2B où les structured outputs démontrent immédiatement leur valeur.

Structured outputs LLM 2026 : produire du JSON fiable en production

JSON mode, function calling, Instructor, Zod, Pydantic : comment passer d'un LLM qui hallucine du JSON cassé à un pipeline structuré prêt pour la production.

Adapté à toute taille de structure

Artisan
Startup
PME / TPE
ETI
Grand Groupe
C
Chokri Siala
··15-blog-ia-llm

#Le problème : pourquoi les LLM produisent du JSON invalide en production

Quand une équipe technique intègre un LLM pour la première fois dans un système métier, la première surprise arrive rapidement : le modèle produit « presque » du JSON. Presque, c'est le mot qui fait mal en production.

Les cas d'échec les plus fréquents que nous observons chez Nehos :

  • Markdown wrapping : le modèle enveloppe le JSON dans des balises json ... , ce qui casse JSON.parse() immédiatement
  • Trailing commas : une virgule tras la dernière clé d'un objet, valide en JavaScript 2015+ mais invalide en JSON strict
  • Champs hallucinés : le modèle ajoute des clés non demandées, ou omet des champs obligatoires selon son interprétation du prompt
  • Types incoherents : un champ amount retourné comme string "1500.00" au lieu de number 1500
  • Truncation : sur des réponses longues, le modèle tronque le JSON avant la fermeture des accolades, produisant un document non parseable

Ces erreurs sont non déterministes : elles surviennent parfois, pas systématiquement. C'est exactement ce qui les rend dangereuses en production — le pipeline fonctionne 95 % du temps, et l’équipe découvre le problème le jour où un batch d'extraction de factures s'arrête à 3h du matin.

La solution n'est pas d’écrire de meilleurs prompts. La solution est d’utiliser les mécanismes de structured outputs conçus spécifiquement pour ce problème.


#JSON mode vs structured outputs : différences entre OpenAI, Anthropic et Mistral

Chaque fournisseur a adopté une approche différente, avec des garanties différentes. Comprendre ces nuances évite des surprises d'architecture.

#OpenAI : JSON mode et Structured Outputs (deux choses distinctes)

OpenAI propose deux mécanismes différents que beaucoup de développeurs confondent.

JSON mode (response_format: { type: "json_object" }) : garantit que la sortie sera un JSON syntaxiquement valide. Mais il ne valide pas la structure — le modèle peut produire n'importe quel objet JSON valide, pas forcément celui que vous attendez.

Structured Outputs (response_format: { type: "json_schema", json_schema: {...} }) : introduit en août 2024, ce mode garantit que la sortie respecte exactement le JSON Schema fourni. C'est une contrainte au niveau du décodage des tokens — techniquement, le modèle ne peut pas produire une sortie qui viole le schéma. Disponible sur gpt-4o et gpt-4o-mini.

// OpenAI Structured Outputs avec json_schema
const response = await openai.chat.completions.create({
  model: "gpt-4o",
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "invoice_extraction",
      schema: {
        type: "object",
        properties: {
          vendor: { type: "string" },
          amount: { type: "number" },
          dueDate: { type: "string", format: "date" },
          lineItems: {
            type: "array",
            items: {
              type: "object",
              properties: {
                description: { type: "string" },
                unitPrice: { type: "number" },
                quantity: { type: "integer" }
              },
              required: ["description", "unitPrice", "quantity"],
              additionalProperties: false
            }
          }
        },
        required: ["vendor", "amount", "dueDate", "lineItems"],
        additionalProperties: false
      },
      strict: true
    }
  },
  messages: [{ role: "user", content: prompt }]
});

Point clé : le flag strict: true est indispensable. Sans lui, OpenAI ne garantit pas le respect intégral du schéma.

#Anthropic Claude : tool use comme mécanisme de structured output

Anthropic ne propose pas de JSON mode natif au sens OpenAI. L'approche recommandée est le tool use (ou function calling) : vous déclarez un outil fictif dont les paramètres définissent la structure attendue, puis vous forcez le modèle à appeler cet outil.

// Anthropic tool use pour structured output
const response = await anthropic.messages.create({
  model: "claude-opus-4-5",
  max_tokens: 1024,
  tools: [{
    name: "extract_invoice",
    description: "Extraire les donn\u00e9es structur\u00e9es d'une facture",
    input_schema: {
      type: "object",
      properties: {
        vendor: { type: "string", description: "Nom du fournisseur" },
        amount: { type: "number", description: "Montant total TTC en euros" },
        dueDate: { type: "string", description: "Date d'\u00e9ch\u00e9ance ISO 8601" }
      },
      required: ["vendor", "amount", "dueDate"]
    }
  }],
  tool_choice: { type: "tool", name: "extract_invoice" },
  messages: [{ role: "user", content: prompt }]
});
const data = response.content[0].input;

Le tool_choice: { type: "tool", name: "extract_invoice" } force le modèle à appeler cet outil spécifique — garantissant une sortie structurée.

#Mistral : JSON mode et function calling

Mistral propose un JSON mode (response_format: { type: "json_object" }) sur ses modèles Instruct et Large. La garantie est similaire à celle du JSON mode OpenAI : JSON syntaxiquement valide, mais sans validation de schéma native. Pour une validation stricte, il faut coupler avec Instructor ou une validation Zod/Pydantic côté applicatif.

FonctionnalitéOpenAI gpt-4oAnthropic ClaudeMistral Large
JSON mode basiqueOuiNon natifOui
Structured Outputs (schema strict)Oui (json_schema)Via tool usePartiel
Function callingOuiOui (tool use)Oui
Garantie de conformité schémaForte (strict mode)Forte (tool use)Moyenne
Latence ajoutée+8-15 %+10-20 %+5-12 %

Pour une comparaison plus large des forces de chaque modèle, consultez notre article Mistral vs OpenAI vs Anthropic : quel LLM choisir en 2026 ?.


#Zod et TypeScript : valider les sorties LLM côté application

Même avec le JSON mode activé, une couche de validation côté applicatif reste indispensable. En TypeScript, Zod s'est imposé comme le standard de facto.

Pourquoi Zod plutôt que des assertions manuelles ? Trois raisons techniques :

  1. Inférence de type automatique : z.infer<typeof InvoiceSchema> génère le type TypeScript correspondant, sans duplication de code
  2. Messages d'erreur précis : Zod retourne exactement quel champ est invalide et pourquoi — utile pour le debugging et le self-repair
  3. Transformations intégrées : .transform() permet de coercer une string "1500.50" en number 1500.50 directement dans le schéma
import { z } from "zod";

const LineItemSchema = z.object({
  description: z.string().min(1),
  unitPrice: z.number().positive(),
  quantity: z.number().int().positive(),
  total: z.number().optional(),
});

const InvoiceSchema = z.object({
  vendor: z.string().min(1),
  invoiceNumber: z.string(),
  amount: z.number().positive(),
  currency: z.enum(["EUR", "USD", "GBP"]).default("EUR"),
  dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  lineItems: z.array(LineItemSchema).min(1),
  isPaid: z.boolean().default(false),
});

type Invoice = z.infer<typeof InvoiceSchema>;

// Validation après réponse LLM
const rawJson = JSON.parse(llmResponse);
const result = InvoiceSchema.safeParse(rawJson);

if (!result.success) {
  console.error("LLM output invalide:", result.error.format());
  // Déclencher un retry avec le message d'erreur Zod
} else {
  const invoice: Invoice = result.data;
  // Traitement sécurisé
}

La méthode .safeParse() ne lève pas d'exception — elle retourne un objet { success: true, data } ou { success: false, error }. C'est le pattern recommandé pour les systèmes qui doivent gérer les erreurs sans interrompre le flux.


#Instructor library : l'outil qui change tout pour TypeScript et Python

Instructor est une librairie open source créée par Jason Liu qui résout élégamment le problème complet : définir un schéma, appeler le LLM, valider la sortie et gérer les retries avec auto-correction — en quelques lignes.

Disponible en Python (Pydantic) et TypeScript (Zod). C'est l'une des rares librairies qui fonctionne avec OpenAI, Anthropic, Mistral, Cohere et les modèles Ollama locaux avec la même API.

// Instructor TypeScript avec Zod
import Instructor from "@instructor-ai/instructor";
import OpenAI from "openai";
import { z } from "zod";

const oai = new OpenAI();
const client = Instructor({ client: oai, mode: "TOOLS" });

const ContractRiskSchema = z.object({
  riskLevel: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
  terminationClause: z.string().describe("Clause de résiliation textuelle"),
  penaltyAmount: z.number().nullable().describe("Pénalités en euros, null si absentes"),
  autoRenewal: z.boolean(),
  keyDates: z.array(z.object({
    label: z.string(),
    date: z.string(),
  })),
  summary: z.string().max(500),
});

type ContractRisk = z.infer<typeof ContractRiskSchema>;

const analysis = await client.chat.completions.create({
  model: "gpt-4o",
  response_model: { schema: ContractRiskSchema, name: "ContractRisk" },
  max_retries: 3,
  messages: [
    {
      role: "system",
      content: "Tu es un expert juridique. Analyse le contrat et extrais les informations demandées."
    },
    { role: "user", content: contractText }
  ],
});

// analysis est typé ContractRisk, garanti valide
console.log(analysis.riskLevel); // "HIGH"

Avec max_retries: 3, Instructor réessaie automatiquement en injectant le message d'erreur Zod dans le prompt de correction. Le modèle voit son erreur et se corrige — c'est le pattern self-repair qui monte le taux de succès à 99,5 %+ sur les schémas bien conçus.

Pour les équipes qui travaillent sur des systèmes d'agents plus larges, ce pattern s'intègre naturellement dans les architectures décrites dans notre article sur LangGraph vs CrewAI vs n8n.


#Function calling et tool use : l'approche la plus robuste

Le function calling (OpenAI) et le tool use (Anthropic) sont techniquement les mécanismes les plus robustes pour obtenir des structured outputs, car ils opèrent au niveau du décodage du modèle — pas seulement au niveau du prompt.

Comment ça fonctionne en interne ? Quand vous déclarez une fonction, le modèle est entraîné à produire des appels de fonction au format JSON strict. Les tokens possibles à chaque position sont contraints par le schéma — le modèle ne peut littéralement pas générer un token invalide.

La différence avec un simple JSON mode : le JSON mode contraint uniquement la structure JSON globale (accolades, virgules, guillemets). Le function calling contraint également les clés, les types et les valeurs autorisées selon le schéma déclaré.

Pattern recommandé pour les extractions critiques :

// Pattern function calling avec parallel tool calls
const tools = [
  {
    type: "function",
    function: {
      name: "extract_contract_metadata",
      description: "Extraire les métadonnées structurées d'un contrat B2B",
      parameters: {
        type: "object",
        properties: {
          parties: {
            type: "array",
            items: {
              type: "object",
              properties: {
                name: { type: "string" },
                role: { type: "string", enum: ["CLIENT", "VENDOR", "THIRD_PARTY"] },
                siret: { type: "string", pattern: "^[0-9]{14}$" }
              },
              required: ["name", "role"]
            }
          },
          contractValue: { type: "number" },
          startDate: { type: "string", format: "date" },
          endDate: { type: "string", format: "date" },
          renewalType: { type: "string", enum: ["AUTO", "MANUAL", "NONE"] }
        },
        required: ["parties", "contractValue", "startDate", "renewalType"],
        additionalProperties: false
      }
    }
  }
];

Pour les agents IA qui utilisent ces outils en chaîne, voir notre guide sur les agents IA : définition et cas d'usage B2B et l'article sur MCP (Model Context Protocol).


#Pydantic et Zod schemas : définir des contrats de sortie précis

La qualité du schéma détermine directement la qualité de l'extraction. Voici les règles de design que nous appliquons systématiquement chez Nehos.

#Règle 1 : descriptions dans le schéma, pas seulement dans le prompt

Les descriptions de champ (description, Field(description=...) en Pydantic) sont injectées dans le contexte du modèle. Un champ amount sans description sera interprété différemment selon le contexte. Avec description: "Montant total TTC en euros, sans le symbole devise", l'ambiguïté disparait.

#Règle 2 : additionalProperties: false systématiquement

Sans cette contrainte, le modèle peut ajouter des champs non prévus qui cassent la validation downstream. En Zod : .strict() ou .strip() selon que vous voulez rejeter ou ignorer les champs supplémentaires.

#Règle 3 : typer les enums plutôt que des strings libres

# Pydantic - mauvais
class Invoice(BaseModel):
    status: str  # Le modèle peut écrire "paid", "Paid", "PAID", "payée"...

# Pydantic - bien
from enum import Enum

class InvoiceStatus(str, Enum):
    PENDING = "PENDING"
    PAID = "PAID"
    OVERDUE = "OVERDUE"
    CANCELLED = "CANCELLED"

class Invoice(BaseModel):
    vendor: str = Field(description="Nom du fournisseur tel qu'il apparaît sur la facture")
    amount: float = Field(gt=0, description="Montant total TTC en euros")
    status: InvoiceStatus = Field(description="Statut de paiement de la facture")
    due_date: date = Field(description="Date d'\u00e9ch\u00e9ance au format YYYY-MM-DD")
    line_items: list[LineItem] = Field(min_length=1)

#Règle 4 : optional vs required — être explicite

Pour les champs qui peuvent légitimement être absents d'un document (numéro SIRET sur une facture étrangère, par exemple), utilisez Optional[str] = None en Pydantic ou z.string().nullable() en Zod. Forcer un champ required sur une valeur absente génère des hallucinations — le modèle inventera une valeur plutôt que de violer le schéma.

Pour les équipes qui travaillent sur du code de qualité production avec ces patterns, notre article sur la qualité du code IA détaille les standards de revüe et de test applicables.


#Gestion des erreurs et retry : pattern production-ready

Le retry naif (réessayer à l'identique) est inefficace — si le modèle a produit une sortie invalide, il va probablement la reproduire. Le pattern self-repair est plus efficace : on injecte le message d'erreur de validation dans le prompt de retry.

// Pattern retry avec self-repair en TypeScript
async function extractWithRetry<T>(
  schema: z.ZodType<T>,
  prompt: string,
  maxRetries: number = 3
): Promise<T> {
  let lastError: string | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const messages = [
      { role: "system", content: "Tu extrais des données structurées depuis des documents." },
      { role: "user", content: prompt },
    ];

    if (lastError) {
      messages.push({
        role: "user",
        content: `Ta réponse précédente était invalide. Erreurs de validation : ${lastError}. Corrige et renvoie uniquement le JSON valide.`
      });
    }

    const response = await openai.chat.completions.create({
      model: "gpt-4o",
      response_format: { type: "json_object" },
      messages,
    });

    const raw = JSON.parse(response.choices[0].message.content || "{}");
    const result = schema.safeParse(raw);

    if (result.success) {
      return result.data;
    }

    lastError = JSON.stringify(result.error.format());
    console.warn(`Attempt ${attempt + 1}/${maxRetries} failed:`, lastError);
  }

  throw new Error(`Extraction failed after ${maxRetries} attempts. Last error: ${lastError}`);
}

Stratégies complémentaires :

  • Exponentiel backoff : sur les erreurs rate-limit, attendre 2^n secondes entre les tentatives
  • Fallback model : si gpt-4o échoue 3 fois, basculer sur o1-mini avec un prompt plus explicite
  • Logging structuré : chaque tentative échoue est loggée avec le raw output LLM pour analyse post-mortem
  • Circuit breaker : après X échecs consécutifs, suspendre l'extraction et alerter l'équipe plutôt que de boucler indéfiniment

Ces patterns s'inscrivent dans une approche plus large de l'ingénierie de prompts robuste — notre article prompt engineering avancé pour l'entreprise développe les techniques de système prompt et de few-shot examples.


#Cas d'usage : extraction de données factures, contrats et emails B2B

Ces trois cas d'usage représentent la majorité des projets structured outputs que Nehos a déployés en production.

#1. Extraction de factures fournisseurs

Contexte : une direction des achats reçoit 800 à 3 000 factures PDF par mois, en format hétérogène (prestataires français, européens, formats papier scannés). L'objectif : extraire les données vers l'ERP sans ressaisie manuelle.

Stack : OCR (AWS Textract ou Azure Form Recognizer) + LLM avec structured output pour la normalisation + Pydantic pour la validation + rejet vers file de relecture humaine si le score de confiance est bas.

Le schéma Pydantic type pour ce cas couvre : vendor_name, vendor_siret, invoice_number, invoice_date, due_date, total_ht, total_tva, total_ttc, currency, line_items[], payment_method. Les champs total_ht + total_tva sont validés par une contrainte métier : total_ht + total_tva == total_ttc (° epsilon pour les arrondis).

Pour les outils OCR disponibles en 2026, notre comparatif OCR IA pour factures et contrats présente les options et leur précision respective.

#2. Analyse de contrats B2B

Contexte : un service juridique doit revûer 150 contrats fournisseurs par trimestre. Chaque contrat fait 10 à 80 pages. L'objectif : générer un rapport de risques structuré pour chaque contrat en moins de 5 minutes.

Approche chunking + structured output : le contrat est découpé en sections logiques (clauses, annexes), chaque section est analysée indépendamment avec un schéma spécifique, puis les résultats sont agrégés dans un rapport final.

Le schéma d'extraction pour une clause :

class ClauseAnalysis(BaseModel):
    clause_type: Literal["termination", "penalty", "renewal", "liability", "IP", "other"]
    risk_level: Literal["LOW", "MEDIUM", "HIGH"]
    clause_text: str = Field(description="Texte exact de la clause")
    risk_explanation: str = Field(max_length=300)
    recommended_action: Optional[str] = None

Ce pipeline s'appuie sur les architectures RAG détaillées dans notre article RAG : architecture et outils pour l'entreprise.

#3. Classification et routing d'emails B2B

Contexte : une boîte email support@entreprise.com reçoit 500 messages par jour : demandes commerciales, factures, réclamations, candidatures, spams. L'objectif : classer, enrichir et router automatiquement vers le bon système (CRM, ERP, ATS, ignorer).

Schéma de classification :

const EmailClassificationSchema = z.object({
  category: z.enum(["COMMERCIAL_INQUIRY", "INVOICE", "COMPLAINT", "APPLICATION", "SPAM", "OTHER"]),
  urgency: z.enum(["LOW", "NORMAL", "HIGH", "CRITICAL"]),
  language: z.string().length(2), // ISO 639-1
  sentiment: z.enum(["POSITIVE", "NEUTRAL", "NEGATIVE"]),
  extractedEntities: z.object({
    companyName: z.string().nullable(),
    contactName: z.string().nullable(),
    contactEmail: z.string().email().nullable(),
    estimatedBudget: z.number().nullable(),
    productMentioned: z.array(z.string()),
  }),
  suggestedAction: z.string().max(200),
  confidenceScore: z.number().min(0).max(1),
});

Le confidenceScore permet de router vers la file de relecture humaine les emails dont la classification est incertaine (score < 0.75), plutôt que de risquer un routing erroné.

Ces usages s'inscrivent dans la dynamique plus large des LLM open source comme Llama ou DeepSeek qui permettent aussi des structured outputs on-premise pour les données sensibles.


#Performance : impact sur la latence et le coût des structured outputs

Une question revênt systématiquement lors des projets : les structured outputs augmentent-ils le coût et la latence de manière significative ?

#Impact sur la latence

D'après nos mesures en production sur plusieurs projets Nehos :

MécanismeLatence ajoutée vs prompt libreFiabilité
JSON mode basique+5 à 8 %Moyenne (JSON valide, schéma non garanti)
Structured Outputs OpenAI (strict)+10 à 15 %Très haute
Function calling / tool use+12 à 20 %Très haute
Instructor avec 1 retry+25 à 40 % (cas d'échec)Quasi-totale

La latence supplémentaire vient principalement du traitement du schéma et de la contrainte de décodage. Sur un appel de 500ms en mode normal, on monte à 550-600ms en structured output strict — acceptable pour tous les use cases non temps-réel.

#Impact sur les tokens et le coût

Le schéma JSON injecté dans le prompt consôme des tokens d'entrée. Un schéma Zod/Pydantic de taille moyenne représente 200 à 600 tokens supplémentaires. Sur le modèle gpt-4o à 5$/MTok en input, cela représente 0.001 à 0.003 ¢ par appel — négligeable.

Le vrai levier de coût est à l'opposé : les structured outputs réduisent les erreurs de parsing, donc réduisent les retries non planifiés. Un système sans structured output qui a 3 % de taux d'échec de parsing sur 100 000 appels jour génère 3 000 appels supplémentaires — bien plus coûteux que le surcoût du schéma.

#Optimisation pratique

  • Cacher le schéma : si vous utilisez le même schéma dans tous vos appels, la fonctionnalité de prompt caching (OpenAI, Anthropic) permet de ne pas retokenizer le schéma à chaque fois
  • Simplifier les schémas de second niveau : pour l'agrégation de résultats multi-chunks, un schéma simplifié suffit souvent
  • Monitorer le taux de retry : un taux > 2 % sur un schéma spécifique signale généralement un problème de description de champ ou une contrainte trop stricte

Pour mesurer précisément l'impact sur la productivité de vos équipes, notre article sur la productivité développeur IA propose des métriques concrètes. Et si vous souhaitez voir ces patterns appliqués dans nos réalisations, la page services agents IA présente des cas concrets.

Questions & Réponses

Questions fréquentes

Le JSON mode (`response_format: { type: "json_object" }`) garantit uniquement que la sortie est un JSON syntaxiquement valide — sans contrainte sur les clés, les types ou la structure. Les Structured Outputs (`response_format: { type: "json_schema" }` avec `strict: true`) garantissent que la sortie respecte exactement le JSON Schema fourni, y compris les types de champ, les champs obligatoires et l'absence de champs supplémentaires. Pour de la production, seuls les Structured Outputs offrent une garantie véritable.
Oui. Instructor supporte OpenAI, Anthropic Claude, Mistral, Cohere, Google Gemini, Groq et les modèles locaux via Ollama. L'API est unifiée : vous changez le client sous-jacent sans modifier le reste du code. Pour Anthropic, Instructor utilise automatiquement le mécanisme tool use ; pour Mistral, il utilise le JSON mode couplé à une validation Pydantic ou Zod côté client. C'est l'un des avantages majeurs de la librairie : abstraire les différences de implémentation entre fournisseurs.
Oui, mais avec des variations de qualité. Les modèles Llama 3.1 (8B, 70B, 405B), Mistral 7B Instruct, Qwen 2.5 et DeepSeek V3 supportent le function calling via leur fine-tuning dédié. La fiabilité est inférieure à celle de GPT-4o ou Claude Opus, surtout sur des schémas complexes ou des documents longs. L'approche recommandée pour le self-hosting : coupler le function calling avec une validation Pydantic/Zod stricte et un taux de retry plus élevé (max_retries: 5 au lieu de 3).
Déclarez les champs potentiellement absents comme `Optional[str] = None` en Pydantic ou `z.string().nullable()` en Zod. Ajoutez dans la description du champ : `null si non mentionné dans le document`. Sans cette instruction, le modèle peut halluciner une valeur plutôt que de retourner null, parce que le schéma require le champ. Cette combinaison — nullable dans le schéma ET instruction dans la description — réduit le taux d'hallucination sur les champs optionnels à moins de 1 % sur GPT-4o.
Sur un pipeline de 100 000 appels par jour avec GPT-4o, le surcoût lié au schéma (300 tokens input supplémentaires en moyenne) représente environ à partir de 592 €/mois. L'économie réalisée sur les retries évités (3 % d'échecs sans structured outputs) est bien supérieure. De plus, les structured outputs éliminent le coût de développement et de maintenance des parsers manuels. Le TCO sur 12 mois est systématiquement positif par rapport à une approche sans structured outputs.
Directement, non — la fenêtre contextuelle limite la quantité de texte traité en une fois. L'approche production pour les longs documents est le chunking sémantique : découper le document en sections cohérentes, appliquer un structured output spécifique à chaque section, puis agréger les résultats via un LLM de synthèse avec un schéma réduit. Pour les contrats longs, on combine généralement ce pattern avec un RAG qui extrait d'abord les sections pertinentes avant l'extraction structurée.
Sur GPT-4o et Claude Opus en structured output strict, les few-shot examples ne sont généralement pas nécessaires pour des schémas standards. Ils deviennent utiles dans deux cas : quand le domaine est très spécifique (terminologie métiers peu représentée dans les données d'entraînement) et quand les valeurs attendues suivent des conventions non standard (codes produits internes, formats de référence propriétaires). Un ou deux exemples input/output dans le system prompt suffisent — au-delà, le gain est marginal et le coût token augmente.
Réserver un audit