« Réponds en JSON » ne suffit pas. Tôt ou tard le modèle ajoute un ```json, une phrase d'intro, ou un champ manquant, et ton json_decode renvoie null en prod un vendredi soir. Voici le pattern qui tient.

Trois niveaux de fiabilité

MéthodeFiabilitéCoût
« Réponds en JSON » dans le prompt~85 %0
+ schéma explicite + exemple~97 %tokens en plus
+ mode structured output natif (API)~100 %selon provider
Plus on contraint, plus c'est fiable. En 2026 le structured output natif existe chez les 3 grands, mais le fallback prompt+validation reste utile.

Validation + retry ciblé

Même à 99 %, il faut un filet. Le pattern : valider, et si invalide, renvoyer l'erreur au modèle pour qu'il corrige (un seul retry, pas une boucle infinie).

function getJson(string $prompt, array $required, int $tries = 2): array {
    $msg = $prompt;
    for ($i = 0; $i < $tries; $i++) {
        $raw = callLlm($msg);
        $raw = trim(preg_replace('/^```json|```$/m', '', $raw));
        $data = json_decode($raw, true);
        $missing = array_diff($required, array_keys($data ?? []));
        if (is_array($data) && !$missing) return $data;
        $msg = $prompt . "\n\nTa réponse précédente était invalide ("
             . ($data === null ? 'JSON malformé' : 'champs manquants: '
             . implode(',', $missing)) . "). Renvoie UNIQUEMENT le JSON.";
    }
    throw new RuntimeException('JSON non valide après ' . $tries . ' essais');
}

Règles

  • Toujours stripper les fences ``` avant de décoder.
  • Valider la présence des champs, pas juste que c'est du JSON.
  • Un seul retry avec le message d'erreur précis : ça corrige 95 % des cas restants sans exploser le coût.
  • Logger les échecs (cf. Logger un LLM sans dépendance) pour ajuster le prompt.