Extraire du JSON d’un LLM sans s’arracher les cheveux : la méthode du rempart de parsing
GenAI, LLM, Python, JSON, Parsing, Production, Robustesse
C’est le constat amer de tout développeur s’essayant à la mise en production de la GenAI : le prompt engineering le plus strict ne garantit jamais une reproductibilité à 100%. Vous demandez un JSON pur, mais le modèle (comme gemma2:2b) décide, par excès de politesse ou simple probabilité, d’ajouter un petit “Voici votre résultat :” avant l’accolade ouvrante.
Résultat ? Une exception irrécupérable dans votre parser JSON natif et un pipeline qui s’effondre en plein traitement batch. Voici comment construire une politique de parsing défensive.
1 Le mirage du JSON parfait
En mode zero-shot, nous forçons souvent les modèles à structurer leurs réponses. Cependant, la nature probabiliste des LLMs rend la sortie fragile. La vulnérabilité est souvent ignorée lors des tests unitaires, pour ne surgir qu’au milieu de la nuit lors d’un traitement massif de données.
Le problème ne vient pas seulement du “bruit” textuel : le texte avant ou après le JSON peut corrompre le flux. Mais il s’agit aussi des hallucinations de vocabulaire à l’intérieur même des champs.
2 La stratégie du rempart technique
Pour sécuriser l’intégration LLM-vers-API, nous implémentons une logique de filtrage en trois étapes :
- Isolation brutale : on ne fait pas confiance à la chaîne de caractères complète. On utilise une expression régulière (Regex) pour “découper” et extraire uniquement le bloc JSON.
- Parsing protégé : on tente le chargement du dictionnaire uniquement sur ce fragment.
- Normalisation métier : si le LLM a divagué (ex : il écrit “Information non disponible” au lieu du flag “NE” demandé), une couche de post-processing scanne et standardise les valeurs.
2.1 Focus sur l’implémentation
Le cœur du réacteur réside dans une fonction de parsing “asymétrique” qui agit comme un proxy entre l’IA et votre base de données.
import re
import json
def parse_json_response(response: str) -> dict[str, str] | None:
# 1. Isolation via re.DOTALL pour ignorer le bruit poli du LLM
match = re.search(r"\{.*\}", response, re.DOTALL)
if match:
try:
raw_json = match.group(0)
jresp = json.loads(raw_json)
# 2. Lissage défensif (tolérance à l'hallucination de vocabulaire)
for aspect, opinion in jresp.items():
if "non exprim" in str(opinion).lower():
jresp[aspect] = "NE"
return jresp
except json.JSONDecodeError:
# Fallback gracieux au lieu de propager l'erreur
return None
return None3 Stratégie d’adoption et limites
Pour intégrer cette approche durablement, voici quelques recommandations :
- Le proxy obligatoire : un LLM ne devrait jamais communiquer directement avec votre interface de données sans ce proxy de nettoyage.
- La gestion des dead letters : stockez les échecs pour les relancer avec une
temperatureréglée strictement à 0.0, ou pour une analyse humaine. - Attention à l’imbrication : la regex présentée ici est efficace pour des objets plats. Pour du JSON très profond, des outils comme Pydantic sont préférables.
4 Conclusion
En production, la fiabilité d’un pipeline GenAI ne se mesure pas à la qualité de son meilleur résultat, mais à sa capacité à gérer le pire. En encapsulant vos appels LLM derrière un rempart de parsing et de normalisation, vous faites bondir la robustesse de vos intégrations.
Cette stratégie de parsing défensif a été éprouvée lors de la phase de benchmark LLM du projet FeelingsAnalysis.
5 Références et lectures complémentaires
- Jason Liu (2023). Instructor: Structured Extraction using LLMs. La bibliothèque de référence qui a formalisé l’utilisation de Pydantic pour transformer l’incertitude des modèles de langage en structures de données déterministes.
- OpenAI Documentation (2024). Structured Outputs and JSON Mode. Guide essentiel pour comprendre comment les contraintes au niveau du moteur d’inférence (logit bias et décodage contraint) garantissent la validité syntaxique des sorties.