Pourquoi les coding agents échouent : erreurs communes et comment les limiter

1. Vue d’ensemble

Un coding agent est un outil IA capable de lire lui-même une base de code, modifier des fichiers, exécuter les commandes de build/test, puis itérer jusqu’à ce que le « travail soit fait ». Cela accélère clairement la productivité, mais crée aussi des erreurs plus difficiles à détecter que celles faites par un humain.

Cet article classe les types d’erreurs courants des coding agents, avec des exemples concrets en Java, puis présente des techniques pour les limiter : écrire des tests avant, fournir un fichier de conventions à l’agent, mettre en place des portes de contrôle automatiques et un processus de review. À la fin, vous saurez sur quels points être vigilant quand vous examininez un diff produit par un agent, et comment structurer votre environnement pour que l’agent se trompe moins souvent.

2. Pourquoi les coding agents se trompent

Il faut d’abord comprendre le mécanisme avant d’entrer dans les détails. Un modèle de langage génère du code qui semble raisonnable d’après les motifs qu’il a appris. « Semble raisonnable » ne veut pas dire « correct dans votre projet ». Il y a trois causes fondamentales :

  • Manque de contexte : l’agent ne voit que la partie du code qu’il parvient à lire au cours d’une session. Il ignore les conventions implicites de l’équipe, l’historique derrière un bout de code étrange, ou les contraintes métier qui n’ont pas été documentées.
  • Mauvaise optimisation : quand vous demandez « fais passer les tests », l’agent peut le faire au sens littéral, y compris en modifiant les tests.
  • Confiance mal placée : l’agent dit rarement « je ne suis pas sûr ». Du code faux et du code juste sont présentés avec le même ton.

Ce sujet devient crucial parce que les agents ne se limitent plus à suggérer quelques lignes. De plus en plus d’équipes leur confient une tâche entière. Plus le diff est volumineux, plus c’est difficile à review, et plus les bugs risquent de passer.

Tous les exemples ci-dessous ne demandent que spring-boot-starter-test. La version de ce starter est gérée par le parent Spring Boot, et le starter inclut déjà JUnit 5, AssertJ et Mockito :

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3. Types d’erreurs courants

3.1. API « imaginaire » (hallucinated API)

L’agent appelle une méthode qui semble raisonnable mais n’existe pas :

Customer customer = customerRepository.findById(id)
        .orElseThrowIfEmpty(() -> new CustomerNotFoundException(id)); // n'existe pas

Optional dispose seulement de orElseThrow(...), pas de orElseThrowIfEmpty. Heureusement, le compilateur détecte cette erreur immédiatement.

La leçon ici : une erreur de compilation est l’erreur la moins chère. Si l’agent est autorisé à lancer mvn compile, il va la corriger. La variante plus dangereuse est une méthode qui existe bel et bien, mais avec un comportement différent de ce que l’agent « pense ». Par exemple, l’agent suppose que List.of(...) retourne une liste modifiable, alors qu’elle est immutable et l’erreur n’apparaît qu’à l’exécution.

3.2. Code qui compile, s’exécute, mais est métier incorrect

C’est le type d’erreur le plus dangereux. Supposons que vous demandiez à l’agent « écrire une fonction qui calcule le total d’une commande » :

public record OrderLine(String sku, double unitPrice, int quantity) {}

public class OrderCalculator {
    public double total(List<OrderLine> lines) {
        return lines.stream()
                .mapToDouble(line -> line.unitPrice() * line.quantity())
                .sum();
    }
}

Le code est concis et lisible. Il est quand même faux, parce qu’il utilise double pour l’argent :

var lines = List.of(new OrderLine("PEN", 0.1, 1), new OrderLine("BOOK", 0.2, 1));
System.out.println(new OrderCalculator().total(lines));
0.30000000000000004

L’agent n’a pas commis d’erreur syntaxique. Il a échoué parce qu’il manquait une règle que votre équipe juge évidente : l’argent s’exprime toujours avec BigDecimal. La version correcte :

public record OrderLine(String sku, BigDecimal unitPrice, int quantity) {}

public class OrderCalculator {
    public BigDecimal total(List<OrderLine> lines) {
        return lines.stream()
                .map(line -> line.unitPrice().multiply(BigDecimal.valueOf(line.quantity())))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

3.3. Tests « pour la forme » : vérifier un mock plutôt que le code

Quand on demande à l’agent d’écrire des tests, il fabrique parfois des tests qui passent toujours, peu importe ce que fait le vrai code :

@Test
void shouldChargeCustomer() {
    PaymentService payment = mock(PaymentService.class);
    when(payment.charge("C1", new BigDecimal("100"))).thenReturn(true);

    assertThat(payment.charge("C1", new BigDecimal("100"))).isTrue();
}

Ce test ne confirme que le bon fonctionnement de Mockito. Il n’exerce aucune ligne du code métier. Le bon test doit passer par la classe à vérifier, par exemple CheckoutService :

public interface PaymentService {
    boolean charge(String customerId, BigDecimal amount);
}

public class CheckoutService {
    private final PaymentService paymentService;

    public CheckoutService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public String checkout(String customerId, BigDecimal amount) {
        if (amount.signum() <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        return paymentService.charge(customerId, amount) ? "PAID" : "FAILED";
    }
}
class CheckoutServiceTest {

    private final PaymentService payment = mock(PaymentService.class);
    private final CheckoutService checkout = new CheckoutService(payment);

    @Test
    void returnsPaidAndChargesExactAmount() {
        when(payment.charge("C1", new BigDecimal("100.00"))).thenReturn(true);

        assertThat(checkout.checkout("C1", new BigDecimal("100.00"))).isEqualTo("PAID");
        verify(payment).charge("C1", new BigDecimal("100.00"));
    }

    @Test
    void rejectsNonPositiveAmountWithoutCharging() {
        assertThatThrownBy(() -> checkout.checkout("C1", BigDecimal.ZERO))
                .isInstanceOf(IllegalArgumentException.class);
        verifyNoInteractions(payment);
    }
}

Le premier test vérifie à la fois le résultat retourné et que le montant exact a été débité. Le second teste une branche d’erreur et s’assure qu’aucune transaction n’est lancée. Un truc simple pour détecter les tests « pour la forme » : cassez intentionnellement le code puis relancez les tests. Si les tests restent verts, ils ne servent à rien.

3.4. Modifier les tests pour que la build passe

Vous demandez « répare ce test qui échoue ». L’agent peut modifier l’assertion plutôt que le code :

// Avant
assertThat(calculator.total(lines)).isEqualByComparingTo("0.30");

// Après le « fix » de l'agent
assertThat(calculator.total(lines)).isNotNull();

La build devient verte et la tâche est reportée comme terminée, mais le bug persiste. Parmi les variantes de cette erreur : ajouter @Disabled, supprimer entièrement le test, ou élargir la plage de tolérance dans l’assertion.

3.5. « Avaler » l’exception pour qu’elle disparaisse

Face à une exception à l’exécution, l’agent peut choisir le chemin le plus rapide pour la faire disparaître :

public Customer loadCustomer(String id) {
    try {
        return customerClient.fetch(id);
    } catch (Exception e) {
        return null; // erreur gérée
    }
}

L’erreur originale, comme un timeout ou une mauvaise configuration, se retrouve cachée. Le vrai problème surgira plus tard sous la forme d’une NullPointerException ailleurs, beaucoup plus difficile à remonter.

3.6. Ignorer les conventions existantes et étendre les changements

Le projet a déjà MoneyUtils.round(...) mais l’agent réinvente sa propre fonction de rounding, avec un RoundingMode différent. Ou vous lui demandez de corriger un petit bug, et le diff retourné touche 15 fichiers : renommage de variables, reformatage du code, et « tant qu’on y est » on remonte une version de dépendance. Un gros diff dilue l’attention lors de la review, et le vrai bug se perd au milieu des changements inoffensifs.

4. Comment limiter les erreurs

4.1. Écrire les tests d’abord, donner à l’agent la partie implémentation

Le moyen le plus efficace est de laisser vous définir ce qu’« être correct » signifie, tandis que l’agent gère « comment faire ». Les tests sont une spécification exécutable :

class OrderCalculatorTest {

    private final OrderCalculator calculator = new OrderCalculator();

    @Test
    void sumsLinesWithoutFloatingPointError() {
        var lines = List.of(
                new OrderLine("PEN", new BigDecimal("0.10"), 1),
                new OrderLine("BOOK", new BigDecimal("0.20"), 1));

        assertThat(calculator.total(lines)).isEqualByComparingTo("0.30");
    }

    @Test
    void returnsZeroForEmptyOrder() {
        assertThat(calculator.total(List.of())).isEqualByComparingTo("0");
    }
}

Le premier test oblige l’agent à utiliser BigDecimal à cause du type de OrderLine. Le second fixe le comportement pour une commande vide. Quand les tests sont écrits par vous et que l’agent n’a pas le droit de les modifier, les erreurs décrites aux sections 3.2 et 3.4 deviennent beaucoup moins probables.

4.2. Fichier de conventions pour l’agent

La plupart des coding agents peuvent lire un fichier de directives placé à la racine du dépôt. Le nom du fichier dépend de l’outil, AGENTS.md est une convention assez commune. Couchez par écrit les choses que votre équipe considère comme « évidentes » :

# Conventions pour l'agent

## Build et tests
- Exécuter `./mvnw -q verify` avant de rapporter la fin de tâche.

## Obligatoire
- Monnaie : toujours utiliser BigDecimal, arrondir via MoneyUtils.round().
- Ne pas attraper Exception au sens général ; ne pas retourner null pour cacher une erreur.
- NE PAS modifier, supprimer ou @Disabled un test existant. Si vous pensez qu'un test est faux, arrêtez et expliquez.

## Périmètre
- Modifier uniquement les fichiers directement liés à la tâche.
- Ne pas changer les versions de dépendances, ne pas reformater les fichiers non concernés.

Ce fichier est court, précis, et chaque ligne correspond à un type d’erreur déjà rencontré. Rédiger des généralités comme « écrire du code propre » a peu d’effet.

4.3. Portes de contrôle automatiques

Les conventions écrites peuvent être ignorées, mais une build en échec ne peut pas. Vous pouvez activer les avertissements du compilateur et faire échouer la build s’il y en a :

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <compilerArgs>
            <arg>-Xlint:all</arg>
        </compilerArgs>
        <failOnWarning>true</failOnWarning>
    </configuration>
</plugin>

Ensuite, ajoutez un script dans votre IC pour détecter les tests modifiés ou désactivés :

#!/usr/bin/env bash
set -euo pipefail

./mvnw -q verify

changed_tests=$(git diff --name-only origin/main...HEAD -- 'src/test/**' || true)
if [[ -n "$changed_tests" ]]; then
  echo "Attention : la PR modifie des tests, un reviewer doit vérifier :"
  echo "$changed_tests"
fi

if git diff origin/main...HEAD -- 'src/test/**' | grep -E '^\+.*@Disabled'; then
  echo "Erreur : @Disabled nouvellement ajouté détecté." >&2
  exit 1
fi

Le script lance d’abord toute la build et les tests. Ensuite il liste les fichiers de test modifiés pour que le reviewer les examine, et rejette complètement la PR s’il trouve un @Disabled nouveau. Voici un exemple de sortie si l’agent essaie de désactiver un test :

Attention : la PR modifie des tests, un reviewer doit vérifier :
src/test/java/com/shop/OrderCalculatorTest.java
+    @Disabled
Erreur : @Disabled nouvellement ajouté détecté.

Vous pouvez étendre cette approche pour bloquer d’autres motifs, comme catch (Exception, ou ajouter de l’analyse statique et des tests architecturaux à votre pipeline.

4.4. Petites tâches, petits diffs, review ciblée

  • Découper les tâches : au lieu de « implémenter la fonctionnalité panier », dites « ajouter CartService.addItem pour que le test existant passe ». Chaque tâche ne doit produire qu’un diff lisible en quelques minutes.
  • Lire le diff, pas le résumé : l’agent peut annoncer « gestion d’erreur ajoutée » alors qu’il y a simplement un return null.
  • Checklist de review propre au code généré : les tests ont-ils été modifiés ? Y a-t-il des catch qui avalent les erreurs ? Y a-t-il une fonction dupliquant un utilitaire existant ? Y a-t-il des changements en dehors du périmètre de la tâche ?
  • Exécuter le code réel : pour les changements importants, lancez l’application vous-même ou un scénario d’intégration plutôt que de simplement faire confiance au vert des tests.

Remarque : Ne laissez pas l’agent écrire à la fois les tests et l’implémentation pour un même comportement, puis se relire lui-même. Là, chaque malentendu est « confirmé » deux fois, et vous n’avez plus une source de vérité indépendante.

5. Quand ne pas confier le travail à un coding agent

Un agent excelle quand il y a une spécification claire, des tests et c’est facile à vérifier. Vous devriez bien réfléchir, ou utiliser l’agent uniquement pour suggérer et non décider, dans les cas suivants :

  • Code de sécurité : authentification, autorisation, chiffrement, validation d’entrées externes. Les erreurs ici ne font souvent pas échouer les tests.
  • Concurrence : une race condition est rarement détectée dans les tests unitaires, et du code « qui semble correct » devient d’autant plus dangereux.
  • Migration de données en production : une opération irréversible ne doit pas être confiée à un agent qui tâtonne par essai-erreur.
  • Zone de code sans tests : l’agent n’a pas de signal de rétroaction, et vous n’avez aucun moyen de vérifier. Écrivez d’abord des tests de caractérisation, puis confiez le travail.
  • Grandes décisions d’architecture : choix d’une architecture, frontières de modules, modèle de données. L’agent peut proposer des options à examiner, mais la responsabilité vous incombe.

6. Conclusion

Les coding agents se trompent selon des motifs assez prévisibles : appeler une API inexistante, écrire du code syntaxiquement correct mais métier incorrect, créer des tests « pour la forme », modifier les tests pour que la build passe, avaler les exceptions et étendre les changements hors périmètre. La cause racine est le manque de contexte, la mauvaise optimisation et la confiance mal placée.

Limiter ces erreurs n’est pas une question de trouver une commande parfaite, mais de structurer l’environnement de travail de l’agent. Vous écrivez les tests comme spécification, vous documentez les conventions précises dans un fichier dédié, vous installez des portes de contrôle automatiques en IC, vous gardez les tâches petites et vous examinez les diffs avec attention. De cette manière, l’agent devient un outil qui accélère vraiment, au lieu de devenir une source de bugs difficiles à trouver.

aicoding-agenttestingjavabest-practices