Limites du JSON Mode et gestion d'erreurs
Les limites à connaître
Le JSON Mode garantit un JSON syntaxiquement valide, mais pas un JSON sémantiquement correct. Cette distinction est fondamentale pour la production. Comprendre les limites vous évitera des bugs subtils et des heures de débogage.
Aucune garantie de schéma
Le JSON Mode ne sait rien de votre schéma attendu. Le modèle peut :
- Omettre des champs que vous attendez
- Ajouter des champs que vous n’avez pas demandés
- Changer les noms de clés (par exemple
firstNameau lieu defirst_name) - Modifier les types (retourner
"42"string au lieu de42nombre)
# Vous attendez ceci :
# {"name": "Alice", "age": 30, "city": "Paris"}
# Mais vous pourriez recevoir :
# {"nom": "Alice", "âge": "trente ans", "ville": "Paris", "pays": "France"}
Le JSON est valide, mais la structure ne correspond pas à ce que votre code attend. C’est la différence fondamentale avec les Custom Structured Outputs.
Validation côté client obligatoire
Puisque le schéma n’est pas garanti, vous devez valider la sortie côté client. Voici un pattern robuste :
import json
from typing import Optional
def parse_and_validate(raw_content: str, required_keys: list[str]) -> dict:
"""Parse le JSON et vérifie les clés requises."""
try:
data = json.loads(raw_content)
except json.JSONDecodeError as e:
raise ValueError(f"JSON invalide : {e}")
if not isinstance(data, dict):
raise ValueError(f"Attendu un objet JSON, reçu {type(data).__name__}")
missing = [k for k in required_keys if k not in data]
if missing:
raise ValueError(f"Clés manquantes : {missing}")
return data
# Utilisation
try:
result = parse_and_validate(
response.choices[0].message.content,
required_keys=["name", "category", "priority"]
)
process_ticket(result)
except ValueError as e:
log_error(f"Sortie invalide : {e}")
fallback_processing(raw_content)
L’équivalent TypeScript avec une validation plus poussée :
interface TicketClassification {
category: string;
priority: string;
sentiment: string;
summary: string;
}
function validateTicket(raw: string): TicketClassification {
const data = JSON.parse(raw);
const requiredKeys = ["category", "priority", "sentiment", "summary"];
const missing = requiredKeys.filter((k) => !(k in data));
if (missing.length > 0) {
throw new Error(`Clés manquantes : ${missing.join(", ")}`);
}
const validCategories = ["billing", "account", "technical", "feature_request", "other"];
if (!validCategories.includes(data.category)) {
throw new Error(`Catégorie invalide : ${data.category}`);
}
return data as TicketClassification;
}
Gestion des erreurs courantes
JSON tronqué
Si la réponse dépasse max_tokens, le JSON sera tronqué et donc invalide :
response = client.chat.complete(
model="mistral-small-latest",
messages=messages,
response_format={"type": "json_object"},
max_tokens=50 # Trop petit pour un JSON complet
)
# response.choices[0].finish_reason sera "length" au lieu de "stop"
if response.choices[0].finish_reason == "length":
print("Attention : réponse tronquée, augmentez max_tokens")
Solution : vérifiez toujours finish_reason. S’il vaut "length", la réponse est incomplète.
Types inattendus
Le modèle peut retourner des types différents de ceux attendus :
def safe_get_int(data: dict, key: str, default: int = 0) -> int:
"""Extrait un entier, avec conversion si nécessaire."""
value = data.get(key)
if value is None:
return default
if isinstance(value, int):
return value
if isinstance(value, float):
return int(value)
if isinstance(value, str):
try:
return int(value)
except ValueError:
return default
return default
# Utilisation
age = safe_get_int(data, "age", default=0)
Clés renommées
Le modèle peut utiliser des synonymes ou des langues différentes pour les clés :
def normalize_keys(data: dict, key_mapping: dict) -> dict:
"""Normalise les clés en utilisant un mapping de synonymes."""
normalized = {}
for standard_key, aliases in key_mapping.items():
for alias in aliases:
if alias in data:
normalized[standard_key] = data[alias]
break
return normalized
# Mapping de synonymes
mapping = {
"name": ["name", "nom", "full_name", "fullName"],
"email": ["email", "mail", "e-mail", "emailAddress"],
"phone": ["phone", "telephone", "tel", "phoneNumber"],
}
normalized = normalize_keys(raw_data, mapping)
Pattern de retry
Quand la validation échoue, un retry avec un prompt corrigé peut résoudre le problème :
import json
def call_with_retry(client, messages, max_retries=3):
"""Appel avec retry et feedback d'erreur."""
for attempt in range(max_retries):
response = client.chat.complete(
model="mistral-large-latest",
messages=messages,
response_format={"type": "json_object"},
temperature=0
)
raw = response.choices[0].message.content
try:
data = json.loads(raw)
validate_schema(data) # Votre fonction de validation
return data
except (json.JSONDecodeError, ValueError) as e:
if attempt < max_retries - 1:
# Ajouter le feedback d'erreur au contexte
messages.append({"role": "assistant", "content": raw})
messages.append({
"role": "user",
"content": f"Ta réponse est invalide : {e}. Corrige et retourne un JSON valide."
})
else:
raise RuntimeError(f"Échec après {max_retries} tentatives : {e}")
Quand passer aux Custom Structured Outputs
Si vous vous retrouvez à écrire beaucoup de code de validation, c’est un signal fort qu’il est temps de migrer vers les Custom Structured Outputs. Le seuil typique :
- Plus de 5 champs à valider ? Passez au Custom.
- Des types complexes (listes imbriquées, enums) ? Passez au Custom.
- Un pipeline critique en production ? Passez au Custom.
Points clés à retenir
- Le JSON Mode garantit un JSON valide, pas un schéma valide
- Validez toujours la sortie côté client : clés présentes, types corrects, valeurs dans les bornes
- Vérifiez
finish_reasonpour détecter les réponses tronquées - Implémentez un mécanisme de retry avec feedback d’erreur
- Si la validation devient complexe, migrez vers les Custom Structured Outputs