Du rouge au vert, mesuré
« L'agent a corrigé le bug » ne suffit pas à décider d'un déploiement. Votre directeur technique vous demandera combien de tours, combien de tokens, combien d'euros, avec quelle variance d'une exécution à l'autre, et comment vous savez que c'est vraiment corrigé. Cette leçon assemble les pièces et produit ces chiffres sur une tâche réelle.
La tâche
Le dépôt d'exemple (files/exemple/) est le module de facturation d'un grossiste fictif. Son README fixe trois règles : remise de 5 % à partir de 50 unités et de 10 % à partir de 100, arrondi commercial au centime (0,125 € donne 0,13 €), TVA de 20 % sur le HT remisé. Deux bugs réalistes y sont cachés :
remises.pycomparequantite > seuil: un client qui commande exactement 50 unités n'a pas sa remise ;montants.pyappellequantize(CENTIME)sans mode d'arrondi :decimalapplique alors l'arrondi bancaire (ROUND_HALF_EVEN), et 0,125 devient 0,12.
Le second est le plus intéressant : le code semble correct, et seule la lecture conjointe de la règle métier et de la documentation de decimal le révèle. Quatre tests sur six échouent.
flowchart LR A["make e2e"] --> B["copie du dépôt dans sorties/depot, git init"] B --> C["pytest par le harness : rouge"] C --> D["agent : Gemini + 5 outils, mode auto"] D --> E["pytest par le harness, indépendamment de l'agent"] E -->|"vert et statut terminé"| F["code de sortie 0"] E -->|"sinon"| G["code de sortie 1"] D --> H["trajectoire.jsonl, trajectoire.html"]
Le point clé est l'étape E : le harness relance les tests lui-même. Le compte rendu de l'agent (« les 6 tests passent ») n'est jamais cru sur parole.
Résultat obtenu
Exécution réelle de make e2e, 2026-10-15 à 20 h 49, gemini-3.8-flash, mode auto :
Avant : 4 failed, 2 passed in 0.05s
Après : 6 passed in 0.02s
statut terminé
tours 17
appels d'outils 16
refus de permission 2
points de reprise 2
tokens en entrée 60 661
tokens en sortie 2 321
coût estimé 0.0542 $ (tarif 0.75 / 3.75 $ par M)
durée 45.1 sDiff final, identique dans les exécutions 1, 3 et 4 dont le journal a été conservé : >= dans remises.py, rounding=ROUND_HALF_UP dans montants.py. Aucun test modifié.
Cinq exécutions, même tâche
| Exécution (2026-10-15) | Tours | Appels | Refus | Compaction | Entrée | Sortie | Coût | Durée |
|---|---|---|---|---|---|---|---|---|
| 1 · budget 24 000 | 16 | 15 | 1 | 0 | 56 826 | 1 765 | 0,0492 $ | 44,0 s |
| 2 · budget 5 000 | 13 | 12 | 1 | 1 | 34 290 | 2 171 | 0,0339 $ | 38,7 s |
| 3 · budget 5 000 | 13 | 12 | 1 | 1 | 34 418 | 2 086 | 0,0336 $ | 39,0 s |
4 · make e2e final | 17 | 16 | 2 | 0 | 60 661 | 2 321 | 0,0542 $ | 45,1 s |
| 5 · contrôle des codes de sortie | 16 | 15 | 2 | 0 | 51 578 | 1 683 | 0,0450 $ | 34,1 s |
Ce que ces chiffres disent, avec la prudence qu'impose un échantillon de cinq :
- L'entrée domine le coût. Sur l'exécution 4, l'entrée pèse 0,0455 : 84 % du coût vient de l'historique renvoyé à chaque tour. Les leviers sont donc la longueur de l'historique (compaction, troncature) et le nombre de tours, bien avant la verbosité du modèle.
- La compaction a coûté moins cher ici, environ 0,034 , résumé compris. Mais les trajectoires diffèrent aussi par leur nombre de tours : ne généralisez pas un écart mesuré sur deux exécutions.
- Les trajectoires varient. 13 à 17 tours pour le même résultat. L'exécution 4 a perdu un tour sur un
pytestnu refusé ; les exécutions 1 à 4 ont toutes tentégit diff, refusé. Ajoutergit diffà la liste blanche économiserait probablement un ou deux tours par session. - Le modèle travaille en série. Un seul appel d'outil par tour dans les exécutions 1 à 4, alors que le harness accepte des appels parallèles. Lire les trois fichiers sources en un tour économiserait deux tours ; une consigne système le suggérant est un levier à mesurer.
Au tarif publié pour gemini-3.8-flash (0,75 en sortie par million de tokens jusqu'au 31 décembre 2026, puis 1,50 ), la même session coûtera environ deux fois plus à partir de 2027. Le prix est une variable d'environnement (PRICE_IN_PER_M, PRICE_OUT_PER_M) pour cette raison.
Ce qui casse en production
- L'agent déclare un succès qu'il n'a pas vérifié. Fréquent quand la session s'arrête sur un plafond juste après une édition. Parade : la vérification indépendante de l'étape E, qui conditionne le code de sortie.
- Tests instables. Un test qui échoue une fois sur dix fait conclure à l'agent que sa correction est fausse ; il défait du code correct. Parade : stabiliser la suite avant de la confier à un agent, ou relancer les échecs une fois avant de les montrer.
- Quota. Plusieurs agents en parallèle sur la même clé reçoivent des 429. Le harness réessaie avec attente exponentielle (cinq essais, jusqu'à 60 s), puis échoue proprement. En CI, limitez la concurrence plutôt que d'allonger les attentes.
- Coût de queue. La moyenne est de 0,05 $, mais une session qui tourne en rond jusqu'à
MAX_TURNS=25avec un historique de 20 000 tokens coûte environ dix fois plus. C'est le 90e centile qui fait la facture : mesurez-le (exercice).
La règle de décision
Confiez à un agent en mode auto les tâches dont le critère de réussite est exécutable (une suite de tests, un linter, un build) et vérifié par le harness. Pour une tâche sans critère exécutable, gardez le mode ask et une revue humaine du diff. Avant d'industrialiser, faites tourner la même tâche 20 fois et regardez le taux de réussite, le 90e centile des tours et du coût.
Pratique
Un rapport dit « 6 passed » dans le compte rendu de l'agent, mais make e2e renvoie le code 1. Quelle explication est la plus probable ?
Le code de sortie dépend de la vérification faite par le harness après la session, pas du texte du modèle. Un écart signifie que le dépôt a changé après le dernier pytest de l'agent, ou que l'agent a affirmé sans vérifier. Le réseau coupé n'empêche pas pytest. Un dépassement de budget donnerait le statut « budget épuisé », visible dans le rapport.
Vous devez diviser par deux le coût moyen par tâche. D'après les mesures de cette leçon, par quoi commencez-vous ?
84 % du coût mesuré vient des tokens d'entrée, qui croissent avec le nombre de tours et la taille de l'historique. Raccourcir les réponses agit sur les 16 % restants. Un modèle à sortie moins chère se heurte au même constat. Le journal est écrit localement et ne coûte rien en tokens.
Exercice. Écrivez stats.py : à partir de plusieurs fichiers trajectoire*.jsonl, affichez le taux de réussite, la médiane et le 90e centile des tours et du coût.
Voir la solution
import statistics, sys
from pathlib import Path
from harness.trajectory import load
ends = [next(e for e in load(Path(p)) if e["event"] == "end") for p in sys.argv[1:]]
ok = [e for e in ends if e["status"] == "terminé"]
def p90(xs): return statistics.quantiles(xs, n=10)[-1] if len(xs) >= 2 else xs[0]
for key in ("turns", "cost_usd"):
xs = [e[key] for e in ends]
print(f"{key}: médiane {statistics.median(xs)} p90 {p90(xs):.4f}")
print(f"réussite : {len(ok)}/{len(ends)}")« terminé » signifie que le modèle s'est arrêté de lui-même, pas que la suite est verte. Pour un vrai taux de réussite, journalisez aussi le verdict de la vérification indépendante (événement verify dans cmd_demo) et comptez celui-là.
- Je vérifie le résultat de l'agent indépendamment de ce qu'il affirme
- Je mesure tours, tokens d'entrée et de sortie, coût et durée pour chaque session
- Je sais lire d'où vient le coût d'un agent et quel levier actionner
- Je raisonne sur une distribution d'exécutions, pas sur un seul run