Obtenir du JSON fiable d'un LLM : structured outputs et validation
- 01Active le mode JSON de l'API (response_format) plutôt que d'espérer un JSON propre dans du texte
- 02Valide toujours la sortie contre un schéma : un JSON syntaxiquement valide peut avoir de mauvais champs
- 03Prévois un retry : en cas de sortie invalide, renvoie l'erreur au modèle pour qu'il se corrige
Tu extrais des données d'un texte avec un LLM et tu galères à parser sa réponse ? Un « Voici le JSON : » de trop, un bloc de code markdown autour, des virgules superflues, du français qui déborde… À la fin de ce tuto, tu auras une extraction JSON fiable : mode structuré côté API, validation par schéma, et retry automatique. Exemples en TypeScript, transposables ailleurs.
Étape 1 — Activer le mode JSON de l'API
La plupart des fournisseurs proposent un response_format qui garantit un objet JSON syntaxiquement valide. Exemple avec l'API Mistral :
const res = await fetch("https://api.mistral.ai/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.MISTRAL_API_KEY}`,
},
body: JSON.stringify({
model: "mistral-small-latest",
messages: [
{ role: "system", content: "Tu extrais des données. Réponds en JSON strict." },
{ role: "user", content: `Extrais nom, email et budget de : "${texte}"` },
],
response_format: { type: "json_object" },
}),
});
const data = await res.json();
const raw = data.choices[0].message.content; // déjà un objet JSON valide en string
Avec ce mode, le JSON n'est plus entouré d'un bloc de code parasite. Chez les fournisseurs qui l'entourent quand même, garde une sécurité :
function stripFence(text: string): string {
const t = text.trim();
return t.startsWith("```")
? t.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim()
: t;
}
Étape 2 — Valider contre un schéma
Un JSON valide n'est pas un JSON correct : le modèle peut renvoyer budget: "beaucoup" au lieu d'un nombre. Valide avec un schéma, ici Zod :
import { z } from "zod";
const Lead = z.object({
nom: z.string().min(1),
email: z.string().email(),
budget: z.number().nullable(),
});
const parsed = Lead.safeParse(JSON.parse(stripFence(raw)));
if (parsed.success) {
console.log(parsed.data); // typé et garanti conforme
}
safeParse ne jette pas : il renvoie success: false et les erreurs, ce qui te laisse décider quoi faire.
Étape 3 — Retry avec le message d'erreur
Quand la validation échoue, la meilleure correction, c'est de renvoyer l'erreur au modèle :
async function extractLead(texte: string, tries = 2): Promise<z.infer<typeof Lead> | null> {
let feedback = "";
for (let i = 0; i < tries; i++) {
const raw = await callModel(texte, feedback); // reprend l'appel de l'étape 1
const parsed = Lead.safeParse(JSON.parse(stripFence(raw)));
if (parsed.success) return parsed.data;
feedback = `Ta réponse précédente était invalide : ${parsed.error.message}. Corrige et renvoie un JSON conforme.`;
}
return null;
}
Deux tentatives suffisent dans l'immense majorité des cas.
Adapter à ton cas
- Extraction en masse : mets
temperature: 0pour des sorties stables et reproductibles. - Schéma complexe : décris le format attendu dans le prompt système en plus du
response_format— le modèle suit mieux quand il « voit » la structure. - Autre fournisseur : OpenAI propose des structured outputs qui garantissent la conformité au schéma directement (
response_formatavec un JSON Schema). Claude n'a pas de mode JSON natif : demande le JSON dans le prompt et applique lestripFence+ validation ci-dessus.
En cas de souci
JSON.parseéchoue : c'est presque toujours un bloc markdown ou du texte avant/après. AppliquestripFence, et si besoin extrais du premier{au dernier}.- Champs corrects mais valeurs fantaisistes : baisse la température et précise dans le prompt « n'invente pas : mets
nullsi l'info est absente ». - Le retry boucle sans converger : ton schéma est peut-être trop strict pour ce que contient le texte. Assouplis (champs optionnels) plutôt que de forcer.
- Coût qui grimpe : un JSON long consomme des tokens. Ne demande que les champs utiles, et évite de renvoyer tout le texte source dans la réponse.
Articles liés
Construire un chatbot en streaming sur son site avec une API IA
Afficher la réponse d'un LLM token par token (effet « ça s'écrit en direct ») avec une route serveur qui relaie le flux et un client qui le lit.
Le function calling : laisser une IA déclencher des actions
Construis pas à pas un assistant qui appelle vraiment tes fonctions : on part d'un appel vide, on ajoute un outil, on exécute le code, et on boucle. Script complet et réutilisable à la fin.
Faire tourner un modèle IA en local avec Ollama (gratuit et privé)
De l'installation à l'appel API en Python, en passant par une interface de chat type ChatGPT en local : le guide complet pour faire tourner un LLM sur ta machine, avec dépannage et réglages de performance.