7. Sortie structurée avec Pydantic
Dans les chapitres précédents, nous avons travaillé avec les sorties des LLM sous forme de chaînes de texte brutes. Cela fonctionne bien pour les chatbots où les humains lisent les réponses, mais lors de la construction d'agents IA où les programmes doivent analyser et interpréter les sorties des LLM, nous avons besoin de données structurées et prévisibles. Dans ce chapitre, vous apprendrez à utiliser les schémas Pydantic pour faire en sorte que le LLM retourne des objets Python structurés.
7.1) Pourquoi une sortie structurée ?
Le problème avec la sortie LLM en texte libre
Commençons par comprendre pourquoi les réponses en texte brut créent des problèmes dans les applications réelles. Considérez ce scénario courant :
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
# Demander au LLM des informations sur un produit
message = HumanMessage(content="""
Extrayez les informations du produit à partir de ce texte :
"L'UltraWidget Pro coûte 299,99 $ et est actuellement en stock."
""")
response = llm.invoke([message])
print(response.content)Sortie :
Nom du produit : UltraWidget Pro
Prix : 299,99 $
Disponibilité : En stockLa sortie semble correcte. Mais supposons maintenant que vous ayez besoin d'utiliser ces données dans votre application Python. Comment extraire le prix sous forme de nombre ? Comment vérifier la disponibilité de manière programmatique ? Vous pourriez essayer une analyse de chaîne comme ceci :
# Approche d'analyse fragile
text = response.content
price_line = [line for line in text.split('\n') if 'Prix :' in line][0]
price_str = price_line.split('$')[1]
price = float(price_str) # Fragile - et si le format change ?Cette analyse semble fonctionner. Mais en réalité, elle ne fonctionne pas. Voici pourquoi :
Pourquoi cette approche échoue :
- Le LLM pourrait formater la réponse différemment la prochaine fois ("Prix : 299,99 USD" ou "Prix de détail : 299,99 $")
Voici des exemples de différentes sorties qui peuvent se produire pour le même prompt :
# Exemple 1
"Le produit est UltraWidget Pro, au prix de 299,99 $, et il est disponible."
# Exemple 2
"Produit : UltraWidget Pro
Coût : 299,99 dollars
Statut : Disponible"
# Exemple 3
"UltraWidget Pro - 299,99 $ (en stock)"
# Exemple 4
"J'ai trouvé l'UltraWidget Pro. Il coûte 299,99 $ et est actuellement disponible à l'achat."Lorsque la réponse du LLM change, vous avez besoin d'une logique d'analyse complètement différente. Cela rend difficile la construction d'applications fiables.
- Les réponses des LLM sont imprévisibles : Le même prompt peut produire des formats différents à chaque fois
- L'analyse de chaînes est plus difficile qu'il n'y paraît : Vous devez gérer
$, les espaces, les sauts de ligne, les virgules, et plus encore - Aucune sécurité de type : Vous ne pouvez pas être sûr que
priceest un float, une chaîne ou None - La gestion des erreurs est difficile : Si le LLM dit "Prix non disponible", votre appel
float()plante - Impossible à maintenir : Changez légèrement le prompt et vous réécrivez tout le code d'analyse
L'idée centrale : Python a besoin de contrats, pas de prose
Pensez à la communication avec un serveur API en Python. Lorsque vous appelez une API REST spécifique, vous vous attendez à ce qu'elle retourne une réponse JSON définie :
# Vous attendez cette structure
{
"product_name": "UltraWidget Pro",
"price": 299.99,
"in_stock": true
}Lors de la construction d'applications IA, vous avez besoin du même principe. La sortie du LLM doit être retournée sous forme de données avec une structure définie, et non sous forme de texte libre à chaque fois.
Prose vs Contrat :
- Prose : Texte naturel en forme libre. Bon pour que les humains le lisent, mais difficile à traiter pour les programmes.
- Contrat : Données avec structure et types définis. Une promesse que "ces champs existeront avec ces types."
La sortie structurée signifie définir un contrat : "LLM, j'ai besoin exactement de ces champs, avec exactement ces types, dans exactement ce format."
C'est là que Pydantic intervient. Pydantic est la bibliothèque de validation de données la plus populaire de Python, et LangChain l'utilise pour recevoir les sorties des LLM sous forme structurée.
Le changement de modèle mental :
- Avant : "LLM, parle-moi de ce produit" → Analyser du texte imprévisible
- Après : "LLM, réponds dans un format défini" → Recevoir un objet Python structuré
Ce passage de la prose aux contrats est fondamental pour construire des agents IA fiables. Lorsqu'un agent doit décider de sa prochaine action en fonction des réponses du LLM (par exemple, acheter si en stock, s'inscrire pour une notification sinon), il doit recevoir des réponses dans un format défini.
7.2) Votre première sortie structurée
En 7.1, nous avons appris pourquoi les LLM devraient répondre avec une structure définie au lieu de texte libre. Voyons maintenant comment implémenter cela réellement.
L'idée clé : Simplement demander au LLM "veuillez répondre dans ce format" ne suffit pas. Vous devez définir la structure de données exacte dans le code Python et faire en sorte que LangChain la transmette au LLM. Cette structure de données définie s'appelle un schéma.
Qu'est-ce qu'un schéma ?
Un schéma est un plan qui définit la structure des données. Il spécifie :
- Quels champs doivent être présents
- Quel type chaque champ devrait avoir (chaîne, nombre, booléen, etc.)
- Quelles contraintes s'appliquent (optionnel vs obligatoire, plages valides, etc.)
En Python, nous définissons les schémas en utilisant la classe BaseModel de Pydantic. Voici l'exemple le plus simple possible :
from pydantic import BaseModel
class ProductInfo(BaseModel):
product_name: str
price: float
in_stock: boolCe schéma dit : "Un objet ProductInfo doit avoir exactement trois champs : un product_name (chaîne), un price (float) et un in_stock (booléen)."
Le modèle en trois étapes : Définir, Lier, Invoquer
L'utilisation de la sortie structurée est simple. Rappelez-vous simplement trois étapes :
- Définir : Créer un schéma avec une classe Pydantic
- Lier : Connecter le schéma au LLM en utilisant
.with_structured_output() - Invoquer : Appeler
.invoke()pour obtenir un objet typé
Ceci est le modèle standard que vous utiliserez pour la plupart des tâches d'extraction structurée :
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
# Étape 1 : Définir le schéma
class ProductInfo(BaseModel):
product_name: str = Field(description="Le nom complet du produit")
price: float = Field(description="Prix en USD")
in_stock: bool = Field(description="Si le produit est disponible")
# Étape 2 : Lier le schéma au LLM
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
# Étape 3 : Invoquer et obtenir un objet typé
message = HumanMessage(content="""
Extrayez les informations du produit à partir de ce texte :
"L'UltraWidget Pro coûte 299,99 $ et est actuellement en stock."
""")
result = structured_llm.invoke([message])
# result est maintenant un objet ProductInfo, pas une chaîne
print(type(result)) # <class '__main__.ProductInfo'>
print(result.product_name) # UltraWidget Pro
print(result.price) # 299.99
print(result.in_stock) # TrueQue s'est-il passé ?
- Définition du schéma : Nous avons défini les champs et les types que nous voulons
- Liaison :
.with_structured_output(ProductInfo)configure le LLM pour utiliser la sortie structurée - Invocation & Réponse : Lorsque
.invoke()est appelé, LangChain transmet le schéma JSON au LLM, et le LLM répond avec du JSON correspondant à cette structure - Conversion automatique : LangChain convertit le JSON en un objet
ProductInfo- aucun code d'analyse nécessaire
Pas d'analyse. Pas de conversion de type. Pas d'erreurs.
Utilisez ce modèle en 3 étapes comme votre modèle. Suivez-le chaque fois que vous avez besoin d'une sortie structurée.
Descriptions de champs : La clé pour guider le LLM
Dans l'exemple de définition de schéma ci-dessus, nous avons utilisé Field(description="..."). Cette description n'est pas seulement de la documentation. Ce sont des instructions que le LLM lit et suit.
Dans l'utilisation typique de Pydantic, les descriptions de Field sont optionnelles :
# Pydantic régulier - la description est une documentation pour les humains
class User(BaseModel):
name: str = Field(description="Nom de l'utilisateur") # Fonctionne bien sans elleMais lorsque vous travaillez avec des LLM, elles sont essentielles :
# Avec les LLM - la description détermine le comportement du LLM
class CustomerFeedback(BaseModel):
sentiment: str = Field(
description="Sentiment global : 'positive', 'negative' ou 'neutral'"
)Le LLM lit cette description et l'utilise pour décider comment répondre.
Voyons cela en action :
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
class CustomerFeedback(BaseModel):
sentiment: str = Field(
description="Sentiment global : 'positive', 'negative' ou 'neutral'"
)
main_issue: str = Field(
description="La plainte ou préoccupation principale, le cas échéant. Utilisez 'none' si aucun problème n'est mentionné."
)
urgency: str = Field(
description="Quelle est l'urgence du problème : 'low', 'medium' ou 'high'"
)
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(CustomerFeedback)
message = HumanMessage(content="""
Analysez ce retour client :
"Le produit fonctionne bien, mais la livraison a pris 3 semaines. J'ai déjà manqué la date limite de mon projet. Veuillez répondre immédiatement."
""")
result = structured_llm.invoke([message])
print(result.sentiment) # negative
print(result.main_issue) # Livraison lente
print(result.urgency) # highComment les descriptions façonnent les décisions du LLM :
- Description de
sentiment→ Le LLM apprend que les valeurs valides sont 'positive', 'negative', 'neutral' → Le problème de livraison a causé une date limite manquée, donc choisit 'negative' - Description de
main_issue→ Le LLM est instruit de "trouver la plainte principale" → Identifie "livraison lente" comme le problème - Description de
urgency→ Le LLM apprend que l'urgence doit être 'low', 'medium' ou 'high' → Voit "Veuillez répondre immédiatement" et choisit 'high'
Que se passe-t-il sans descriptions ?
sentiment: str # Pas de descriptionLe LLM pourrait retourner "negative", "bad", "unsatisfied", "2/5", "disappointed" dans des formats imprévisibles, rendant difficile la gestion des valeurs par votre code.
Point clé : Les descriptions de champs font partie de votre code qui contrôle le comportement du LLM. Écrivez-les clairement et spécifiquement.
Champs catégoriels : Spécifier les valeurs autorisées
Dans l'exemple ci-dessus, le champ sentiment ne peut avoir que trois valeurs : 'positive', 'negative' ou 'neutral'. Les champs qui doivent être l'une d'un ensemble spécifique de valeurs sont appelés champs catégoriels.
Pour les champs catégoriels, listez toutes les valeurs possibles dans la description :
sentiment: str = Field(
description="Sentiment : exactement 'positive', 'negative' ou 'neutral' (minuscules)"
)En spécifiant "exactement" et "(minuscules)", nous soulignons que le LLM devrait répondre avec précisément l'une de ces trois valeurs.
Cependant, il n'y a aucune garantie que le LLM répondra toujours avec l'une des valeurs spécifiées. C'est pourquoi vous devez écrire du code défensif.
Cas où le LLM retourne des valeurs inattendues :
result.sentiment = "Positive" # Majuscule
result.sentiment = "NEGATIVE" # Tout en majuscules
result.sentiment = "good" # Mot différent entièrementÉcrire du code défensif :
allowed = {"positive", "negative", "neutral"}
# Convertir en minuscules et vérifier
sentiment = result.sentiment.lower()
if sentiment not in allowed:
sentiment = "neutral" # Utiliser la valeur par défaut pour les valeurs inattendues
# Maintenant sentiment est garanti d'être l'une des valeurs autoriséesPoint clé :
- Spécifier les valeurs autorisées dans la description → Le LLM est plus susceptible de répondre correctement
- Valider dans le code → Gérer les valeurs inattendues en toute sécurité
Note : Le chapitre 18 montre des modèles plus forts utilisant les enums Python pour l'application.
Comparaison : Analyse manuelle vs Sortie structurée
Comparons la même tâche avec et sans sortie structurée pour voir la différence :
Analyse manuelle :
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini")
message = HumanMessage(content="""
Extrayez le nom du produit, le prix et la disponibilité à partir de :
"L'UltraWidget Pro coûte 299,99 $ et est actuellement en stock."
Format : nom | prix | disponibilité
""")
response = llm.invoke([message])
text = response.content
# Analyse manuelle
parts = text.split('|')
product_name = parts[0].strip()
price_str = parts[1].strip().replace('$', '')
price = float(price_str)
availability = parts[2].strip().lower()
in_stock = 'en stock' in availability or 'disponible' in availability
print(f"Nom : {product_name}")
print(f"Prix : {price} $")
print(f"En stock : {in_stock}")Sortie structurée :
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
class ProductInfo(BaseModel):
product_name: str = Field(description="Le nom complet du produit")
price: float = Field(description="Prix en USD")
in_stock: bool = Field(description="Si le produit est disponible")
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
message = HumanMessage(content="""
Extrayez les informations du produit à partir de :
"L'UltraWidget Pro coûte 299,99 $ et est actuellement en stock."
""")
result = structured_llm.invoke([message])
print(f"Nom : {result.product_name}")
print(f"Prix : {result.price} $")
print(f"En stock : {result.in_stock}")Différences clés :
- Pas de logique d'analyse : La version structurée n'a aucun code d'analyse
- Sécurité de type :
result.priceest garanti d'être un float - Code plus simple : Pas de regex, pas de division de chaîne, pas de conversion de type manuelle
- Validation : Pydantic garantit que tous les champs requis sont présents
- Maintenabilité : Changer le schéma est plus facile que de mettre à jour la logique d'analyse
7.3) Considérations de conception de schéma
Maintenant que vous savez comment utiliser la sortie structurée, apprenons à concevoir de bons schémas. Cette section couvre les principes de conception pratiques pour distinguer les champs obligatoires des champs optionnels.
Champs obligatoires
Par défaut, tous les champs d'un modèle Pydantic sont obligatoires. Cela signifie que le LLM doit extraire ou déduire une valeur pour chaque champ obligatoire à partir du prompt de l'utilisateur et la fournir dans la réponse.
from pydantic import BaseModel
class ProductInfo(BaseModel):
product_name: str
price: float
in_stock: boolLorsque vous utilisez ce schéma, le LLM tentera de trouver des valeurs pour les trois champs (product_name, price, in_stock) dans le texte d'entrée.
Mais que se passe-t-il lorsque le prompt manque d'informations pour un champ obligatoire ?
Nous pourrions nous attendre au comportement suivant :
- Le LLM ne peut pas trouver l'information dans le prompt
- Le LLM omet ce champ de sa réponse
- LangChain ne peut pas créer une instance
ProductInfovalide ValidationErrorest levée
Cependant, cela ne se produit pas toujours.
La raison est que différents LLM peuvent gérer les informations manquantes différemment.
Certains LLM (tels que les modèles OpenAI) ont tendance à générer des valeurs même lorsque les informations requises ne sont pas présentes dans le prompt. Dans ce cas, ValidationError ne se produit pas, mais cela peut causer de plus gros problèmes car votre application Python peut traiter des informations fabriquées comme si elles étaient réelles.
Nous aborderons comment résoudre ce problème dans la Section 7.4 : Quand les choses tournent mal.
Pour l'instant, sachez simplement que tous les LLM ne gèrent pas les informations manquantes de la même manière.
Champs optionnels
Vous pouvez avoir besoin de champs qui peuvent légitimement être présents ou absents, même dans des cas normaux. Par exemple, une note de livraison (delivery_note) peut ou non être fournie par le client, même pour une commande valide.
Quand utiliser Optional :
- Les données elles-mêmes peuvent ne pas exister (par exemple, lorsque les avis anonymes sont autorisés, les avis anonymes n'ont pas de nom de réviseur)
- Vous voulez que le LLM indique explicitement les informations manquantes plutôt que de fabriquer une valeur
Pour rendre un champ optionnel, utilisez le type Optional de Python du module typing :
from typing import Optional
class ProductReview(BaseModel):
rating: int
review_text: str
reviewer_name: Optional[str] = None # Les avis anonymes n'ont pas de nom de réviseurNote : Les utilisateurs de Python 3.10+ peuvent utiliser
str | Noneau lieu deOptional[str].
Lorsqu'un champ est Optional :
- Le LLM peut l'omettre de la réponse si l'information n'est pas trouvée dans le prompt
- Les champs omis sont définis sur la valeur par défaut (
None)
Voici un exemple complet :
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, Field
from typing import Optional
class ProductInfo(BaseModel):
product_name: str
price: float
in_stock: bool
discount_percentage: Optional[float] = None
warranty_years: Optional[int] = None
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
message = HumanMessage(content="""
Extrayez les informations du produit : "L'UltraWidget Pro coûte 299,99 $ et est en stock."
""")
result = structured_llm.invoke([message])
print(result.product_name) # UltraWidget Pro
print(result.price) # 299.99
print(result.in_stock) # True
print(result.discount_percentage) # None (non mentionné)
print(result.warranty_years) # None (non mentionné)Liste de vérification de conception de schéma
Avant de finaliser votre schéma, posez-vous la question :
Sélection de champs :
- Les champs obligatoires sont-ils vraiment essentiels ? (Que se passe-t-il si ce champ manque dans le prompt ?)
- Les champs optionnels peuvent-ils légitimement être absents même dans des cas normaux ?
Spécification de champs :
- Chaque champ a-t-il une description claire ?
- Les champs catégoriels sont-ils explicitement contraints ? (par exemple, "doit être exactement 'A', 'B' ou 'C'")
7.4) Quand les choses tournent mal
Deux problèmes peuvent survenir lors de l'utilisation de la sortie structurée :
- Le LLM omet les valeurs de champs obligatoires → ValidationError se produit
- Le LLM fabrique des informations manquantes → Pas de ValidationError, mais votre code traite des données incorrectes
Cette section couvre comment gérer chacun.
Comprendre les erreurs de validation
Lorsque le prompt de l'utilisateur manque d'informations pour les champs obligatoires définis dans le schéma, le LLM ne peut pas fournir de valeurs pour ces champs. Le programme Python lève alors une ValidationError :
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, ValidationError
class ProductInfo(BaseModel):
product_name: str
price: float
in_stock: bool
llm = ChatAnthropic(model='claude-sonnet-4-5')
structured_llm = llm.with_structured_output(ProductInfo)
# Entrée manquant d'informations requises
message = HumanMessage(content="""
Extrayez les informations du produit à partir de : "Le widget est génial ! Hautement recommandé."
""")
try:
result = structured_llm.invoke([message])
print(result)
except ValidationError as e:
print("ValidationError s'est produite")Note : Lorsque le prompt manque d'informations pour les champs obligatoires, certains LLM peuvent fabriquer des valeurs et les fournir dans la réponse. Dans ce cas,
ValidationErrorne se produira pas, mais un problème plus grave survient. Nous couvrirons cela dans la section suivante.
Lorsque le prompt manque d'informations pour les champs obligatoires et que ValidationError se produit, c'est en fait utile pour votre application Python. L'application peut détecter qu'un problème s'est produit et gérer l'erreur de manière contrôlée. Les stratégies de récupération d'erreur sont couvertes dans le Chapitre 14 (récupération d'erreur au niveau de l'agent) et le Chapitre 17 (logique de nouvelle tentative avec gestion d'état).
Le problème plus grave : Le LLM fabrique des informations manquantes
Comme nous l'avons discuté dans la Section 7.3, certains LLM présentent un comportement plus dangereux : ils fabriquent des valeurs et les fournissent dans les réponses lorsque des informations manquent dans le prompt.
Comment le problème se produit :
- Le prompt manque d'informations requises
- Le LLM génère quand même des valeurs plausibles
ValidationErrorne se produit PAS- Votre application Python traite les données fabriquées comme si elles étaient réelles
Exemple :
class ProductInfo(BaseModel):
product_name: str
price: float
in_stock: bool
message = HumanMessage(content="""
Extrayez les informations du produit à partir de : "Le widget est génial !"
""")
# Lorsque certains LLM fabriquent des valeurs pour product_name, price et in_stock
result = structured_llm.invoke([message])
# Aucune erreur levée !
print(result.product_name) # "widget" (extrait du texte)
print(result.price) # 0.0 (fabriqué !)
print(result.in_stock) # False (fabriqué !)
# Problème : Vous ne pouvez pas dire quelles valeurs sont réelles vs fabriquéesC'est pire qu'une ValidationError parce que :
- Votre application Python continue de s'exécuter avec de mauvaises données
- Vous ne savez pas quels champs sont réels vs fabriqués
- La logique en aval peut prendre de mauvaises décisions basées sur de fausses données
Solution : Utiliser des champs optionnels avec validation
La solution consiste à définir tous les champs obligatoires comme Optional, puis à utiliser un validateur pour vérifier que tous les champs obligatoires ont des valeurs.
from typing import Optional
from pydantic import BaseModel, model_validator, ValidationError
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
class ProductInfo(BaseModel):
# Ceux-ci sont en fait obligatoires, mais déclarés comme Optional
# La vraie validation se produit dans le validateur ci-dessous
product_name: Optional[str] = None
price: Optional[float] = None
in_stock: Optional[bool] = None
@model_validator(mode='after')
def check_required_fields(self):
"""Valide que tous les champs essentiels sont présents"""
if self.product_name is None or self.price is None or self.in_stock is None:
raise ValueError("Tous les champs (product_name, price, in_stock) doivent être fournis")
return self
llm = ChatOpenAI(model="gpt-4o-mini")
structured_llm = llm.with_structured_output(ProductInfo)
# Tester avec des données incomplètes
message = HumanMessage(content="""
Extrayez les informations du produit à partir de : "Le widget est génial !"
""")
try:
result = structured_llm.invoke([message])
# Si nous arrivons ici, tous les champs sont garantis d'être présents
print(f"Produit : {result.product_name}")
print(f"Prix : {result.price} $")
except ValidationError as e:
# Les champs obligatoires manquent - l'extraction a échoué
print(f"Extraction incomplète : {e}")Ce qui se passe ici :
@model_validatorest le décorateur de Pydantic qui ajoute une logique de validation personnaliséemode='after'signifie que la validation s'exécute après que tous les champs ont été analysés- Si un champ est
None, nous levonsValueErrorpour signaler des données incomplètes - Pydantic enveloppe automatiquement cette
ValueErrordans uneValidationError
Pourquoi cela fonctionne :
Lorsque le prompt manque d'informations pour les champs :
- Le LLM ne fabrique pas de valeurs et omet ces champs de la réponse
- Dans ce cas, ces champs deviennent
None - Si ces champs sont en fait obligatoires, le validateur Pydantic lève
ValueError - Pydantic l'enveloppe comme
ValidationError
De cette façon, votre application Python reçoit une erreur explicite à gérer, plutôt que des données fabriquées.
Point clé : Lorsque le prompt manque d'informations pour les champs obligatoires, obtenir une ValidationError est parfaitement normal et attendu. Le vrai danger est les données fabriquées. Utilisez des champs Optional avec des validateurs pour empêcher le LLM de fabriquer des informations manquantes, tout en détectant explicitement quand les champs obligatoires manquent.
Résumé du chapitre :
Dans ce chapitre, vous avez appris à transformer la sortie du LLM en objets Python fiables :
- Pourquoi c'est important : L'analyse de texte libre est fragile ; la sortie basée sur un schéma fournit la sécurité de type
- Comment l'utiliser : Définir des schémas avec
BaseModelde Pydantic → Lier avec.with_structured_output() - Principes de conception : Choisir les champs obligatoires vs optionnels, guider le LLM avec des descriptions de champs
- Gérer les problèmes : ValidationError est normale ; le vrai danger est les données fabriquées