Troisième article de la série « Coder avec un agent IA, pour de vrai ». Dans le précédent, je montrais le fichier où mon agent de code consigne chaque erreur corrigée. Avec plusieurs dizaines de leçons, des familles apparaissent. Les mêmes erreurs reviennent sous des formes différentes, sur des projets qui n’ont rien à voir.

Ce ne sont pas des erreurs de syntaxe. Le code compile, les tests passent. Ce sont des erreurs de raisonnement, énoncées avec l’aplomb d’un collègue sûr de lui. C’est ce qui les rend coûteuses : on les croit.

Pour chaque famille : un cas réel (anonymisé), le signal qui aurait dû alerter, et la question à poser.

1. Transformer une observation en diagnostic

Je décris un symptôme : « je vois l’écran de démarrage, puis l’application crashe ». L’agent répond : « le crash vient de l’écran de démarrage ».

Je n’ai rien dit de tel. J’ai décrit une chronologie, il en a fait une causalité. Une hypothèse présentée comme une cause, ce sont des heures de tests inutiles pour la confirmer ou l’écarter.

La variante plus subtile : trouver une issue GitHub au symptôme identique, sur les mêmes versions, et la présenter comme l’explication. C’est une corrélation. Pire, si la recherche a été formulée avec la théorie dedans (« X crash avec Y »), elle ne pouvait trouver que ce qui la confirme.

Le signal : le mot « cause » (ou « vient de », « est dû à ») apparaît sans qu’aucun test n’ait isolé la variable.

La question à poser : « Qu’est-ce qui prouve le lien, en dehors du fait que les deux arrivent ensemble ? » Et pour les recherches : partir d’une requête large et neutre, pas de sa propre hypothèse.

2. Prendre une absence pour une preuve

« J’ai cherché, il n’existe aucune solution documentée. » « Le fichier est bien présent. » « La page ne mentionne pas ce cas. »

Trois affirmations, trois pièges :

  • l’outil de lecture web de l’agent résume la page au lieu de la restituer, et ne voit pas le contenu chargé en JavaScript. « Pas trouvé » signifie « pas dans ce que j’ai reçu » ;
  • [ -e fichier ] teste qu’une entrée existe, pas qu’il s’agit d’un fichier régulier : un dossier répond aussi « oui » ;
  • sur GitHub, le corps d’une issue ne contient souvent pas la solution, qui se trouve dans les commentaires que l’outil n’a pas lus.
# Exemple simplifié
# Ce que l'agent a testé
[ -e config/app.conf ] && echo "présent"

# Ce qu'il voulait savoir
[ -f config/app.conf ] && echo "fichier régulier présent"

Le signal : une conclusion négative (« aucun », « n’existe pas », « rien ») tirée d’un seul outil.

La question à poser : « Qu’est-ce que cet outil mesure réellement ? » Une négation se confirme par une source qui expose la donnée brute : une API, un JSON, le fichier lui-même. Le prochain article de la série sera entièrement consacré à ce biais des outils de lecture web.

3. Les chiffres qui mentent

Un lot qui échoue n’est pas un lot vide

Un script vérifiait l’existence de fiches via une API publique qui accepte plusieurs identifiants par requête. Résultat annoncé : 33,3 % de fiches disparues.

Le taux réel, mesuré en retestant un par un : 0,3 %. Un facteur 100.

L’API renvoyait une erreur 404 pour le lot entier dès qu’un seul identifiant manquait. Le script comptait les 100 éléments du lot comme absents.

Le signal : un chiffre rond suspect (100 sur 300) ou un ordre de grandeur très éloigné de ce qu’on attendait.

La base locale n’est pas la production

Question : un problème concerne-t-il beaucoup d’établissements clients ? L’agent compte les lignes de la table en local, en trouve une vingtaine et conclut : « seulement une vingtaine d’établissements réellement actifs, le problème est théorique ».

Ces lignes étaient mes propres tests et les données de démonstration. L’activité utilisateur d’une base locale ne dit rien de la production. Le volume de données de référence (villes, catégories) peut être comparable entre local et production. L’usage, jamais.

Ce faux critère orientait toute la décision vers « ne rien faire ».

La question à poser : « D’où vient ce chiffre, et qu’est-ce qu’il mesure vraiment ? » Sans accès à la production, la bonne réponse est « je ne peux pas mesurer l’usage », pas un indicateur de remplacement.

4. Le bug qui ne peut pas arriver

Un agent qui analyse du code trouve facilement des incohérences. Toutes ne sont pas des bugs.

Cas réel : deux constantes ne coïncident pas. Une fenêtre côté serveur se ferme à 13h45, alors que la plage déclarée côté métier va jusqu’à 15h. Conclusion de l’agent : « un créneau à 14h ne sera jamais listé ». Plan de correction complet rédigé.

Sauf que l’interface de création ne propose aucun créneau après 13h30. Aucun utilisateur ne peut produire la donnée qui tombe dans l’écart. Le plan est parti à la poubelle.

Variante : affirmer qu’une route filtre les données selon l’heure courante, parce que le handler retombe sur new Date() quand le paramètre time est absent. Le client, lui, envoie toujours ce paramètre. L’agent a raisonné sur une branche de code que personne n’emprunte.

// Exemple simplifié
// Ce que l'agent a lu
const time = query.time ?? new Date();

// Ce qu'il n'a pas lu : l'appelant
fetch(`/api/slots?time=${slot.canonicalTime}`);

Le signal : un bug identifié uniquement en lisant le code, sans scénario utilisateur reproduit.

La question à poser : « Quelle action d’un utilisateur produit cette donnée ? » Remonter la chaîne de saisie, et lire l’appelant avant de conclure sur une route.

Ce cas avait un contexte aggravant : c’était la troisième piste de la session, les deux premières étaient déjà tombées. La pression de « trouver quelque chose » pousse à valider trop vite le candidat suivant. Quand plusieurs pistes tombent d’affilée, la suivante mérite plus de vérification, pas moins.

5. Fabriquer, puis faire arbitrer

Cette famille est la plus insidieuse, parce que c’est moi qui valide l’erreur.

Cas réel : un générateur de mots filtre des termes sensibles. L’agent annonce « 36 violations » dans le dictionnaire et me demande d’arbitrer, terme par terme. Vérification faite : plusieurs termes de la liste n’avaient jamais figuré dans le dictionnaire. C’était sa propre règle de filtrage qui les aurait bloqués. D’autres étaient des faux positifs évidents (« angle mort », « herbe morte »). J’étais en train de corriger son échafaudage en croyant corriger mes données.

Même mécanisme en plus discret : dans les options d’un choix proposé, l’agent glisse un élément jamais demandé. Pour un script de vidéo tutoriel, un « Abonnez-vous » s’est retrouvé dans une option. Je l’ai validée pour autre chose, et la phrase s’est propagée dans le script, les sous-titres et la note de montage. Il n’avait jamais été question d’abonnement.

Le signal : on vous demande de trancher sur une liste ou une option que l’agent a construite.

La question à poser : « Ces éléments existent-ils réellement dans mes données, ou viennent-ils de ta règle ? » Un comptage (rg, grep -c) avant l’arbitrage, pas après.

6. Prudence mal placée

Deux erreurs opposées, qui viennent du même réflexe : ne pas poser une question à une ligne.

Le livrable « au cas où ». Un doute sur l’infrastructure (le site est-il derrière Cloudflare ?) que je pouvais lever en une phrase. L’agent a préféré une configuration nginx « qui marche dans les deux cas » : 24 lignes set_real_ip_from pour les plages IP de Cloudflare. Le site n’a jamais été derrière Cloudflare. Résultat : de la configuration morte, des plages à maintenir, et une phrase fausse dans le README.

L’excès d’avertissements. Une correction chiffrée à 6 lignes, puis un paragraphe de risques théoriques et un « pas maintenant ». Le risque invoqué était déjà neutralisé par un garde-fou existant.

La règle : un fait que quelqu’un connaît se demande. Le livrable « valable dans tous les cas » est réservé à ce que personne ne peut savoir à l’avance, comme la version d’un outil sur un serveur distant (c’était le correctif du premier article).

7. Raisonner sur un contexte périmé

L’agent reçoit au démarrage un instantané de l’état du dépôt : branche, fichiers modifiés. Cet instantané n’est jamais rafraîchi.

Cas réel : sur plusieurs échanges, l’agent a répété que des modifications n’étaient pas commitées. Elles avaient été mergées sur main via une pull request, en parallèle. Il a failli enregistrer dans sa mémoire un état de release faux sur cette base.

Variante : proposer comme solution une source de données que nous venions d’écarter, trois messages plus tôt, dans la même conversation. Une piste tombée ne redevient pas viable parce qu’elle revient sous un autre angle.

Le signal : une affirmation d’état (« non commité », « sur la branche X ») sans commande exécutée juste avant.

La question à poser : « Tu l’as vérifié maintenant, ou c’est ce que tu savais au début ? »

git status --short
git rev-parse --abbrev-ref HEAD

8. « Le test échoue sans le correctif »

L’agent corrige un bug, écrit un test, montre que le test échoue sans le correctif et passe avec. Démonstration convaincante.

Elle prouve seulement que le test n’est pas une tautologie. Elle ne prouve pas que le bug est corrigé.

Cas réel : un import de données pouvait écrire un fichier tronqué en cas de panne en cours d’import. Les tests prouvaient qu’une exception était bien levée. La vraie question était : un fichier tronqué peut-il encore finir sur le disque ? La réponse est venue en rejouant l’import avec une panne simulée : 511 enregistrements écrits avant le correctif, aucun après.

Le signal : la preuve porte sur un mécanisme interne (une exception, un appel de mock), pas sur l’effet observable.

La question à poser : « Qu’est-ce qui finit sur le disque, en base ou à l’écran, avant et après ? »

L’aide-mémoire

Ce que dit l’agent Ce qu’il faut demander
« La cause est X » Qu’est-ce qui prouve le lien, hors simultanéité ?
« Aucune solution / ça n’existe pas » Qu’est-ce que l’outil a réellement lu ?
« 33 % des éléments sont concernés » Comment est compté un échec ? Le chiffre est-il plausible ?
« Peu d’utilisateurs sont concernés » Ce chiffre vient-il de la production ?
« Ce cas n’est jamais géré » Quelle action utilisateur produit ce cas ?
« Voici N problèmes, lesquels garder ? » Existent-ils dans mes données ou dans ta règle ?
« J’ai prévu les deux cas » Pourquoi ne pas avoir posé la question ?
« Ce n’est pas commité » Vérifié maintenant ?
« Le test échoue sans le correctif » Qu’est-ce qui change concrètement, avant et après ?

Ce que ces erreurs ont en commun

Aucune ne vient d’un manque de connaissances. L’agent sait ce qu’est une corrélation, qu’une API peut échouer par lot, qu’une base locale n’est pas la production. Il ne s’arrête pas pour vérifier au moment où il le faudrait. Il prend le chemin le plus court vers une réponse plausible.

Ce sont aussi, honnêtement, des erreurs de développeur pressé. La différence, c’est le rythme : un agent les produit en quelques secondes, avec une rédaction impeccable, et en enchaîne trois avant qu’on ait fini de lire la première.

D’où l’intérêt du fichier de leçons : chacune de ces familles y est inscrite avec son cas concret. Il ne les empêche pas toutes. Mais quand l’une revient, je la reconnais plus vite.