Aller au contenu principal

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 firstName au lieu de first_name)
  • Modifier les types (retourner "42" string au lieu de 42 nombre)
# 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_reason pour 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