Aller au contenu principal

Module 9 — Évaluation systématique d'une consigne

Les huit modules précédents ont donné des techniques. Ce module donne le seul moyen honnête de choisir entre deux techniques : mesurer. Sans jeu de test annoté, tout jugement sur la qualité d'une consigne est une impression, et les impressions se trompent.

Pourquoi une consigne « qui a l'air de marcher » ne suffit pas

Sur les cinq courriels que vous testez à la main pendant le développement, vous n'observez pas la même distribution que sur les milliers de courriels qui arrivent en production. Trois biais typiques :

Biais de sélection. Vous testez sur les courriels que vous connaissez, et vous les connaissez parce qu'ils sont normaux. Les cas limites — le courriel bilingue, celui qui ne contient qu'une salutation, celui qui mélange deux réclamations — sont absents de votre échantillon.

Biais de confirmation. Vous regardez d'abord si le champ le plus visible (le motif) est correct. Vous oubliez de vérifier l'urgence, qui change une fois sur dix. Les erreurs invisibles ne sont pas les moins graves.

Biais de fraîcheur. Vous venez de modifier la consigne, le premier courriel que vous testez donne la bonne réponse, vous concluez que la modification est bonne. Cinq exécutions ne sont pas un échantillon représentatif.

L'évaluation systématique corrige ces trois biais en même temps.

Constituer le jeu de test

Trente à cinquante entrées suffisent pour commencer. Elles doivent couvrir :

  • Les cas nominaux (60 à 70 % du jeu)
  • Les cas ambigus (15 à 20 %) : plusieurs produits, motifs mélangés, urgence indirecte
  • Les cas dégradés (10 à 15 %) : courriels courts, très longs, bilingues, contenant du HTML, contenant du JSON
  • Les cas hostiles (5 à 10 %) : tentatives d'injection connues (module 8)

Chaque entrée est annotée par un humain avec la sortie attendue. L'annotation est l'étape la plus coûteuse ; c'est aussi la plus rentable.

[
{
"id": "cr_042",
"courriel": "Bonjour, ma cafetière ne chauffe plus...",
"attendu": {
"motif": "produit_defectueux",
"produit": "cafetière Baresso Duo",
"urgence": "moyenne",
"action_demandee": "remplacement",
},
"categorie": "nominal",
},
...
]

Métriques par champ, pas globales

Une métrique globale « la consigne A est meilleure que la B avec 87 % contre 83 % » cache l'essentiel. Un tableau par champ révèle la mécanique :

ChampConsigne AConsigne B
motif95 %92 %
produit78 %85 %
urgence82 %79 %
action_demandee91 %88 %

La consigne B est meilleure sur le produit, moins bonne sur le motif. Selon la question métier — les erreurs de motif coûtent-elles plus que les erreurs de produit ? — la décision change.

Sur un champ à valeurs énumérées, la métrique de base est l'exactitude par valeur (correcte quand la prédiction égale l'attendu). Sur un champ libre comme le produit, on peut préférer une correspondance approximative — normaliser, retirer la casse, comparer.

Exactitude(f)=1Ni=1N1(y^i(f)=yi(f))\text{Exactitude}(f) = \frac{1}{N} \sum_{i=1}^{N} \mathbb{1}(\hat{y}_i^{(f)} = y_i^{(f)})

Pour un champ à quatre classes déséquilibrées, l'exactitude peut être trompeuse — voir cours 04 sur l'apprentissage supervisé. Une matrice de confusion par champ complète l'image.

Comparaison A/B honnête

Comparer deux consignes exige un protocole strict :

  1. Même jeu de test.
  2. Même modèle, même version, même temperature.
  3. Résultats archivés avec la version exacte de la consigne.
  4. Test statistique sur la différence, pas comparaison de moyennes brutes.
def evaluer(consigne: str, jeu: list) -> dict:
predictions = [appeler(consigne, x["courriel"]) for x in jeu]
metriques = {}
for champ in ["motif", "produit", "urgence", "action_demandee"]:
correctes = sum(1 for p, x in zip(predictions, jeu)
if p[champ] == x["attendu"][champ])
metriques[champ] = correctes / len(jeu)
return metriques

resultats_A = evaluer(CONSIGNE_A, jeu_test)
resultats_B = evaluer(CONSIGNE_B, jeu_test)

Sur un jeu de 50 entrées, un écart de 4 points sur un champ (par exemple 88 % contre 92 %) représente 2 courriels de différence. Avec cette taille d'échantillon, l'écart n'est pas significatif — il peut disparaître au prochain tirage. Pour trancher, il faut soit un jeu plus grand, soit répéter la mesure sur plusieurs sous-échantillons.

Détecter les régressions au changement de modèle

Le fournisseur met à jour son modèle. Votre consigne, qui marchait, peut échouer différemment. Sans jeu de test, vous vous en apercevez par les plaintes clients, ce qui coûte cher.

La discipline utile :

# ci_evaluation.py, à lancer avant chaque déploiement
seuil = {"motif": 0.90, "produit": 0.75, "urgence": 0.80,
"action_demandee": 0.85}

metriques = evaluer(CONSIGNE, jeu_test)
for champ, s in seuil.items():
if metriques[champ] < s:
raise SystemExit(f"regression sur {champ} : {metriques[champ]} < {s}")

Ce script intégré au pipeline CI casse le déploiement dès qu'une métrique passe sous son seuil. Le compromis est de fixer les seuils correctement : trop bas, les régressions passent ; trop haut, les faux positifs bloquent inutilement.

Ce que ce jeu ne peut pas mesurer

Trois angles morts communs :

La qualité stylistique des sorties libres (voir module 6). Une réponse rédigée peut être correcte en contenu et médiocre en ton. La mesure automatique de ton exige soit un annotateur humain, soit un « modèle juge » qui a ses propres biais.

Le coût. Une consigne qui passe de 82 % à 88 % en doublant le nombre de jetons envoyés peut être un mauvais choix économique. Il faut mesurer aussi la moyenne des jetons entrée et sortie par appel.

La latence en production. Les mesures faites sur un jeu de test statique ne rendent pas compte des variations de latence sous charge. Une consigne qui déclenche une chaîne de pensée longue peut ralentir l'application au point de créer une file d'attente.

Score global=αExactitudeβCouˆtγLatence\text{Score global} = \alpha \cdot \text{Exactitude} - \beta \cdot \text{Coût} - \gamma \cdot \text{Latence}

Avec des poids α\alpha, β\beta, γ\gamma qui dépendent du métier. Ce score est une convention interne, pas une vérité — mais l'écrire force l'équipe à énoncer ses priorités.

L'évaluation sans jeu de test est de la superstition

« La consigne v3 me semble meilleure » n'est pas une évaluation. Sans jeu de test annoté, sans métriques par champ, sans comparaison statistique, toutes les affirmations sur la qualité d'une consigne relèvent de l'intuition — souvent fausse, jamais reproductible. C'est la faute la plus fréquente des équipes qui débutent en ingénierie des consignes, et celle qui coûte le plus cher en production.

Automatiser dès le troisième changement de consigne

La première fois qu'on modifie une consigne, l'évaluation à la main paraît raisonnable. La deuxième aussi. À la troisième, on ne se souvient plus de la version précédente. Écrire le script d'évaluation dès que l'on prévoit d'itérer prend une heure et sauve les mois suivants.

En résumé

  • Une consigne qui « a l'air de marcher » sur cinq exemples cache presque toujours des erreurs invisibles sur les cas limites.
  • Le jeu de test annoté couvre cas nominaux, ambigus, dégradés et hostiles, et se maintient au fil du temps comme une suite de tests.
  • Mesurer par champ révèle des compromis que la moyenne globale masque, et permet de décider en fonction du coût métier de chaque erreur.
  • Un pipeline CI d'évaluation détecte les régressions au changement de modèle avant qu'elles n'atteignent la production.

Module suivant : la bibliothèque de consignes — pour qu'une consigne évaluée avec soin ne se perde pas dans le fichier notes.txt d'un seul développeur.