Sur cette page
- Un programme ne lit pas une phrase : il lit des champs. Le format imposé lui donne les champs, à chaque réponse.
- Le format se décrit une fois, avec Pydantic ; les quatre éditeurs l'acceptent.
- OpenAI et Anthropic garantissent le respect du format ; Google et Mistral le font respecter, et demandent de vérifier.
- Un format respecté ne veut pas dire un contenu juste : un montant mal lu passe le format sans encombre.
- Les montants se contrôlent par des règles écrites dans le programme ; le modèle lit, il ne calcule rien.
Objectif
Extraire d'une facture le fournisseur, le numéro, la date, les lignes et les montants, dans un objet que le programme lit sans erreur, puis arrêter par des règles une lecture fausse.
La facture est fabriquée. L'appel au modèle (étape 2) suit la documentation de chaque éditeur, lue le 7 octobre 2026, et n'a pas encore été exécuté par Meridal. Les étapes 1 et 3 ne font aucun appel : leurs sorties sont réelles.
Données fournies
Le texte d'une facture, recopié ici et dans le fichier facture.py de l'étape 1 :
Consignes
Étape 1. Décrire le format
Une classe Pydantic par objet : chaque champ a un nom et un type ; une description dit au modèle ce qu'on attend, comme le format de la date. Les règles de contrôle vivent dans le même fichier.
facture.pyfrom pydantic import BaseModel, Field
TEXTE = """FACTURE n° 2026-0412, du 15 septembre 2026
Émise par : Cabinet Exemple, conseil aux entreprises (entreprise fictive)
Conseil, 10 heures : 1 000,00 € HT
Frais de déplacement : 250,00 € HT
Total hors taxe : 1 250,00 €
TVA à 20 % : 250,00 €
Total TTC : 1 500,00 €"""
class Ligne(BaseModel):
libelle: str
montant_ht: float
class Facture(BaseModel):
fournisseur: str
numero: str
date: str = Field(description="Date de la facture, au format AAAA-MM-JJ")
lignes: list[Ligne]
total_ht: float
taux_tva: float = Field(description="Taux de TVA en pour cent, par exemple 20")
total_tva: float
total_ttc: float
def controler(f, ecart=0.01):
"""Les règles qu'une facture juste respecte. Le modèle lit ; il ne calcule rien."""
regles = [
("Hors taxe = somme des lignes", sum(l.montant_ht for l in f.lignes), f.total_ht),
("TVA = hors taxe × taux", f.total_ht * f.taux_tva / 100, f.total_tva),
("Total = hors taxe + TVA", f.total_ht + f.total_tva, f.total_ttc),
]
return [(nom, abs(calcule - lu) <= ecart, calcule, lu) for nom, calcule, lu in regles]Ce que Pydantic en tire, et qui part à l'éditeur : un schéma JSON, où chaque champ est obligatoire.
schema.pyimport json from facture import Facture print(json.dumps(Facture.model_json_schema()["required"], ensure_ascii=False)) print(json.dumps(Facture.model_json_schema()["properties"]["date"], ensure_ascii=False))
uv run schema.py["fournisseur", "numero", "date", "lignes", "total_ht", "taux_tva", "total_tva", "total_ttc"]{"description": "Date de la facture, au format AAAA-MM-JJ", "title": "Date", "type": "string"}
Essayé le 7 octobre 2026 avec Python 3.12.7 et Pydantic 2.6.0.
Étape 2. Demander le format
responses.parse envoie le format, et rend un objet Facture déjà lu, dans output_parsed.
extraire.pyfrom dotenv import load_dotenv
load_dotenv()
from openai import OpenAI
from facture import TEXTE, Facture
client = OpenAI()
response = client.responses.parse(
model="gpt-6-luna",
input=[
{"role": "system", "content": "Extrais les informations de la facture."},
{"role": "user", "content": TEXTE},
],
text_format=Facture,
)
facture = response.output_parsed
print(facture)uv run extraire.pyPas encore exécuté par Meridal. Ce qu'on attend : une Facture avec le fournisseur, le numéro 2026-0412, la date 2026-09-15, deux lignes et les quatre montants de la facture.
messages.parse envoie le format, et rend l'objet lu dans parsed_output.
extraire.pyfrom dotenv import load_dotenv
load_dotenv()
import anthropic
from facture import TEXTE, Facture
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Extrais les informations de cette facture :\n\n" + TEXTE}],
output_format=Facture,
)
facture = response.parsed_output
print(facture)uv run extraire.pyPas encore exécuté par Meridal. Ce qu'on attend : une Facture avec le fournisseur, le numéro 2026-0412, la date 2026-09-15, deux lignes et les quatre montants de la facture.
Le format part sous forme de schéma JSON ; la réponse est du JSON, que Pydantic relit et vérifie (model_validate_json).
extraire.pyfrom dotenv import load_dotenv
load_dotenv()
from google import genai
from facture import TEXTE, Facture
client = genai.Client()
interaction = client.interactions.create(
model="gemini-3.8-flash",
input="Extrais les informations de cette facture :\n\n" + TEXTE,
response_format={
"type": "text",
"mime_type": "application/json",
"schema": Facture.model_json_schema(),
},
)
facture = Facture.model_validate_json(interaction.output_text)
print(facture)uv run extraire.pyPas encore exécuté par Meridal. Ce qu'on attend : une Facture avec le fournisseur, le numéro 2026-0412, la date 2026-09-15, deux lignes et les quatre montants de la facture.
chat.parse envoie le format, et rend l'objet lu dans choices[0].message.parsed.
extraire.pyfrom dotenv import load_dotenv
load_dotenv()
import os
from mistralai.client import Mistral
from facture import TEXTE, Facture
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
reponse = client.chat.parse(
model="mistral-large-latest",
messages=[
{"role": "system", "content": "Extrais les informations de la facture."},
{"role": "user", "content": TEXTE},
],
response_format=Facture,
max_tokens=1024,
temperature=0,
)
facture = reponse.choices[0].message.parsed
print(facture)uv run extraire.pyPas encore exécuté par Meridal. Ce qu'on attend : une Facture avec le fournisseur, le numéro 2026-0412, la date 2026-09-15, deux lignes et les quatre montants de la facture.
Étape 3. Contrôler
Trois règles qu'une facture juste respecte, appliquées à deux lectures de la même facture : la bonne, et une où le total TTC a été lu 1 580 au lieu de 1 500. Les deux lectures sont fabriquées ; le contrôle, lui, a tourné.
controle.pyfrom facture import Facture, Ligne, controler
# deux lectures de la même facture : la bonne, et une où un 0 a été lu comme un 8
lignes = [Ligne(libelle="Conseil, 10 heures", montant_ht=1000.0),
Ligne(libelle="Frais de déplacement", montant_ht=250.0)]
juste = Facture(fournisseur="Cabinet Exemple", numero="2026-0412", date="2026-09-15", lignes=lignes,
total_ht=1250.0, taux_tva=20.0, total_tva=250.0, total_ttc=1500.0)
fausse = juste.model_copy(update={"total_ttc": 1580.0})
for nom, f in (("Lecture juste", juste), ("Lecture fausse", fausse)):
print(nom)
for regle, passe, calcule, lu in controler(f):
print(f" {'passe ' if passe else 'ÉCHOUE'} {regle} : {calcule:.2f} attendu, {lu:.2f} lu")uv run controle.pyLecture juste passe Hors taxe = somme des lignes : 1250.00 attendu, 1250.00 lu passe TVA = hors taxe × taux : 250.00 attendu, 250.00 lu passe Total = hors taxe + TVA : 1500.00 attendu, 1500.00 luLecture fausse passe Hors taxe = somme des lignes : 1250.00 attendu, 1250.00 lu passe TVA = hors taxe × taux : 250.00 attendu, 250.00 lu ÉCHOUE Total = hors taxe + TVA : 1500.00 attendu, 1580.00 lu
Essayé le 7 octobre 2026 avec Python 3.12.7 et Pydantic 2.6.0.
Les deux lectures ont la bonne forme : tous les champs, au bon type. Seule la règle arrête la seconde. Une facture qui échoue se met de côté avec la règle en échec, pour qu'une personne la regarde.
Critères de réussite
Garanties de chaque éditeur
Indications
OpenAI refuse le schéma
CauseChez OpenAI, chaque champ doit être obligatoire, la racine doit être un objet, et chaque objet doit interdire les champs en plus ; certains mots du schéma ne sont pas acceptés.
À fairePassez par responses.parse avec la classe Pydantic, qui écrit le schéma comme il faut ; un champ facultatif s'écrit « type ou null ».
La première demande est lente
CauseUn nouveau schéma se prépare à la première demande ; chez Anthropic, il reste prêt 24 heures après son dernier usage.
À faireRien : les demandes suivantes vont plus vite.
Chez Anthropic, les citations ne marchent plus
CauseLe format imposé ne se combine pas avec les citations.
À faireChoisir : un objet à lire, ou une réponse qui cite ses sources.
La réponse est coupée
CauseLe plafond de tokens est trop bas pour l'objet entier.
À faireRelevez max_tokens, et traitez une réponse incomplète comme une erreur.
Correction commentée
Ouvrir la correction
La formeLes quatre éditeurs reçoivent le même format, tiré de la même classe : seuls le nom de la méthode et l'endroit où lire l'objet changent.
Le fondLa date demande une conversion (« 15 septembre 2026 » en 2026-09-15) : c'est le rôle de la description du champ. Les montants, eux, se recopient tels qu'ils sont écrits ; le programme les vérifie ensuite, et ne demande jamais au modèle de les calculer.
Pourquoi les règlesLe format garantit la forme, pas la justesse : une lecture fausse a exactement la même forme qu'une bonne. Dans la chaîne du dossier de vente (réalisation R3), un chiffre douteux est mis de côté, jamais comblé.
Extensions
- Des montants exacts : un nombre à virgule flottante arrondit ; pour la comptabilité, recopier le montant en texte et le convertir en
Decimaldans le programme. - Un lot de factures : la même chaîne sur un dossier entier, avec la liste des factures mises de côté ; c'est le projet d'extraction de factures.
- Le document lui-même : lire un PDF ou une image plutôt qu'un texte recopié, avec les limites que dit l'extraction des chiffres d'un document.
Lectures suivantes
Rien avant : c'est un point de départ.
Réponse dans un format imposé
Limites et sources
L'appel de l'étape 2 suit la documentation des éditeurs du 7 octobre 2026 et n'a pas encore été exécuté par Meridal. Les étapes 1 et 3 ont tourné avec Python 3.12.7 et Pydantic 2.6.0 ; la version publiée de Pydantic est la 2.13.5.
La facture et les deux lectures du contrôle sont fabriquées.
- OpenAI, Structured Outputs ; Anthropic, Structured outputs ; Google, Structured output ; Mistral, Custom structured outputs.
- Le relevé complet :
apprendre/recherches/2026-10-07-dev-appels-modeles.json.
Écrit par Meridal, avec l'aide de l'IA ; chaque fait vérifié à sa source et relu avant publication. Texte et schémas sous licence CC BY 4.0 : reprenez-les en citant Meridal. Le code sous licence MIT.
Préparer les documents qui reviennent
1 650 € à 5 100 €, puis sur devis