« 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éthode | Fiabilité | 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 |
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.