Décider sur des preuves
Lundi, un développeur raccourcit le prompt de tri des tickets : 60 % de jetons en moins, il le teste sur cinq messages, tout paraît correct, la PR est fusionnée. Jeudi, le service client découvre que l'assistant promet des « remboursements immédiats » sur des doubles prélèvements, ce que la politique interdit. Personne n'a eu tort de vouloir réduire le coût. L'erreur est d'avoir décidé sans instrument de mesure.
Le mécanisme : quatre questions, un code de sortie
Un harness d'évaluation répond à quatre questions, toujours dans cet ordre :
- Sur quels cas ? Un jeu de données figé, versionné, conçu pour discriminer (leçon 1.2).
- Noté comment ? Des scorers, du plus déterministe au plus coûteux (module 2).
- Comparé à quoi ? À la version actuellement en production, sur les mêmes cas, avec une marge d'erreur (leçon 3.1).
- Pour décider quoi ? Une règle écrite à l'avance, traduite en code de sortie 0 ou 1 (leçon 3.2).
sequenceDiagram participant Dev as "Pull request" participant H as "Harness" participant App as "Application (A puis B)" participant J as "Scorers et juge" Dev->>H: "modèle ou prompt modifié" H->>App: "30 cas, version A (référence)" H->>App: "30 cas, version B (candidat)" App-->>H: "sorties (cache si déjà vues)" H->>J: "noter chaque sortie" J-->>H: "scores par cas" H->>H: "différences appariées, IC 95 %" H-->>Dev: "rapport.html + code 0 ou 1"
On distingue ce harness de deux choses voisines. L'observabilité (traces, tableaux de bord) regarde le trafic réel après coup ; elle détecte, elle n'empêche pas. Un benchmark public mesure un modèle sur une tâche générique ; il ne dit rien de votre prompt ni de vos clients. Le harness travaille avant la mise en production, sur votre tâche, avec vos cas.
La mise en œuvre : le contrat de la cible
Le harness n'appelle jamais une copie du prompt dans un notebook. Il importe le vrai code de l'application, par un contrat minimal :
def trier(inp: dict[str, Any], *, model: str, client: Any, prompt: str) -> dict[str, Any]:
r = client.generate_content(
model=model,
contents=inp["ticket"],
config=types.GenerateContentConfig(
system_instruction=prompt, temperature=0,
response_mime_type="application/json", response_json_schema=TRIAGE_SCHEMA),
)
return json.loads(r.text)Trois paramètres viennent du harness : le modèle, le client (qui compte chaque jeton, leçon 2.1) et le prompt (pour comparer deux versions sans toucher au code). Tout le reste se déclare dans une suite :
nom: triage
jeu: data/tickets.jsonl
cible: app.support:trier
modele: gemini-3.8-flash # la version en production ; une PR qui le change est comparée à main
prompt: app/prompts/triage_v1.txt
concurrence: 4
budget_usd: 0.50Installez le dépôt et vérifiez que tout fonctionne sans clé :
cd files
make setup
make testRésultat obtenu (2026-10-16)
Prêt. Renseignez .env puis : make test
.............................. [100%]
30 passed in 0.68smake setup a pris 14 secondes sur un poste Linux avec Python 3.10.12 (google-genai 2.31.0). Les 30 tests utilisent un faux client Gemini (tests/fakes.py) : ils vérifient la logique du harness, pas la qualité du modèle.
Votre équipe veut évaluer un nouveau prompt. Un data scientist propose de recopier le prompt dans un notebook et d'y appeler Gemini sur 50 tickets, « pour aller vite ». Que répondez-vous ?
La divergence entre le code évalué et le code déployé est la première cause de « l'éval était verte, la prod casse ». Le schéma de sortie, la température, le nettoyage du JSON, le system_instruction vivent dans le code de l'application. temperature=0 ne règle rien à ce problème. Le notebook reste utile pour explorer, pas pour décider.
Ce qui casse en production
| Symptôme | Cause réelle | Parade |
|---|---|---|
| « Testé sur 5 exemples, ça marchait » puis incident | échantillon choisi après coup, sans cas difficiles | jeu de données écrit avant le changement, avec cas limites (leçon 1.2) |
| L'évaluation de l'agent a créé de vrais remboursements | les outils de l'agent pointaient vers la production | outils factices ou bac à sable obligatoires dans la suite ; ici COMMANDES est un dictionnaire en mémoire |
| Scores excellents, clients mécontents | on évalue la catégorie, personne n'évalue le texte envoyé au client | au moins un scorer par sortie visible du client (juge, leçon 2.3) |
| Deux personnes obtiennent des chiffres différents | modèle, prompt ou jeu de données non tracés | summary.json enregistre modèle, empreinte du prompt, du code et du jeu de données |
La règle de décision
Construisez un harness dès qu'un changement de modèle ou de prompt peut atteindre un client sans relecture humaine, ou dès que le coût d'une erreur dépasse le coût de 0,30 USD d'évaluation. N'en construisez pas pour un prototype jetable ou un usage interne où chaque sortie est relue : une grille de relecture suffit. Surveillez un indicateur dès le premier jour : le nombre de changements fusionnés sans passage par la porte.
Un responsable propose de remplacer le harness par un tableau de bord de satisfaction client, « qui mesure la vraie qualité ». Quel est le meilleur argument pour garder les deux ?
Les deux instruments ne jouent pas le même rôle : l'observabilité constate, le harness prévient. La satisfaction est utile (et ses pires cas alimentent le jeu de données), mais elle arrive tard et mélange mille causes. Dire qu'elle n'est pas corrélée à la qualité est faux ; dire qu'elle coûte trop cher ne répond pas à la question.
Pratique
Exercice. Écrivez une seconde cible resumer(inp, *, model, client, prompt) qui produit un résumé de ticket en une phrase, et la suite YAML minimale qui l'évalue avec un seul scorer de type regex vérifiant que le résumé fait moins de 200 caractères. Quelles clés de la suite sont obligatoires ?
Voir la solution
def resumer(inp, *, model, client, prompt):
r = client.generate_content(model=model, contents=inp["ticket"],
config=types.GenerateContentConfig(system_instruction=prompt, temperature=0))
return {"resume": r.text.strip()}nom: resume
jeu: data/tickets.jsonl
cible: app.support:resumer
prompt: app/prompts/resume_v1.txt
scorers:
- {nom: longueur, type: regex, champ: resume, motif: "(?s).{1,199}"}Les clés obligatoires sont nom, jeu, cible et scorers (vérifiées par Suite.load). modele se donne dans la suite ou par --model. Le motif utilise (?s) pour que . accepte les retours à la ligne, et fullmatch impose la longueur totale.
- Je sais expliquer la différence entre harness, observabilité et benchmark public.
- Je fais appeler par le harness la fonction de production, jamais une copie.
- J'ai obtenu
30 passedavecmake test, sans clé. - Je sais où se déclarent le modèle, le prompt et le budget d'une suite.