Une expérience de loop engineering montre que les boucles de rétroaction et les vérificateurs peuvent échouer silencieusement
Points clés
- •L’expérience a évalué deux composants du loop engineering : une boucle de rétroaction utilisant de vrais échecs de tests et une configuration maker/checker séparant la génération de code du jugement final.
- •Une première exécution de test n’a montré aucun avantage de la rétroaction réelle, car le harnais de tests cachés renvoyait de simples erreurs d’assertion sans détails utiles sur les échecs.
- •Après l’ajout au harnais des entrées en échec, des sorties attendues et des résultats réels, la rétroaction réelle a résolu un problème qu’une boucle de nouvelle tentative générique n’avait pas résolu.
- •Le vérificateur exécutant des tests a accepté à tort 3 candidats incorrects sur 8, soit un taux supérieur à celui de deux vérificateurs fondés sur une opinion dans la comparaison mesurée.
- •Le système combiné final a amélioré les résultats principalement grâce à la boucle de rétroaction, tandis que le vérificateur a produit une fausse acceptation qui n’a été détectée que par la notation avec tests cachés.

Dernière mise à jour le 27 juillet 2026 par l’équipe éditoriale. Initialement publié sur Towards AI.
« My job is to write loops. »
Cette déclaration vient de Boris Cherny, qui dirige Claude Code chez Anthropic. Cherny a indiqué qu’il avait cessé de prompter Claude directement et qu’il consacre désormais son temps à concevoir les boucles qui le promptent à sa place [1]. Cette remarque, ainsi que plusieurs commentaires similaires, a contribué à déclencher cette année une vague d’articles explicatifs sur le loop engineering [1][2]. Après en avoir lu six, j’en ai construit un.
Plus précisément, j’ai construit les deux composants que presque tous les articles explicatifs décrivent, mais que peu semblent exécuter de bout en bout. Le premier était une boucle run-until-done : au lieu de demander au modèle de refaire une supposition après un échec, elle lui fournit ses propres échecs de tests réels. Le second était une configuration maker/checker : le modèle qui écrit le code n’est pas autorisé à être le juge final de la validité de ce code.
Cette distinction est importante, car les workflows d’agents de codage reposent souvent sur la même séparation : un système propose une modification, tandis qu’un autre outil, une suite de tests ou un modèle décide si cette modification est acceptable. Si le canal de rétroaction ou le vérificateur est faible, la boucle peut sembler automatisée sans devenir réellement plus sûre.
J’ai implémenté les deux à partir de zéro en environ 600 lignes de Python, les ai connectés à claude-opus-4-8 et les ai évalués avec MBPP+ [3]. MBPP+ est un benchmark EvalPlus composé de petites tâches de programmation Python, ce qui le rendait utile pour isoler le comportement de la boucle sans introduire les complications d’un dépôt logiciel à grande échelle. Le coût total de toutes les expériences abordées ici a été inférieur à deux dollars. Le second composant — celui qui est souvent traité comme la partie la plus sûre parce qu’il « exécute réellement des tests » au lieu de se fier à la parole du modèle — a produit, dans mes mesures, des résultats dont ces articles explicatifs ne m’avaient pas averti.
La version courte : les deux éléments centraux du loop engineering sont simples à connecter et faciles à mal configurer sans que cela se voie. Ma boucle de « rétroaction réelle » a d’abord semblé impossible à distinguer de tentatives aléatoires, jusqu’à ce que je trouve un bug dans mon propre harnais de test. Mon vérificateur « sûr » exécutant des tests affichait un taux de fausses acceptations plus élevé qu’un vérificateur qui demandait simplement au modèle à quel point il se sentait confiant. Construire la boucle est la partie facile, soit les premiers 20 %.
Connecter une boucle à rien
Une grande partie des textes sur le loop engineering s’arrête au schéma de câblage. Ils énumèrent un déclencheur, un objectif vérifiable, des outils, un état et des règles d’arrêt — cinq cases avec des flèches entre elles. Le message implicite est qu’une fois les cases connectées, la boucle fonctionne.
C’est comme installer un détecteur de fumée et déclarer la maison sûre parce que le détecteur est fixé au plafond et câblé, sans vérifier s’il contient une pile fonctionnelle.
J’ai rencontré exactement cet échec avec ma boucle de « rétroaction réelle ». Elle était connectée à la sortie effective des tests, et non à un prompt générique de nouvelle tentative. En théorie, elle aurait dû nettement surpasser une boucle qui ne recevait que le message : « c’était faux, réessaie ». Ma première exécution a montré le contraire.
Le correcteur qui corrige le correcteur
Avant de toucher à la boucle elle-même, j’ai construit le composant dont tout le reste dépendait : un système de notation qui exécute le code candidat dans un sous-processus isolé avec un délai d’expiration strict et l’évalue avec des tests cachés.
Je ne lui ai pas fait confiance tant qu’il ne s’était pas évalué lui-même. Lorsqu’on lui fournissait une solution connue comme correcte, elle devait passer. Lorsqu’on lui fournissait une solution connue comme incorrecte, elle devait échouer et joindre l’erreur d’assertion. Lorsqu’on lui fournissait une boucle infinie, elle devait être arrêtée par le délai d’expiration au lieu de rester bloquée indéfiniment.
J’ai ensuite validé l’ensemble du pipeline avec 75 solutions de référence MBPP+. Les 75 ont réussi. Ce n’est qu’après cela que j’ai fait confiance aux chiffres produits par la boucle.
La boucle qui semblait correcte mais ne l’était pas
La boucle elle-même était très simple. Elle générait une solution, l’évaluait et, en cas d’échec, renvoyait le vrai stderr — non pas un « réessaie » générique, mais l’erreur réelle — pendant au maximum trois tentatives.
J’ai également construit un groupe témoin, car je ne voulais pas me fier à un chiffre principal sans point de comparaison. Le témoin utilisait la même boucle, mais remplaçait l’erreur réelle par une instruction générique : « c’était faux, écris une solution différente ». Si la rétroaction réelle ne faisait pas clairement mieux que cela, alors quelque chose dans le câblage était cassé.
Lors de la première exécution sur 35 problèmes, les trois branches étaient identiques. Ce n’était pas la preuve d’une boucle fonctionnelle. C’était un signal d’alerte ayant la forme d’une boucle.
Au lieu de me concentrer sur la métrique globale, j’ai examiné les échecs et trouvé le problème : le harnais de tests cachés de MBPP+ échouait avec une simple AssertionError. Il n’incluait ni l’entrée ayant échoué, ni la valeur attendue, ni la valeur réelle. Par conséquent, la « rétroaction réelle » était informationnellement identique à « réessaie », car le modèle ne recevait rien sur quoi il pouvait agir.
J’ai instrumenté le harnais afin qu’il signale l’entrée en échec, la sortie attendue et la valeur effectivement renvoyée par le code. Sur les mêmes 35 problèmes lors d’une deuxième exécution, la rétroaction réelle a permis de résoudre un problème que la branche générique ne pouvait pas résoudre, au prix d’environ 2 500 tokens d’entrée supplémentaires sur l’ensemble de l’exécution.
La boucle n’était pas cassée. Le signal auquel elle était connectée était vide. Seule la branche témoin l’a révélé ; la métrique principale seule ne l’aurait pas fait.
Le vérificateur a échoué d’une manière que la théorie ne prévoyait pas
Une boucle a besoin d’une règle d’arrêt, et « le modèle dit que c’est terminé » n’en est pas une. J’ai donc construit un vérificateur qui écrit ses propres tests à partir de la spécification. Il ne voit jamais les tests cachés et ne voit jamais le code de la solution qu’il juge. Il exécute ensuite ces tests. La règle consistait à accepter uniquement en cas de réussite complète et à rejeter par défaut.
J’ai comparé ce vérificateur à trois vérificateurs plus faibles sur 41 candidats produits par ma boucle : 33 corrects et 8 incorrects. La mesure portait sur le taux de fausses acceptations, c’est-à-dire la fréquence à laquelle chaque vérificateur laissait passer du code alors qu’il était en réalité défectueux.
| Vérificateur | Fausse acceptation | Faux rejet |
|---|---|---|
| Tout accepter | 8/8 — 100% | 0/33 — 0% |
| Demander au modèle s’il est confiant | 2/8 — 25% | 4/33 — 12% |
| Un second modèle lit le code | 2/8 — 25% | 5/33 — 15% |
| Écrit des tests et les exécute | 3/8 — 38% | 1/33 — 3% |
Je m’attendais à ce que le vérificateur exécutant des tests l’emporte nettement sur les fausses acceptations. Ce ne fut pas le cas. Il a laissé passer une proportion plus élevée de code incorrect que chacun des vérificateurs fondés sur une opinion.
La raison importait davantage que le chiffre. Les huit candidats incorrects provenaient de trois problèmes aux spécifications réellement ambiguës. Le vérificateur et le correcteur étaient le même modèle lisant la même phrase ambiguë. Les tests autoécrits par le vérificateur encodaient donc la même mauvaise interprétation que celle déjà présente dans le code incorrect, ce qui permettait à ce code de réussir ces tests sans difficulté.
Les vérificateurs fondés sur une opinion ont « gagné » cette comparaison sur les fausses acceptations surtout parce qu’ils étaient généralement hésitants. Cette même hésitation explique aussi pourquoi ils ont rejeté à tort quatre à cinq fois plus de code correct.
Exécuter des tests n’est pas une protection automatique contre les fausses acceptations. C’est un autre type de preuve : une preuve qui comprend une entrée précise, une valeur attendue et une valeur réelle, plutôt qu’une impression générale. C’est ce qui rend son taux de faux rejets de 3 % exploitable comme véritable garde-fou. Un vérificateur qui rejette 15 % du bon travail peut noyer un workflow sous les nouvelles tentatives avant même d’empêcher une mauvaise fusion.
Combiner les deux éléments
La composition finale exécute d’abord la boucle de rétroaction. Si elle échoue, elle échantillonne de nouveaux candidats et laisse le vérificateur — et non la confiance du modèle — décider de ce qui est soumis. Elle inclut également une voie explicite d’abandon si rien ne franchit le seuil.
J’ai évalué cette configuration sur une tranche réservée de MBPP+ que je n’avais pas utilisée lors de la construction des composants précédents. La notation a été effectuée avec les tests cachés, indépendamment de ce que le vérificateur avait décidé.
La boucle a fait pratiquement tout le travail : elle a ajouté 14,2 points, récupéré cinq des six échecs en un seul essai et nécessité environ 10 appels d’API supplémentaires. L’étape de vérification s’est déclenchée exactement une fois, sur le problème que la boucle ne parvenait pas à résoudre. Son premier candidat échantillonné a réussi ses propres tests autoécrits et a tout de même échoué aux tests cachés.
Il s’agissait d’une fausse acceptation en conditions réelles, correspondant au mode de défaillance prédit par le tableau précédent. Elle n’a été détectée que parce que l’exécuteur notait les soumissions avec une source de vérité que le vérificateur ne voit jamais. Si le propre jugement du vérificateur avait été final, ce bug aurait été livré.
Le coût total de cette étape a été de 45 appels, soit environ treize cents.
Là où l’approche se casse
Cette approche fonctionne lorsque l’objectif est réellement testable : une fonction avec des cas de test cachés, un schéma qui valide ou ne valide pas, ou un oracle que la boucle ne peut pas contourner par la conversation. Elle ne fonctionne pas lorsque la spécification elle-même est ambiguë, car un vérificateur reposant sur le même modèle peut hériter de la même mauvaise interprétation que le générateur. Dans ce cas, la solution n’est pas nécessairement un vérificateur plus ingénieux. C’est une spécification plus claire, ou un oracle indépendant issu d’une famille de modèles entièrement différente.
Je n’ai également testé cela que sur de petites fonctions MBPP+ autonomes. L’expérience n’a pas traité le cas d’une grande base de code avec des dépendances entre fichiers. Je n’ai pas construit ni testé d’isolation de worktree pour exécuter plusieurs boucles en parallèle. C’est un vrai problème, mais ce n’est pas celui que cette expérience mesurait.
La boucle s’arrête aussi à « vérifié ». Elle ne décide pas s’il faut appliquer automatiquement une modification ou l’escalader vers un humain. Cela devient un problème distinct et plus difficile dès que le système touche quoi que ce soit disposant d’un véritable accès en écriture.
Par où commencer
Commencez par le correcteur. Écrivez l’autotest — solution connue comme correcte, solution connue comme incorrecte et boucle infinie — avant d’écrire la moindre logique de boucle. Ce script de cinq minutes est la principale protection contre un chiffre principal fictif.
Pour une base de code disposant même d’un petit ensemble de tests unitaires, connectez ensuite la boucle de rétroaction et construisez dès le premier jour la branche témoin de nouvelle tentative générique en parallèle. Ne faites pas confiance à l’amélioration tant qu’elle ne bat pas « réessaie ».
Construisez ensuite un second vérificateur et comparez-le au premier. Lorsque le tableau des fausses acceptations contredit ce que la théorie suggérait — et cela peut arriver — la raison montrera pourquoi la boucle compte davantage que le modèle qui la sous-tend.
Références
[1] Rohan Mistry, “Prompt Engineering Is Dead. Loop Engineering Is Here.,” Towards AI, juillet 2026.
[2] Mehmet Özel, “Loop Engineering for AI Agents : Building Verifiable, Self-Correcting Coding Workflows,” Towards AI, juin 2026.
[3] EvalPlus, “MBPP+ Dataset,” Hugging Face Datasets.