Le paradoxe de la correction de bogues : pourquoi les agents d'IA continuent de briser du code fonctionnel
Voici un scénario que la plupart des équipes rencontrent : vous demandez à un agent d'IA de corriger un bogue. Il refactorise trois fonctions auxiliaires, ajoute des vérifications de nullité défensives et rédige des dizaines de nouveaux tests pour des cas limites qui fonctionnaient déjà. Pire encore, il modifie des parties de l'application qui fonctionnaient parfaitement bien. Vous vouliez un scalpel, mais vous avez obtenu un marteau-piqueur.
Les agents sont presque deux fois plus susceptibles que les humains d'ajouter des clauses de garde et une gestion défensive des erreurs. Là où nous demanderions « pourquoi est-ce nul? », l'agent ajoute if (x == null) et passe à autre chose. L'itération aggrave la situation : sans contraintes appropriées, plus vous discutez avec un agent, plus il dérive de l'intention originale. La correction réelle, si elle est découverte, est enfouie sous des changements qui n'étaient pas nécessaires.
Le problème, c'est que vous et l'agent ne travaillez pas avec la même frontière entre ce qui doit être corrigé et ce qui doit rester intact. Nous avons conçu le flux de travail de correction de bogues de Kiro pour rendre cette frontière explicite. Cette approche est basée sur une méthode que nous appelons l'évolution du code sensible aux propriétés.
Chaque correction de bogue a une double intention : corriger le comportement défectueux et préserver tout le reste. Cette intention divise l'espace des entrées, mais la partition reste habituellement implicite. Nous pouvons la rendre explicite et testable.
La condition du bogue C identifie quand le bogue se déclenche. Elle divise l'espace des entrées en deux :
- Scénarios satisfaisant C → où le bogue se manifeste. Vous voulez que le changement se produise ici.
- Scénarios ne satisfaisant pas C → où le comportement est correct. Vous voulez la préservation ici.

Par exemple, si la suppression d'un nœud d'un arbre binaire de recherche (BST) provoque un plantage lorsque l'enfant droit n'a pas de sous-arbre gauche, C est : le nœud a deux enfants ET node.right.left est None. Tous les autres scénarios de suppression se trouvent hors de C et ne devraient pas être touchés.
Chaque ingénieur expérimenté raisonne à propos de C, souvent de façon implicite. Mais sans C comme artefact explicite et partagé, rien ne garantit que la frontière de l'agent correspond à la vôtre. Lorsque C reste implicite, trois choses peuvent mal se passer :
- L'agent dérive de la frontière. Même lorsque le rapport de bogue est précis, l'agent n'a aucun enregistrement persistant de la frontière. À chaque étape, il réinterprète cette frontière à partir de zéro et, sur plusieurs étapes, ces interprétations dérivent de l'intention originale.
- L'agent invente une frontière. Lorsque le rapport de bogue est vague, l'agent remplit les lacunes avec ses meilleures suppositions, comme le ferait n'importe quel ingénieur. La différence, c'est que l'agent ne les montre pas explicitement. Au moment où vous constatez l'écart lors de la révision de code, le correctif est déjà construit autour de celui-ci.
- L'agent ne peut pas vérifier qu'il a respecté la frontière. Sans un C explicite, il n'existe aucune façon systématique de vérifier si tout le reste fonctionne encore. L'agent peut vérifier sa correction, mais il ne peut pas vérifier s'il est resté à l'intérieur de la frontière.
C délimite donc la frontière. Mais seul, ce n'est pas suffisant. C nous indique quand le bogue se déclenche, mais pas ce que « corrigé » signifie. La postcondition P remplit cette lacune : elle définit ce que le code devrait faire pour les entrées où C est vérifiée, c'est-à-dire ce qui devrait se produire pour les entrées défectueuses. Pour une suppression BST qui plante, P est : l'opération de suppression ne plante pas, retire le nœud et préserve l'invariant du BST.
Sans P, l'agent peut supprimer l'erreur avec un try/except et l'appeler « corrigé ». P le force à s'aligner sur ce que « correct » signifie.
Avec l'évolution du code sensible aux propriétés, nous définissons les propriétés avant d'écrire le code. Une propriété est une affirmation testable : pour toutes les entrées satisfaisant une certaine condition, une certaine garantie tient. Nous utilisons la condition du bogue C et la postcondition P pour définir deux propriétés :
- Propriété de correction (C ⟹ P) : Lorsque C est vérifiée, le code corrigé satisfait P.
- Exemple : La propriété de correction affirme « delete satisfait P sur les arbres où le nœud a deux enfants et node.right.left est None. » Nous pouvons vérifier cela en exécutant delete sur de tels arbres. Si l'un plante, la propriété échoue.
- Propriété de préservation (non C ⟹ inchangé) : Lorsque C n'est pas vérifiée, le code corrigé se comporte de façon identique au code original.
- Exemple : La propriété de préservation affirme « delete se comporte de façon identique sur tous les autres arbres. » Vérifiez-le en exécutant delete sur des arbres hors de C avant et après la correction. Si le comportement change, la propriété échoue.
Ensemble, ces deux propriétés couvrent l'ensemble de l'espace des entrées et contraignent la façon dont l'agent rédige la correction. Tout correctif doit réussir la propriété de correction sans briser la propriété de préservation. Nous appelons cette méthodologie l'évolution du code sensible aux propriétés.
Le flux de travail de correction de bogues de Kiro utilise cette méthodologie en coulisses. Kiro propose la condition du bogue, la postcondition, ainsi que les propriétés de correction et de préservation. Vous les raffinez ensemble, et le spec, les tests et la correction que Kiro génère découlent tous de ces propriétés.
Voici un rapport de bogue concret montrant un bogue classique de structures de données :
Vous collez ceci dans Kiro et optez pour le flux de travail de correction de bogues. Kiro ne se précipite pas vers un correctif. Il partitionne les scénarios défectueux et non défectueux, formule une hypothèse de cause profonde et teste cette hypothèse, avant d'écrire une seule ligne de code.
Kiro analyse le rapport de bogue et génère un document de correction de bogue avec trois catégories d'exigences : le comportement défectueux actuel, la correction attendue et le comportement inchangé qui doit être préservé.

Cela reflète la partition définie par la condition du bogue C. Les exigences de défaut et de correction ciblent les entrées défectueuses. Les exigences de préservation identifient des comportements spécifiques qui ne doivent pas changer.
Le document de correction de bogue divise les scénarios en langage naturel. Kiro le formalise maintenant et enquête sur la raison pour laquelle le bogue existe.
Formalisation de la partition. Kiro extrait la condition du bogue C des exigences de défaut et de correction :
Kiro formalise également ce que « corrigé » signifie sous forme de postcondition P :
Traçage de la cause profonde. Une fois C et P établies, Kiro lit la base de code pour construire une hypothèse de cause profonde : pourquoi les entrées satisfaisant C plantent-elles au lieu de satisfaire P? Il trace le flux d'exécution pour une entrée où C est vérifiée :
Pour une entrée où C est vérifiée, disons la suppression de 5 dans [5, 3, 7], la trace évalue :
L'hypothèse : _find_min reçoit node.right.left au lieu de node.right. Lorsque C est vérifiée, node.right.left est None par définition, donc l'appel plante toujours.
Le point de contrôle. Avant d'écrire du code ou des tests, Kiro présente C, P et l'hypothèse pour votre révision. Rien n'a encore été généré. Si C est trop étroite, trop large ou cible le mauvais scénario, vous avez la chance de la contester et de la raffiner. Si l'hypothèse est erronée, la phase suivante le détecte : les tests pour la propriété de correction devraient échouer sur le code non corrigé avec une AttributeError. S'ils échouent pour une raison différente, ou ne échouent pas du tout, l'hypothèse est réfutée et Kiro réanalyse avant d'écrire une correction.
L'hypothèse de cause profonde fait de la phase de conception plus qu'une simple documentation. C'est une prédiction falsifiable. Toute la stratégie de test qui suit est conçue pour la confirmer ou la réfuter.
Kiro a maintenant une hypothèse : les entrées satisfaisant C plantent parce que _find_min reçoit None. Le plan de tâches la teste avant d'écrire une correction.

Kiro exécute chaque test sur le code non corrigé d'abord, applique la correction, puis retest. Il structure le plan en trois tâches :
Tâche 1. Kiro rédige des tests de condition du bogue pour des entrées à l'intérieur de C et encode le comportement attendu (P). Kiro les exécute sur le code non corrigé. Ils échouent. Cela confirme que le bogue existe exactement là où C le prédit.
Tâche 2. Kiro exécute le code non corrigé sur des entrées hors de C, enregistre le comportement réel et rédige des tests de préservation affirmant ce comportement. Chaque test devrait réussir sur le code non corrigé.
Tâche 3. Kiro corrige le code selon l'hypothèse de cause profonde et réexécute les tests de condition du bogue et de préservation. Le test de condition du bogue qui échouait réussit maintenant — la correction fonctionne. Les tests de préservation réussissent toujours parce que rien d'autre n'aurait dû changer. Si, au contraire, le test de condition du bogue échoue, alors l'hypothèse était erronée et Kiro le signale et réenquête avant d'essayer une autre correction. Si un test de préservation bascule, la correction a des effets secondaires et Kiro restreint la portée du correctif. Dans les deux cas, le résultat est exploitable.
C'est le cycle rouge-vert du développement piloté par les tests combiné avec le test différentiel. Les tests de condition du bogue sont rouges avant la correction, verts après. Les tests de préservation enregistrent le comportement du code non corrigé et affirment que le code corrigé se comporte de la même façon sur les mêmes entrées; le code non corrigé agit comme la spec.
Les deux suites de tests utilisent des tests basés sur les propriétés via Hypothesis. Plutôt que d'écrire des tests pour des arbres spécifiques, Kiro déclare des propriétés de correction et de préservation et utilise Hypothesis pour générer des centaines d'arbres aléatoires pour les vérifier (pour un traitement plus approfondi des tests basés sur les propriétés dans Kiro, voir Votre code correspond-il à votre spec?)
Pourquoi des tests basés sur les propriétés? La condition du bogue dépend de la structure de l'arbre, spécifiquement si node.right.left existe. Cette structure varie de façon combinatoire. Les tests unitaires nécessiteraient de construire manuellement des dizaines d'arbres pour la couvrir. Les tests basés sur les propriétés explorent cet espace automatiquement, générant des centaines d'arbres qui couvrent des combinaisons structurelles qu'une suite écrite à la main ne couvrirait probablement pas.
Les tests de condition du bogue vérifient la propriété de correction :
Ce test encode la propriété de correction : pour tous les arbres où C est vérifiée, delete devrait réussir, retirer le nœud et préserver l'invariant du BST. Sur le code non corrigé, celui-ci échoue avec une AttributeError parce que _find_min(None) tente None.left.
Les tests de préservation vérifient la propriété de préservation :
Ce test encode la propriété de préservation : pour tous les arbres où C n'est pas vérifiée — suppressions de feuilles, suppressions à un enfant, suppressions à deux enfants où node.right.left existe — le comportement est inchangé. Sur le code non corrigé, il réussit. C'est la référence. Après la correction, ce test doit toujours réussir.
Maintenant que les tests de condition du bogue ont confirmé l'hypothèse et que les tests de préservation ont capturé la référence, Kiro rédige une correction d'une seule ligne :
Kiro réexécute les deux suites de tests. Les tests de condition du bogue réussissent maintenant, validant que la correction fonctionne. Les tests de préservation réussissent toujours, validant que rien d'autre n'a changé.
Kiro fonctionne de la même façon sur des bases de code plus grandes. Voici un exemple réel : une fuite de mémoire dans le HeartbeatSyncer d'Apache RocketMQ (le PR original, SWE-PolyBench). HeartbeatSyncer suit les consommateurs connectés dans une carte concurrente. Des entrées sont ajoutées lors de l'enregistrement et retirées lors du désenregistrement. Mais le retrait ne réussit jamais. La carte croît sans limite.
Pour identifier et corriger le bogue, Kiro suit le même flux de travail. L'hypothèse de cause profonde est une incompatibilité de clé :
La clé d'insertion préfixe le groupe de consommateurs, mais la clé de retrait ne le fait pas. Elles ne correspondent jamais. Chaque désenregistrement est une opération sans effet. La condition du bogue C est tout événement CLIENT_UNREGISTER valide où args est non nul et contient un ClientChannelInfo. Chaque événement de désenregistrement provoque une fuite de mémoire.
La correction est toujours d'une seule ligne, mais valider la préservation est plus difficile. Il existe cinq chemins de code distincts à travers le même écouteur : args nul, args non-ClientChannelInfo et enregistrements multi-groupes parmi eux. La logique d'insertion et les autres types d'événements doivent également rester intacts. Chaque scénario hors de C obtient son propre test de préservation, rédigé et réussi avant que Kiro n'applique la correction.
Avec l'évolution du code sensible aux propriétés, vous et Kiro travaillez à partir du même contrat. Kiro rédige la frontière et l'hypothèse. Vous pouvez la contester, la redessiner, restreindre la portée ou demander une approche différente. Au moment où le code est écrit, vous vous êtes entendus tous les deux sur ce qui change et ce qui ne change pas.
Lorsque le contrat s'effondre, le flux de travail le rend évident. Si le rapport de bogue est trop vague pour dériver C, Kiro le signale avant qu'aucun code ne soit écrit. Lorsque l'hypothèse de cause profonde est erronée, le test de condition du bogue le détecte. Lorsque la correction a des effets secondaires, les tests de préservation les détectent. Chaque échec vous indique quoi faire ensuite.
L'évolution du code sensible aux propriétés est la plus efficace pour les changements où le comportement attendu peut être exprimé comme des propriétés fonctionnelles et testables : erreurs logiques, cas limites, exceptions à l'exécution, bogues de traitement des données. Pour les préoccupations non fonctionnelles comme la performance ou les conditions de concurrence, exprimer des propriétés est plus difficile parce qu'elles sont souvent non déterministes et nécessitent un raisonnement temporel. Trouver la meilleure façon de les exprimer comme des affirmations testables demeure une question ouverte.
La correction de bogues n'est qu'une application de l'évolution du code sensible aux propriétés. Les ajouts de fonctionnalités et le refactoring ont la même double intention : changer le comportement à l'intérieur d'une frontière, préserver tout le reste. Cette frontière peut être appliquée avec des propriétés testables. L'application de l'évolution du code sensible aux propriétés au-delà de la correction de bogues est un domaine actif de notre recherche.
Pour l'instant, souvenez-vous : la prochaine fois que vous signalez un bogue, vous dessinez une frontière. Dessinez-la avec Kiro, et les propriétés maintiendront la correction du bon côté.
- State of AI vs. Human Code Generation Report
- Drift No More? Context Equilibria in Multi-Turn LLM Interactions
- Votre code correspond-il à votre spec?
- The Hypothesis Property-Based Testing Library
- QuickCheck
- SWE-PolyBench
- Differential testing
Merci à Aaron Eline, Anjali Joshi et Margarida Ferreira pour leurs efforts sur la méthodologie d'évolution du code sensible aux propriétés et sur le flux de travail de correction de bogues de Kiro.