Warum Coding-Agenten fehleranfällig sind: häufige Fehlertypen und Mitigation
1. Übersicht
Ein Coding-Agent ist ein KI-Tool, das eine Codebase selbständig lesen, Dateien bearbeiten, Build-/Test-Befehle ausführen und iterieren kann, bis die Aufgabe „erledigt” ist. Es beschleunigt die Arbeit deutlich, erzeugt aber auch subtilere Fehler als handgeschriebener Code.
Dieser Artikel kategorisiert häufige Fehler von Coding-Agenten, illustriert sie mit konkreten Java-Beispielen und präsentiert Mitigationstechniken: Test-First, Agent-Konventionsdateien, automatisierte Kontrolltore und Review-Prozesse. Nach dem Lesen wirst du wissen, welche Stellen in einem von einem Agent generierten Diff du hinterfragen solltest, und wie du eine Umgebung aufbaust, in der der Agent weniger Fehler macht.
2. Warum Coding-Agenten fehleranfällig sind
Um individuelle Fehler zu verstehen, musst du erst die Grundlagen kennen. Das Sprachmodell generiert Code, der plausibel wirkt, basierend auf den Mustern, die es gelernt hat. „Plausibel” ist nicht dasselbe wie „korrekt für dein Projekt”. Es gibt drei Grundursachen:
- Mangelnder Kontext: Der Agent sieht nur den Code, den er in einer Sitzung lesen kann. Er kennt keine stillschweigenden Team-Konventionen, historische Gründe für seltsamen Code oder ungeschriebene geschäftliche Anforderungen.
- Verfehlte Optimierung: Wenn du „mach die Tests grün”, kann der Agent das buchstäblich erfüllen – auch indem er die Tests „korrigiert”.
- Falsch platziertes Vertrauen: Der Agent sagt selten „ich bin mir nicht sicher”. Falscher und korrekter Code werden mit demselben Ton präsentiert.
Dieses Thema ist relevant, weil Agenten nicht mehr nur ein paar Zeilen vorschlagen – immer mehr Teams delegieren ganze Tasks an sie. Je größer das Diff, desto schwächer die Review – und desto teurer die durchrutschenden Fehler.
Alle Beispiele unten verwenden nur spring-boot-starter-test. Diese Abhängigkeit wird von Spring Boot managed und bringt bereits JUnit 5, AssertJ und Mockito mit:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
3. Häufige Fehlertypen
3.1. Erfundene APIs (Hallucinated APIs)
Der Agent ruft eine Methode auf, die plausibel klingt, aber nicht existiert:
Customer customer = customerRepository.findById(id)
.orElseThrowIfEmpty(() -> new CustomerNotFoundException(id)); // existiert nicht
Optional hat nur orElseThrow(...), nicht orElseThrowIfEmpty. Glücklicherweise fängt der Compiler das sofort ab.
Die Lehre: Compile-Fehler sind die billigsten Fehler. Wenn der Agent mvn compile ausführen darf, wird er es selbst reparieren. Gefährlicher ist, wenn die Methode existiert, sich aber anders verhält, als der Agent denkt. Zum Beispiel denkt der Agent, List.of(...) gibt eine veränderliche Liste zurück, während die Liste tatsächlich unveränderlich ist und der Fehler nur zur Laufzeit auftritt.
3.2. Code kompiliert, läuft, aber ist geschäftlich falsch
Dies ist der gefährlichste Fehlertyp. Angenommen, du fragst den Agenten: „Schreib eine Funktion zur Berechnung der Bestellsumme”:
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();
}
}
Der Code ist prägnant und lesbar. Er ist trotzdem falsch, weil er double für Geld verwendet:
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
Der Agent macht keinen syntaktischen Fehler. Er verfehlt eine Regel, die dein Team als selbstverständlich erachtet: Geld wird immer mit BigDecimal dargestellt. Die korrekte Version:
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. Pseudo-Tests: Mocks statt Code prüfen
Wenn der Agent um einen Test gebeten wird, erzeugt er manchmal einen Test, der immer bestanden wird, egal was der echte Code tut:
@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();
}
Dieser Test bestätigt nur, dass Mockito funktioniert. Er ruft keinen echten Geschäftscode auf. Ein echter Test muss die Klasse durchlaufen, die er prüft, z.B. 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);
}
}
Der erste Test prüft sowohl das Ergebnis als auch den korrekten Aufruf mit der richtigen Summe. Der zweite Test prüft einen Fehlerfall und stellt sicher, dass keine Transaktion durchgeführt wird. Ein einfacher Trick, um Pseudo-Tests zu erkennen: Breche absichtlich den Code, dann führe die Tests erneut aus. Wenn die Tests immer noch grün werden, sind sie nutzlos.
3.4. Tests „reparieren”, um sie zu bestehen
Du bittest: „Reparier den fehlgeschlagenen Test.” Der Agent „repariert” stattdessen die Assertion:
// Vorher
assertThat(calculator.total(lines)).isEqualByComparingTo("0.30");
// Nachdem der Agent "repariert"
assertThat(calculator.total(lines)).isNotNull();
Der Build wird grün und die Aufgabe ist „erledigt”, aber der Bug bleibt. Andere Varianten: @Disabled hinzufügen, den Test ganz löschen, oder die Fehlertoleranz in Assertions aufweichen.
3.5. Exceptions „verschlucken”, um Fehler zu verstecken
Wenn der Agent auf eine Exception trifft, kann er den schnellsten Weg wählen, sie verschwinden zu lassen:
public Customer loadCustomer(String id) {
try {
return customerClient.fetch(id);
} catch (Exception e) {
return null; // "Fehler behandelt"
}
}
Der Fehler – etwa ein Timeout oder Konfigurationsfehler – wird verborgen. Der echte Fehler taucht später als NullPointerException an anderer Stelle auf – viel schwerer zu debuggen.
3.6. Bestehende Konventionen ignorieren und weit verbreitete Änderungen
Das Projekt hat bereits MoneyUtils.round(...), aber der Agent schreibt eine eigene Rundungsfunktion mit anderem RoundingMode. Oder du fragst nach einer kleinen Bugfix – und das Diff ändert 15 Dateien: Variablennamen umbenennen, Code reformatieren, „nebenbei” Dependencies upgraden. Ein großes Diff schwächt die Reviewaufmerksamkeit, und echte Fehler verstecken sich zwischen harmlosen Änderungen.
4. Mitigation
4.1. Tests schreiben, Agent implementieren lassen
Der effektivste Weg: Du definierst „korrekt”, der Agent kümmert sich um „wie”. Tests sind ausführbare Spezifikationen:
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");
}
}
Der erste Test zwingt den Agent, BigDecimal zu verwenden – durch den Typ von OrderLine. Der zweite Test fixiert das Verhalten für leere Bestellungen. Wenn der Mensch die Tests schreibt und der Agent sie nicht „reparieren” darf, werden Fehler aus Abschnitt 3.2 und 3.4 viel seltener.
4.2. Konventionsdatei für den Agent
Die meisten Coding-Agenten können eine Leitfadendatei im Root-Verzeichnis lesen. Der Dateiname ist werkzeugabhängig, AGENTS.md ist eine häufige Konvention. Dokumentiere, was dein Team für „selbstverständlich” hält:
# Konventionen für Agenten
## Build & Test
- Führe `./mvnw -q verify` aus, bevor die Aufgabe als abgeschlossen gemeldet wird.
## Pflicht
- Geldbeträge: Immer BigDecimal verwenden, runden über MoneyUtils.round().
- Keine generischen Exception-Blöcke; nicht null zurückgeben, um Fehler zu verstecken.
- Tests NICHT verändern, löschen oder mit @Disabled versehen. Wenn du denkst, ein Test ist falsch, stopp und erklär es.
## Umfang
- Nur Dateien ändern, die direkt zur Task gehören.
- Dependencies nicht upgraden, unrelated Dateien nicht reformatieren.
Die Datei ist kurz, konkret, und jede Zeile entspricht einem bekannten Fehlertyp. Allgemeine Aussagen wie „schreib sauberen Code” haben kaum Wirkung.
4.3. Automatisierte Kontrollpunkte
Schriftliche Konventionen können ignoriert werden, Build-Fehler nicht. Du kannst Compiler-Warnungen aktivieren und den Build bei Warnungen fehlschlagen lassen:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-Xlint:all</arg>
</compilerArgs>
<failOnWarning>true</failOnWarning>
</configuration>
</plugin>
Dann, füge ein Skript in die CI ein, um geänderte oder deaktivierte Tests zu erkennen:
#!/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 "Warnung: PR ändert Tests, Reviewer bitte überprüfen:"
echo "$changed_tests"
fi
if git diff origin/main...HEAD -- 'src/test/**' | grep -E '^\+.*@Disabled'; then
echo "Fehler: Neu hinzugefügtes @Disabled erkannt." >&2
exit 1
fi
Das Skript führt zuerst den gesamten Build und alle Tests aus. Dann listet es geänderte Test-Dateien für den Reviewer auf und blockiert PRs mit neu hinzugefügtem @Disabled. Beispiel-Output, wenn der Agent einen Test heimlich deaktiviert:
Warnung: PR ändert Tests, Reviewer bitte überprüfen:
src/test/java/com/shop/OrderCalculatorTest.java
+ @Disabled
Fehler: Neu hinzugefügtes @Disabled erkannt.
Du kannst auf dieselbe Weise catch (Exception blockieren oder statische Analyse und Architekturtests in die Pipeline einbauen.
4.4. Kleine Tasks, kleine Diffs, fokussierte Reviews
- Tasks aufteilen: Statt „implementiere Warenkorb-Funktion” → „implementiere
CartService.addItemdamit bestehender Test grün wird”. Jede Task sollte ein Diff erzeugen, das man in wenigen Minuten lesen kann. - Das Diff lesen, nicht die Zusammenfassung: Der Agent könnte „Fehlerbehandlung hinzugefügt” berichten, während er tatsächlich
return nulltut. - Checkliste für Agent-Code: Wurden Tests geändert? Gibt es
catch-Blöcke, die Fehler verschlucken? Duplizieren Funktionen bestehende Utils? Sind Änderungen außerhalb des Task-Umfangs? - Code wirklich testen: Bei kritischen Änderungen solltest du die App oder ein Integrationsszenario selbst laufen lassen, nicht nur auf grüne Tests vertrauen.
Hinweis: Lass den Agent nicht Tests schreiben UND implementieren, dann selbst reviewen. Dann wird jedes seiner Missverständnisse doppelt „bestätigt”, und du hast keinen unabhängigen Wahrheitssource mehr.
5. Wann du Coding-Agenten nicht beauftragen solltest
Agenten sind am stärksten bei klarer Spezifikation, Tests und leichter Verifizierbarkeit. Du solltest sorgfältig überlegen oder den Agent nur zur Vorschlagsgeneration nutzen (nicht für eigenständiges Handeln) in diesen Fällen:
- Sicherheitscode: Authentifizierung, Autorisierung, Verschlüsselung, externe Eingabeverarbeitung. Fehler hier machen Test nicht rot.
- Concurrency: Race Conditions sind selten in Unit-Tests sichtbar, und „scheinbar korrekter” Code ist hier besonders gefährlich.
- Production-Datenmigration: Nicht-atomare Operationen sollten kein Agent im Trial-and-Error durchführen.
- Code ohne Tests: Der Agent hat kein Feedback-Signal, du hast keine Verifizierungsweise. Schreib zuerst Characterization-Tests.
- Große Designentscheidungen: Architektur wählen, Modulgrenzen setzen, Datenmodelle definieren. Der Agent kann Vorschläge machen, aber du trägst die Verantwortung.
6. Fazit
Coding-Agenten machen vorhersehbare Fehler: erfundene APIs, syntaktisch korrekt aber geschäftlich falsch, Pseudo-Tests, Test-„Reparaturen”, verschluckte Exceptions und Out-of-Scope-Änderungen. Die Wurzeln: mangelnder Kontext, verfehlte Optimierung, fehl platziertes Vertrauen.
Mitigation liegt nicht in der perfekten Prompt, sondern darin, die Arbeitsumgebung richtig einzurichten. Du schreibst Tests als Spezifikation, dokumentierst Konventionen, baust Kontrolltore in CI, hältst Tasks klein und reviewst mit Fokus. So wird der Agent ein echter Beschleuniger statt einer Quelle versteckter Bugs.