L'agent a écrit « terminé », mais la fonctionnalité ne marche pas : pourquoi et comment vérifier le travail d'un agent IA
L'agent écrit « terminé » parce qu'il évalue lui-même son travail et ne voit que ce qu'il a vérifié : en général des tests unitaires, qui ne voient ni les jointures entre modules, ni les migrations, ni la configuration, ni le scénario de bout en bout. On y remédie par la structure, non par le prompt : une définition of done dans AGENTS.md, une exécution E2E obligatoire, un script de vérification seul habilité à passer le statut à passing, et un évaluateur indépendant au contexte propre.
Pourquoi cela arrive
Tous ceux qui travaillent avec des agents connaissent la situation : l'agent a écrit « terminé », mais la fonctionnalité ne marche pas. Il ne ment pas délibérément. Il rend compte de ce qu'il a eu le temps de vérifier et il est sincèrement convaincu que cela suffit. L'écart entre l'affirmation et la réalité tient au schéma « l'exécutant évalue lui-même son travail », non à un défaut d'un modèle particulier.
Il y a trois raisons. La première : les modèles sont enclins à une confiance excessive, la certitude d'une réponse dépasse sa justesse. La deuxième : l'agent évalue son travail dans le même contexte que celui où il l'a fait ; il s'est déjà convaincu que la solution est juste, et demander « sois plus objectif » ne change pas le contexte. La troisième, la plus concrète : l'agent ne dispose que de vérifications partielles. Les tests unitaires passent vite au vert, alors que le scénario de bout en bout n'a jamais été lancé.
Chronique d'un faux « terminé » typique
L'agent a reçu la mission de mettre en place la réinitialisation du mot de passe. Il modifie le schéma de la base, ajoute un point d'accès, branche un modèle d'e-mail et lance les tests unitaires : tout est vert. Un « Terminé » assuré apparaît dans le chat.
En réalité, il s'est passé ceci :
- la migration a échoué à mi-parcours et a laissé le schéma dans un état incohérent ;
- le modèle d'e-mail existe, mais le service de messagerie (SMTP) n'est pas configuré dans l'environnement : aucun e-mail ne part ;
- le scénario « l'utilisateur clique sur le lien → saisit un nouveau mot de passe → se connecte » n'a jamais été exécuté.
Aucun de ces problèmes ne pouvait être détecté par les tests unitaires : la base était simulée, l'envoi d'e-mail était simulé, et aucune exécution de bout en bout n'avait eu lieu.
Pourquoi des tests verts ne prouvent pas que la fonctionnalité marche
Un test unitaire vérifie un module isolément : les dépendances sont remplacées par des simulations (mocks), le monde extérieur est coupé. Cela apporte de la rapidité et de la précision, mais aussi une cécité systématique. Cette cécité n'est pas un défaut des tests, c'est la conséquence de leur conception. Des tests unitaires verts prouvent seulement que chaque élément fonctionne séparément, pas que les éléments fonctionnent ensemble.
Il existe quatre classes de défauts qu'un test unitaire ne voit tout simplement pas :
| Classe de défaut | Exemple | Pourquoi un test unitaire le manque |
|---|---|---|
| Interfaces incompatibles | Un composant renvoie un chemin relatif, l'autre attend un chemin absolu | Chacun est testé avec sa propre simulation, qui « accepte » n'importe quel format |
| Propagation de l'état | Le schéma de la base a changé, mais le cache de l'ORM garde l'ancien | Le test unitaire n'a ni vraie base de données ni cache |
| Cycle de vie des ressources | Descripteurs de fichiers et connexions non libérés, fuite de mémoire sur les gros fichiers | Le test travaille sur de petites données et se termine avant l'épuisement des ressources |
| Environnement et configuration | Une variable d'environnement manque, SMTP n'est pas configuré, la migration n'a pas été appliquée | Le test ne lance pas l'application dans sa configuration réelle |
Les niveaux de vérification sont imbriqués : tout ce que détectent les tests unitaires est aussi détecté par les tests d'intégration, et tout ce que détectent les tests d'intégration est détecté par une exécution de bout en bout (E2E). Chaque niveau supérieur voit strictement davantage. C'est pourquoi le droit de dire « terminé » ne doit appartenir qu'au niveau le plus élevé.
Mesurer le problème : l'écart de vérification
Avant de corriger, comptez. Prenez les 10 dernières tâches clôturées par l'agent. Pour chacune, répondez à deux questions : l'agent a-t-il dit « terminé », et une vérification indépendante l'a-t-elle confirmé (vous avez parcouru le scénario vous-même, le test E2E est vert) ? La proportion des écarts est votre écart de vérification (verification gap).
Ce chiffre est utile pour deux raisons. Il montre l'ampleur du problème sans débat du type « ça semble marcher plutôt bien ». Et il fournit une base de référence : après chaque modification du processus, vous le recalculez sur un ensemble de tâches comparable et vous voyez si cela a aidé.
Vérifier vraiment : trois couches de validation
Une comparaison parlante est celle du garage. Une voiture passe trois postes, et aucun ne peut être sauté.
- Diagnostic (secondes) : syntaxe, types, linter. Il attrape les coquilles et les grosses erreurs.
- Banc d'essai (minutes) : tests unitaires et d'intégration, l'application démarre. Le moteur tourne, mais la voiture reste sur place.
- Essai sur route : un scénario de bout en bout tel qu'un utilisateur le parcourt — via l'API, un navigateur ou la CLI, avec une vraie base de données et une vraie configuration.
La règle de progression : on ne part pas en essai sur route tant que le diagnostic signale des erreurs, et on ne rend pas la voiture sans avoir fait l'essai. Des tests unitaires verts correspondent au deuxième poste franchi, pas au troisième.
Il est pratique de regrouper tout cela derrière un point d'entrée unique :
# Makefile
check:
ruff check .
mypy app
pytest tests/unit tests/integration
e2e:
docker compose up -d db mailhog
alembic upgrade head
pytest tests/e2e
verify: check e2e
Ici, le E2E démarre une vraie base de données et un serveur de messagerie de test, applique les migrations et déroule le scénario de bout en bout. Une migration qui échoue ou une messagerie non configurée se révèlent immédiatement, et non chez le client.
La définition of done dans AGENTS.md
L'agent ne doit pas décider lui-même de ce que signifie « terminé ». Le critère est inscrit dans le fichier d'instructions et formulé de façon vérifiable :
## Definition of Done
Une fonctionnalité n'est terminée que si TOUS les points sont remplis :
1. make check s'est terminé avec le code 0
2. make e2e s'est terminé avec le code 0, et le scénario de la fonctionnalité figure dans tests/e2e
3. Les migrations s'appliquent sur une base vierge et peuvent être annulées (rollback)
4. Les nouvelles variables d'environnement sont décrites dans .env.example
5. PROGRESS.md indique ce qui a été fait et comment c'est vérifié
Ne constituent PAS un signe de fin :
- « le code est écrit et semble correct »
- « les tests unitaires passent »
- « chez moi, ça marche en local »
Une liste explicite de ce qui ne compte pas comme achèvement fonctionne mieux qu'on ne le croit : l'agent s'arrête moins souvent au premier passage vert.
Qui a le droit de passer le statut « passing »
L'étape suivante consiste à retirer complètement le statut d'achèvement des mains de l'agent. Les fonctionnalités sont tenues dans une liste (par exemple feature_list.json) avec les champs « comportement », « commande de vérification » et « état ». Seul un script peut faire passer une fonctionnalité à passing : il lance le make check commun, puis la commande de vérification propre à la fonctionnalité, et en cas de succès il enregistre le statut avec le hash du commit comme preuve. En cas d'échec, la fonctionnalité reste active — il n'y a pas de « presque réussi ».
{
"id": "F04",
"behavior": "L'utilisateur reçoit un e-mail avec un lien et change son mot de passe",
"verification": "pytest tests/e2e/test_password_reset.py",
"state": "active"
}
Le piège est le suivant : le script ne vérifie que ce qui est écrit dans le champ verification. Si une vérification d'architecture fait partie de la définition of done mais n'est pas dans la commande de la fonctionnalité, la faille reste invisible. C'est pourquoi le make check commun doit inclure toutes les vérifications obligatoires, et pas seulement les tests.
Un évaluateur indépendant
L'auto-évaluation de l'exécutant n'est jamais une source de vérité indépendante. La solution structurelle consiste à séparer les rôles :
- le planificateur découpe l'exigence en fonctionnalités assorties de critères d'acceptation et n'écrit pas de code ;
- le générateur implémente une fonctionnalité et lance les tests, mais ne décide pas si elle est terminée ;
- l'évaluateur rend un verdict selon une grille de critères.
L'évaluateur ne doit pas voir les raisonnements du générateur, sinon il hérite de sa confiance. Il ne reçoit que l'exigence, le diff et les résultats des exécutions. Dans Claude Code, c'est facile à mettre en place avec un sous-agent, qui dispose de son propre contexte propre. Mieux encore si l'évaluateur est un autre modèle : ses angles morts ne sont pas les mêmes. Une appréciation du type « ça a l'air bien » est interdite : chaque constat doit renvoyer à un fichier, une ligne ou un journal.
Des messages d'erreur que l'agent peut corriger seul
Une vérification n'est utile que si sa sortie conduit à une correction. « Test failed » ou une simple AssertionError ne disent rien à l'agent. Un bon message indique ce qui est cassé, pourquoi la règle existe et comment corriger :
ERREUR arch-check : app/services/refund.py importe app/db/session.py
POURQUOI : les services n'accèdent pas directement à la base, seulement via les repositories
CORRECTION : utilisez RefundRepository de app/repositories/refund.py
Ce format transforme une vérification en un conseil : l'agent corrige la transgression sans votre intervention.
La vérification manuelle : là où elle reste nécessaire
L'automatisation ne remplace pas l'humain, mais elle réduit son rôle. La vérification manuelle reste nécessaire là où le critère est difficile à formaliser : l'apparence de l'interface, les textes, le confort d'un parcours. Faites-la à l'aide d'une liste de contrôle issue des critères d'acceptation, et non en « cliquant un peu partout ». Si vous écrivez pour la deuxième fois le même commentaire à l'agent, c'est le signal de le transformer en vérification automatique : un test, une règle de linter ou un garde-fou grep dans make check.
Comment le E2E modifie le comportement de l'agent avant même l'exécution
La valeur d'un test de bout en bout ne tient pas seulement aux bogues qu'il attrape. Si l'agent sait à l'avance que son travail sera contrôlé par un scénario E2E, il écrit son code différemment : il pense au contrat avec le composant voisin et pas seulement à sa propre fonction, il traite les chemins d'erreur, et il ne franchit pas les limites d'architecture, car une telle infraction apparaîtrait à l'exécution. La vérification agit comme une contrainte à l'entrée, et pas seulement comme un filtre à la sortie.
Une condition s'impose : le E2E a du sens lorsque le système a des frontières claires. Sur une architecture en spaghetti, un test de bout en bout ne prouvera que le fonctionnement des spaghettis, sans montrer où la conception a été violée. C'est pourquoi on établit d'abord les couches et les règles de dépendance, et seulement ensuite les scénarios de bout en bout. Les agents recopient volontiers les motifs existants d'un dépôt, y compris les mauvais : mieux vaut introduire les contraintes dès le premier jour.
Erreurs fréquentes
- Demander à l'agent de « vérifier plus attentivement ». Le contexte reste le même, l'évaluation restera surestimée.
- Considérer 100 % de couverture comme une garantie. La couverture par tests unitaires ne couvre pas les jointures.
- Refactoriser avant la confirmation de la fonctionnalité. L'ordre est strict : justesse → performance → style.
- Simuler en E2E ce qui casse le plus souvent. La base, les migrations et la configuration doivent tourner pour de vrai dans un test de bout en bout ; les API payantes externes peuvent être remplacées par un serveur de test.
Synthèse
Lorsque l'agent a écrit « terminé » et que la fonctionnalité ne marche pas, le problème se situe presque toujours dans le processus de vérification et non dans le modèle. Calculez l'écart de vérification, ancrez la définition of done dans AGENTS.md, rendez le E2E obligatoire, confiez le statut d'achèvement à un script et ajoutez un évaluateur indépendant. Ce schéma est traité en pratique dans le cours sur le harness engineering.
Questions fréquentes
Peut-on faire lancer les tests E2E par l'agent lui-même ?
Oui, si la commande figure dans AGENTS.md comme partie obligatoire de la définition of done et si l'agent dispose des outils nécessaires. Il est plus fiable de la lancer en plus via un hook ou dans la CI, pour ne pas dépendre de la mémoire de l'agent.
Les tests E2E sont lents, faut-il les lancer après chaque modification ?
Non. Lint, types et tests unitaires systématiquement ; E2E lorsque les modifications touchent plus d'une couche, et avant de déclarer la fonctionnalité terminée.
Comment vérifier le frontend réalisé par l'agent ?
Par un test de bout en bout dans un navigateur (par exemple Playwright) qui clique dans la vraie interface, tandis que l'apparence est vérifiée à la main selon une liste de contrôle ou sur captures d'écran examinées par l'évaluateur.
Une revue de code par un humain suffit-elle à la place de l'évaluateur ?
La revue humaine est utile, mais elle est coûteuse et ne passe pas à l'échelle pour des heures de travail autonome. Un évaluateur muni d'une grille élimine les problèmes évidents avant qu'ils n'arrivent à l'humain, qui tranche les questions litigieuses.
Ce sujet est traité en détail avec de la pratique dans le cours « Harness Engineering: инженерия надёжности AI-агентов ».
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента