Cas d'usage avancés
Mis à jour le 29 juillet 2026
Au-delà des exemples simples
Vous maîtrisez Pydantic, Zod et client.chat.parse(). Les trois cas qui suivent sortent du schéma à quatre champs pour aller vers ce que l’on rencontre en entreprise : des documents comptables, des avis clients à analyser finement, des textes qui relèvent de plusieurs catégories à la fois. Chacun illustre une technique de modélisation différente.
Extraire une facture
Le traitement documentaire est le terrain naturel des schémas imbriqués. Une facture n’est pas un objet plat : elle contient un en-tête, un pied de totaux, et un nombre variable de lignes qui ont elles-mêmes leur propre structure. On modélise donc LineItem séparément, puis on le référence dans une liste. La devise passe par un enum, ce qui interdit au modèle de renvoyer "euros" ou "€" selon la formulation du document.
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class Currency(str, Enum):
eur = "EUR"
usd = "USD"
gbp = "GBP"
class LineItem(BaseModel):
description: str = Field(description="Description de la ligne")
quantity: float = Field(description="Quantité")
unit_price: float = Field(description="Prix unitaire HT")
vat_rate: float = Field(description="Taux de TVA en pourcentage (ex: 20.0)")
total_ht: float = Field(description="Total HT de la ligne")
class InvoiceData(BaseModel):
invoice_number: str = Field(description="Numéro de facture")
date: str = Field(description="Date de facturation au format YYYY-MM-DD")
due_date: Optional[str] = Field(default=None, description="Date d'échéance")
vendor_name: str = Field(description="Nom du fournisseur")
vendor_address: Optional[str] = Field(default=None)
client_name: str = Field(description="Nom du client")
line_items: list[LineItem] = Field(description="Lignes de la facture")
subtotal_ht: float = Field(description="Sous-total HT")
total_vat: float = Field(description="Montant total de TVA")
total_ttc: float = Field(description="Total TTC")
currency: Currency = Field(description="Devise")
payment_method: Optional[str] = Field(default=None)
# Extraction
response = client.chat.parse(
model="mistral-large-latest",
messages=[
{
"role": "system",
"content": """Tu es un expert en comptabilité. Extrais les données de la facture.
Sois précis sur les montants. Calcule les totaux si nécessaire.
Les dates doivent être au format YYYY-MM-DD."""
},
{"role": "user", "content": texte_facture}
],
response_format=InvoiceData,
temperature=0
)
facture = response.choices[0].message.parsed
print(f"Facture {facture.invoice_number} — {facture.vendor_name}")
print(f"Total TTC : {facture.total_ttc} {facture.currency.value}")
for item in facture.line_items:
print(f" - {item.description}: {item.quantity} x {item.unit_price}€ = {item.total_ht}€ HT")
Le format de date imposé dans le system prompt n’est pas un détail : sans lui, un document français produira 12/03/2026 et un document américain 03/12/2026, deux chaînes indiscernables pour votre base de données. La version TypeScript reprend la même architecture, avec le format documenté via .describe().
import { z } from "zod";
const LineItemSchema = z.object({
description: z.string(),
quantity: z.number(),
unitPrice: z.number().describe("Prix unitaire HT"),
vatRate: z.number().describe("Taux de TVA en %"),
totalHt: z.number(),
});
const InvoiceSchema = z.object({
invoiceNumber: z.string(),
date: z.string().describe("Format YYYY-MM-DD"),
dueDate: z.string().nullable(),
vendorName: z.string(),
clientName: z.string(),
lineItems: z.array(LineItemSchema),
subtotalHt: z.number(),
totalVat: z.number(),
totalTtc: z.number(),
currency: z.enum(["EUR", "USD", "GBP"]),
});
const response = await client.chat.parse({
model: "mistral-large-latest",
messages: [
{ role: "system", content: "Extrais les données de la facture avec précision." },
{ role: "user", content: invoiceText },
],
responseFormat: InvoiceSchema,
temperature: 0,
});
const invoice = response.choices[0].message.parsed!;
console.log(`Facture ${invoice.invoiceNumber} — Total: ${invoice.totalTtc} ${invoice.currency}`);
Analyser un avis aspect par aspect
Un avis client réel est rarement uniformément positif ou négatif. L’avis d’hôtel ci-dessous loue la chambre et le petit-déjeuner, condamne le parking et le wifi, et nuance sur le personnel : un score global unique perdrait toute cette information. La solution consiste à modéliser un AspectSentiment réutilisable et à en demander une liste. Le champ evidence est le plus intéressant du schéma — en exigeant une citation du texte, vous obligez le modèle à ancrer son jugement et vous vous donnez les moyens de le vérifier.
from pydantic import BaseModel, Field
class AspectSentiment(BaseModel):
aspect: str = Field(description="L'aspect analysé (qualité, prix, service, livraison, etc.)")
sentiment: str = Field(description="positif, négatif ou neutre")
score: float = Field(description="Score entre -1.0 (très négatif) et 1.0 (très positif)")
evidence: str = Field(description="Citation ou extrait du texte qui justifie le sentiment")
class ReviewAnalysis(BaseModel):
overall_sentiment: str = Field(description="Sentiment global : positif, négatif, mitigé ou neutre")
overall_score: float = Field(description="Score global entre -1.0 et 1.0")
aspects: list[AspectSentiment] = Field(description="Analyse par aspect")
recommendation: bool = Field(description="L'auteur recommande-t-il le produit/service ?")
key_strengths: list[str] = Field(description="Points forts identifiés")
key_weaknesses: list[str] = Field(description="Points faibles identifiés")
avis = """
L'hôtel est magnifique, la chambre était spacieuse et propre. Le petit-déjeuner est excellent
avec un large choix. Par contre, le parking est hors de prix (35€/nuit) et le wifi était
instable tout au long du séjour. Le personnel à la réception était très accueillant,
mais le room service a mis plus d'une heure à livrer notre commande.
"""
response = client.chat.parse(
model="mistral-large-latest",
messages=[
{
"role": "system",
"content": "Analyse cet avis client en détaillant chaque aspect mentionné."
},
{"role": "user", "content": avis}
],
response_format=ReviewAnalysis,
temperature=0
)
review = response.choices[0].message.parsed
print(f"Sentiment global : {review.overall_sentiment} ({review.overall_score:+.1f})")
for aspect in review.aspects:
print(f" [{aspect.score:+.1f}] {aspect.aspect}: {aspect.sentiment}")
Classer un texte dans plusieurs catégories
Contrairement à la classification simple, où un seul label est attribué, la classification multi-label reconnaît qu’un texte peut relever de plusieurs catégories. L’incident décrit plus bas touche à la fois à la sécurité, à la finance, à la fraude et à la protection des données : forcer un label unique appauvrirait le routage.
Deux éléments du schéma méritent d’être notés. reasoning impose une justification par label, ce qui améliore la qualité des scores. requires_human_review transforme l’incertitude du modèle en signal opérationnel : au lieu de découvrir après coup qu’une classification était douteuse, vous la faites remonter automatiquement à un humain quand la confiance passe sous 0.7.
from pydantic import BaseModel, Field
class LabelScore(BaseModel):
label: str = Field(description="Nom de la catégorie")
confidence: float = Field(description="Score de confiance entre 0.0 et 1.0")
reasoning: str = Field(description="Justification en une phrase")
class MultiLabelClassification(BaseModel):
primary_label: str = Field(description="Catégorie principale")
all_labels: list[LabelScore] = Field(
description="Toutes les catégories applicables avec leur score de confiance"
)
requires_human_review: bool = Field(
description="True si la classification est incertaine (confiance < 0.7)"
)
text = """
Notre application de paiement mobile présente une faille de sécurité permettant
l'accès aux données bancaires. Plusieurs clients ont signalé des transactions
non autorisées. L'équipe technique est en cours d'investigation.
"""
response = client.chat.parse(
model="mistral-large-latest",
messages=[
{
"role": "system",
"content": """Classifie ce texte parmi les catégories suivantes (plusieurs possibles) :
security, finance, technical, legal, customer_support, compliance, fraud, data_privacy.
Attribue un score de confiance à chaque catégorie applicable."""
},
{"role": "user", "content": text}
],
response_format=MultiLabelClassification,
temperature=0
)
result = response.choices[0].message.parsed
print(f"Catégorie principale : {result.primary_label}")
print(f"Revue humaine nécessaire : {'Oui' if result.requires_human_review else 'Non'}")
for label in result.all_labels:
print(f" [{label.confidence:.0%}] {label.label} — {label.reasoning}")
Factoriser l’appel
Ces trois exemples partagent la même mécanique : un system prompt, un texte, un schéma, temperature=0. Plutôt que de recopier l’appel à chaque nouveau cas, extrayez-le dans une fonction générique. Le TypeVar borné à BaseModel préserve le typage : l’objet retourné a le type du schéma passé en argument, et votre IDE le sait.
from typing import TypeVar, Type
from pydantic import BaseModel
T = TypeVar("T", bound=BaseModel)
def extract(client, text: str, schema: Type[T], system_prompt: str = "") -> T:
"""Extraction structurée réutilisable."""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": text})
response = client.chat.parse(
model="mistral-large-latest",
messages=messages,
response_format=schema,
temperature=0
)
return response.choices[0].message.parsed
# Utilisation
facture = extract(client, texte_facture, InvoiceData, "Extrais les données de la facture.")
review = extract(client, avis_client, ReviewAnalysis, "Analyse cet avis client.")
classification = extract(client, ticket, MultiLabelClassification, "Classifie ce texte.")
Trois cas d’usage très différents, une seule ligne d’appel chacun : c’est le bénéfice concret d’une bonne modélisation des schémas.
Points clés à retenir
- Les schémas imbriqués (
list[LineItem],list[AspectSentiment]) permettent des analyses complexes - Les enums (
Currency, catégories) contraignent les valeurs possibles - Le champ
evidence/reasoningforce le modèle à justifier ses choix - Un wrapper générique réduit la duplication de code
temperature=0est systématique pour ces cas d’usage en production