01 - Construire un jeu de tests
Tant que tu n'as pas 20 cas de test et un chiffre, tu ne sais pas si ton prompt marche.
Temps de lecture : 14 min | Niveau : Intermediaire
Ce que tu sauras faire apres
- Fabriquer un jeu de 20 cas de test representatifs a partir de tes vraies donnees.
- Ajouter volontairement les cas limites qui cassent ton prompt.
- Ecrire un critere de reussite chiffre au lieu de "ca a l'air bon".
- Automatiser la comparaison attendu / obtenu dans un script Python de 30 lignes.
- Rejouer ce jeu de tests a chaque modification de prompt ou de modele.
🎯 Le probleme, en une phrase
Tu testes ton prompt sur les 3 fichiers que tu as sous la main. Ce sont forcement les 3 fichiers les plus propres. Ton prompt passe. Tu le mets en prod sur 40 000 fichiers dont 400 sont bizarres.
Un jeu de tests, c'est simplement : une liste d'entrees, la sortie attendue pour chacune, et un bout de code qui compare.
C'est exactement la meme idee qu'un test unitaire, ou qu'un test dbt test sur une colonne. La seule difference : la sortie n'est pas toujours comparable caractere par caractere.
🧱 Les 3 principes officiels
La documentation Anthropic donne trois principes pour concevoir des evaluations. Ils sont courts, retiens-les.
- Sois specifique a la tache (Be task-specific). Tes cas de test doivent refleter la distribution reelle de tes donnees. Et il faut y mettre les cas limites.
- Automatise des que possible (Automate when possible). Structure les questions pour qu'une machine puisse noter : choix multiple, comparaison de chaine, notation par code, notation par un LLM.
- Privilegie le volume a la qualite (Prioritize volume over quality). Citation exacte de la doc : « More questions with slightly lower signal automated grading is better than fewer questions with high-quality human hand-graded evals. »
Le troisieme principe est contre-intuitif. Il veut dire : 50 cas notes automatiquement, meme imparfaitement, valent mieux que 8 cas notes a la main. Parce que tu vas rejouer ce jeu 30 fois, et que tu ne reliras jamais 8 cas a la main 30 fois.
🧪 Exemple fil rouge : extraire des champs de 20 factures
Tu recois des factures fournisseurs en PDF. Tu veux en extraire 5 champs pour alimenter une table stg_factures :
numero_facturedate_emission(format ISOYYYY-MM-DD)siret_fournisseurmontant_ht(nombre, point decimal)montant_ttc(nombre, point decimal)
Etape 1 : choisir 20 factures qui ressemblent a la realite
Ne prends pas 20 factures du meme fournisseur. Prends la distribution reelle :
| Type de cas | Combien | Pourquoi |
|---|---|---|
| Factures "normales" de tes 5 principaux fournisseurs | 10 | C'est 80 % de ton volume |
| Facture avec plusieurs pages | 2 | Le champ recherche est parfois page 2 |
| Facture avec un avoir (montant negatif) | 1 | Casse souvent le parsing des montants |
| Facture en anglais ou avec date au format US | 2 | 03/04/2026 : mars ou avril ? |
| Facture scannee de travers / OCR sale | 2 | Cas limite classique |
| Facture sans SIRET visible | 1 | Teste si le modele invente ou dit "absent" |
| Document qui n'est PAS une facture (bon de livraison) | 1 | Teste le refus |
| Facture avec deux totaux (dont un acompte) | 1 | Teste l'ambiguite |
Les cas limites recommandes par la doc Anthropic sont exactement ceux-la : donnees d'entree non pertinentes ou inexistantes, donnees trop longues, entrees de mauvaise qualite, et cas ambigus ou meme deux humains ne seraient pas d'accord.
Conseil pratique : garde toujours au moins 1 cas "ce n'est pas le bon type de document" et 1 cas "l'information n'est pas la". Ce sont ceux qui revelent les hallucinations.
Etape 2 : ecrire la verite terrain (ground truth)
Tu remplis a la main, une seule fois, un fichier de reference. Format simple, versionne dans Git a cote du prompt.
[
{
"id": "F001",
"fichier_texte": "factures_txt/acme_2026_03.txt",
"attendu": {
"numero_facture": "FA-2026-0412",
"date_emission": "2026-03-14",
"siret_fournisseur": "80012345600018",
"montant_ht": 1250.00,
"montant_ttc": 1500.00
}
},
{
"id": "F017",
"fichier_texte": "factures_txt/sans_siret.txt",
"commentaire": "cas limite : SIRET absent du document",
"attendu": {
"numero_facture": "2026-889",
"date_emission": "2026-02-02",
"siret_fournisseur": null,
"montant_ht": 320.50,
"montant_ttc": 384.60
}
}
]Le null est important : tu testes que le modele repond null et n'invente pas un SIRET plausible.
La cle fichier_texte pointe vers le texte deja extrait du PDF (OCR ou pdftotext). Tu separes ainsi deux problemes : la qualite de l'extraction de texte, et la qualite du prompt. Si tu melanges les deux, tu ne sauras jamais lequel des deux a casse.
Etape 3 : definir un critere de reussite chiffre
La doc Anthropic recommande des criteres SMART. Traduction concrete pour toi :
- ❌ Mauvais : « le modele extrait bien les champs »
- ✅ Bon : « sur les 20 factures de reference, au moins 19 sur 20 ont les 5 champs exacts, 0 SIRET invente, et le temps de reponse median est sous 8 secondes »
Remarque la structure : un seuil principal, une contrainte bloquante (0 invention), et une contrainte operationnelle. La doc appelle ca l'evaluation multidimensionnelle : ne juge jamais sur une seule metrique.
Pour ce type de tache, la metrique naturelle est l'exactitude par champ :
exactitude_champ = nb de valeurs exactes / nb total de valeurs attenduesAvec 20 factures x 5 champs = 100 valeurs. Un score de 96/100 te dit tout de suite ou tu en es. Et surtout : quand tu changes le prompt, tu vois si tu passes a 98 ou si tu tombes a 91.
🤖 Etape 4 : automatiser la comparaison
Voici le squelette. Il tient en une page et il te servira pour tous tes prompts d'extraction.
import json
import time
import anthropic
client = anthropic.Anthropic()
# Extraction en gros volume : un modele de la gamme Sonnet suffit et coute moins cher.
# Verifie l'identifiant exact sur la page "Models overview" de la doc avant de lancer.
MODELE = "claude-sonnet-5"
PROMPT = """Tu extrais des champs d'une facture fournisseur.
<facture>
{texte_facture}
</facture>
Renvoie UNIQUEMENT un objet JSON avec exactement ces cles :
numero_facture, date_emission, siret_fournisseur, montant_ht, montant_ttc.
Regles :
- date_emission au format YYYY-MM-DD.
- montant_ht et montant_ttc en nombres, point decimal, sans symbole.
- Si une information n'est pas presente dans la facture, mets null.
- N'invente jamais une valeur.
"""
def extraire(texte_facture: str) -> tuple[dict, float, object]:
"""Renvoie (valeurs extraites, latence en secondes, objet usage de l'API)."""
debut = time.perf_counter()
reponse = client.messages.create(
model=MODELE,
max_tokens=512,
messages=[{"role": "user", "content": PROMPT.format(texte_facture=texte_facture)}],
)
latence = time.perf_counter() - debut
texte = next(b.text for b in reponse.content if b.type == "text")
return json.loads(texte), latence, reponse.usage
def comparer(obtenu: dict, attendu: dict) -> dict:
"""Retourne le detail champ par champ."""
resultat = {}
for cle, valeur_attendue in attendu.items():
valeur_obtenue = obtenu.get(cle)
if isinstance(valeur_attendue, float):
ok = valeur_obtenue is not None and abs(float(valeur_obtenue) - valeur_attendue) < 0.01
elif isinstance(valeur_attendue, str):
ok = str(valeur_obtenue).strip().lower() == valeur_attendue.strip().lower()
else: # None
ok = valeur_obtenue is None
resultat[cle] = ok
return resultatEt la boucle qui produit le score :
cas = json.load(open("jeu_de_tests.json", encoding="utf-8"))
total, justes, inventions = 0, 0, 0
for c in cas:
texte = open(c["fichier_texte"], encoding="utf-8").read()
obtenu, latence, usage = extraire(texte)
detail = comparer(obtenu, c["attendu"])
total += len(detail)
justes += sum(detail.values())
# Regle bloquante : le modele a invente une valeur la ou on attendait null
for cle, attendu_v in c["attendu"].items():
if attendu_v is None and obtenu.get(cle) is not None:
inventions += 1
echecs = [k for k, v in detail.items() if not v]
if echecs:
print(f"{c['id']} : echec sur {echecs} (latence {latence:.1f}s)")
print(f"Exactitude : {justes}/{total} = {justes / total:.1%}")
print(f"Inventions : {inventions} (objectif : 0)")La comparaison ici est deterministe : egalite de chaine normalisee, tolerance numerique de 1 centime. C'est la methode evaluate_exact_match de la doc Anthropic, adaptee a ton cas.
Quand la sortie n'est pas comparable exactement (un resume, une explication), tu passes a d'autres methodes : similarite semantique, ROUGE-L, ou notation par un modele. C'est le sujet de la fiche 02 - Le LLM comme juge.
📋 Template pret a copier : specification d'un jeu de tests
Remplis ce bloc avant d'ecrire la moindre ligne de code. Il t'oblige a decider.
TACHE TESTEE : [DECRIS LA TACHE EN UNE PHRASE]
ENTREE : [TYPE DE DONNEE EN ENTREE, EX : TEXTE OCR D UNE FACTURE]
SORTIE ATTENDUE : [FORMAT EXACT, EX : JSON AVEC LES CLES ...]
JEU DE TESTS
- Nombre de cas : [20 MINIMUM]
- Source des cas : [OU TU LES PRENDS, EX : ECHANTILLON ALEATOIRE DE LA TABLE X SUR 6 MOIS]
- Cas normaux : [COMBIEN]
- Cas limites inclus :
1. [CAS OU L INFORMATION EST ABSENTE]
2. [CAS AVEC DONNEE TRES LONGUE]
3. [CAS DE MAUVAIS TYPE DE DOCUMENT]
4. [CAS AMBIGU OU MEME UN HUMAIN HESITE]
5. [AUTRE CAS LIMITE SPECIFIQUE A TON METIER]
METHODE DE NOTATION : [EXACT MATCH | TOLERANCE NUMERIQUE | REGEX | REQUETE SQL DE CONTROLE | LLM JUGE]
CRITERE DE REUSSITE (chiffre)
- Metrique principale : [EX : EXACTITUDE PAR CHAMP] >= [EX : 95 %]
- Contrainte bloquante : [EX : 0 VALEUR INVENTEE]
- Contrainte operationnelle : [EX : LATENCE MEDIANE < 8 S] et [EX : COUT < 0,01 EUR PAR FACTURE]
BASELINE ACTUELLE : [SCORE DU PROMPT ACTUEL, MESURE AVANT DE TOUCHER A QUOI QUE CE SOIT]🔁 Etape 5 : en faire un reflexe
Trois regles simples :
- Mesure la baseline avant de modifier quoi que ce soit. Sinon tu ne sauras jamais si tu ameliores.
- Rejoue le jeu a chaque changement : nouveau prompt, nouveau modele, nouvelle version du modele. Un modele plus recent n'est pas automatiquement meilleur sur ta tache.
- Garde un jeu de reserve (held-out test set) que tu ne regardes jamais pendant que tu optimises. Sinon tu finis par optimiser pour tes 20 cas au lieu d'optimiser pour la realite.
Ou faire tourner ca ? Dans ton repo, en Python, comme montre plus haut. Tu versionnes le prompt et le jeu de tests ensemble, et tu peux le lancer dans ta CI (GitHub Actions) comme un test classique.
Les consoles des fournisseurs proposent aussi des ecrans d'evaluation sans code, pratiques pour degrossir. Ils evoluent vite : va voir l'interface directement plutot que de te fier a une description ecrite il y a six mois. Dans tous les cas, ton jeu de tests finit par vivre dans Git, a cote du prompt.
⚠️ Les pieges classiques
- Tu ecris les cas de test apres avoir vu la sortie du modele. Tu vas inconsciemment ecrire des cas que le modele reussit. Ecris la verite terrain d'abord.
- Tu n'as que des cas faciles. Le score monte a 100 % et ne bouge plus. Un jeu de tests qui ne descend jamais ne t'apprend rien : ajoute des cas durs.
- Tu compares a l'oeil. Au troisieme aller-retour tu arretes de lire. Automatise des le depart.
- Tu melanges plusieurs taches dans un seul jeu. Un jeu = une tache = un critere.
- Tu oublies la variabilite. Le meme prompt lance deux fois peut donner deux resultats. Sur un doute, lance le jeu 3 fois et regarde la dispersion.
A retenir
- 20 cas minimum, dont au moins 5 cas limites choisis volontairement.
- La regle Anthropic : beaucoup de cas notes automatiquement > peu de cas notes a la main.
- Un critere de reussite = une metrique chiffree + une contrainte bloquante + une contrainte operationnelle.
- Mesure la baseline avant de modifier, rejoue apres chaque changement, garde un jeu de reserve.
- La comparaison exacte (chaine normalisee, tolerance numerique) suffit pour la majorite des taches d'extraction en data engineering.
Sources
Toutes ces pages ont ete ouvertes et verifiees le 26 septembre 2026.
- Anthropic - Define success criteria and build evaluations : les 3 principes, les cas limites a inclure, les criteres SMART et la fonction
evaluate_exact_match. - Microsoft Foundry - Built-in evaluators reference : catalogue d'evaluateurs prets a l'emploi (F1, BLEU, ROUGE, Groundedness, Relevance).
- OpenAI - Graders : les 4 types de correcteurs (string check, text similarity, score model, python).
- promptfoo/promptfoo - code d'exemple sur le lancement d'un prompt sur N cas de test avec seuil de reussite :
examples/getting-started/promptfooconfig.yaml. - confident-ai/deepeval - code d'exemple sur un jeu de tests LLM ecrit en pytest :
examples/getting_started/test_example.py.