Review von KI-generiertem Code
Projektanforderungen an Entwickler in Zeiten von KI, Teil 3
Dienstagmorgen, Sprint Review. Ein Kollege hat einen Coding-Agenten beauftragt, die Bestellauswertung zu überarbeiten. Der Pull Request sieht vorbildlich aus: saubere Struktur, vollständige Unit-Tests. Der Code kompiliert, die Pipeline ist grün. Im Review fällt niemandem etwas auf, der PR wird gemergt. Drei Wochen später meldet das Controlling, dass die Umsatzzahlen pro Filiale nicht mehr stimmen. Die Ursache: ein falscher Join. Die Abfrage verknüpfte Bestellungen über die Branch-ID des Kunden statt über die Branch-ID der Bestellung. Kunden, die in einer Filiale registriert sind, aber in einer anderen bestellen, wurden falsch zugeordnet. Der Code war syntaktisch einwandfrei. Die Tests prüfen, ob Daten zurückkamen. Nur prüfen sie nicht, ob es die richtigen waren.
Kein Einzelfall, sondern das typische Fehlermuster von KI-generiertem Code: technisch korrekt, semantisch falsch. Die ersten beiden Teile dieser Serie zum Thema „Projektanforderungen an Entwickler in Zeiten von KI“ haben gezeigt, wie man einem Agenten Architekturwissen mitgibt und Aufgaben präzise formuliert. Dieser dritte Teil behandelt die letzte Verteidigungslinie: den Code-Review. Fünf Muster, die bei KI-generiertem Code häufig auftreten, jeweils mit einem C#-Beispiel, das kompiliert, Tests besteht und trotzdem falsch ist.
Muster 1: Falsche Join-Bedingungen
Der häufigste und gefährlichste Fehler betrifft die Datenabfrage. Agenten erzeugen syntaktisch korrekte LINQ-Queries, die plausibel aussehen, aber in der Semantik der Verknüpfung falsch sind. Ein Agent, der eine Umsatzauswertung pro Filiale schreiben soll, produziert folgenden Code:
public async Task<List<BranchRevenueDto>>
GetRevenueByBranch(int year)
{
return await _context.Orders
.Where(o => o.OrderDate.Year == year)
.Join(_context.Customers,
o => o.CustomerId,
c => c.Id,
(o, c) => new { o, c })
.GroupBy(x => x.c.BranchId)
.Select(g => new BranchRevenueDto
{
BranchId = g.Key,
Total = g.Sum(
x => x.o.TotalAmount)
})
.ToListAsync();
}
Die Abfrage kompiliert und liefert Ergebnisse. Ein Unit-Test mit einem Kunden, der in derselben Filiale bestellt, in der er registriert ist, wird grün. Aber die Gruppierung erfolgt über c.BranchId, die Stammfiliale des Kunden, nicht über o.BranchId, die Filiale der Bestellung. Sobald Kunden filialübergreifend bestellen, stimmen die Zuordnungen nicht. Korrekt wäre eine Gruppierung über o.BranchId, ohne den überflüssigen Join auf Customers. Die Review-Frage: Wird über die richtige Entity gruppiert, oder hat der Agent eine Indirektion eingefügt, die die Semantik verschiebt?
Muster 2: Übersehene Edge Cases
Agenten erzeugen Code für den Normalfall. Randwerte wie negative Eingaben, Nullwerte oder Nebenläufigkeit werden übersehen, weil sie in der Aufgabenstellung nicht erwähnt werden. Ein typisches Beispiel:
public decimal CalculateDiscount(
Order order)
{
decimal rate = order.Quantity switch
{
>= 100 => 0.15m,
>= 50 => 0.10m,
>= 10 => 0.05m,
_ => 0m
};
return order.UnitPrice
* order.Quantity * rate;
}
Der Code besteht jeden Test mit positiven Mengen. Aber was passiert bei einer Retoure, wenn Quantität den Wert minus 50 hat? Der Wert fällt in den Default-Zweig, der Rabatt ist null, die Gutschrift wird ohne den ursprünglichen Rabatt berechnet. Der Kunde bekommt mehr zurück, als er bezahlt hat. Eine Guard Clause für negative Werte hätte das verhindert. Im Review sollte man bei jeder Berechnung die Extremwerte durchspielen: null, negative Werte, Überlauf. Agenten optimieren auf den Happy Path, weil ihre Trainingsdaten überwiegend Happy-Path-Code enthalten.
Muster 3: Silent Failures
Agenten haben gelernt, dass produktionsreifer Code Exceptions behandeln soll, und interpretieren das als Verschlucken statt Behandeln. Ein Beispiel aus einer Zahlungsverarbeitung:
public async Task<PaymentResult>
ProcessPayment(PaymentRequest req)
{
try
{
var charge = await _gateway
.ChargeAsync(req.Amount,
req.CardToken);
await _orderRepo
.UpdateStatusAsync(
req.OrderId,
OrderStatus.Paid);
return new PaymentResult
{ Success = true };
}
catch (Exception ex)
{
_logger.LogWarning(
"Payment issue: {Msg}",
ex.Message);
return new PaymentResult
{ Success = false };
}
}
Auf den ersten Blick saubere Fehlerbehandlung: Es wird geloggt, ein Fehlerstatus zurückgegeben. Aber wenn ChargeAsync erfolgreich ist und UpdateStatusAsync fehlschlägt, wird die Kreditkarte belastet, die Bestellung bleibt jedoch im Status offen. Der Catch-Block behandelt beide Fehlerquellen identisch. LogWarning statt LogError sorgt dafür, dass Alerts nicht auslösen. Und Success = false suggeriert dem Aufrufer, dass keine Belastung stattfindet. Im Review muss man bei jedem Try-Catch prüfen, ob der Catch alle Fehlerzustände korrekt behandelt oder Teilausführungen maskiert.
Muster 4: Security-Antipatterns
Sicherheitslücken in KI-generiertem Code entstehen durch Auslassungen. Der Agent implementiert die Geschäftslogik, vergisst aber Autorisierung oder baut dynamische Queries ohne Parametrisierung:
[HttpGet("{tenantId}/invoices")]
public async Task<IActionResult>
GetInvoices(int tenantId,
[FromQuery] string filter)
{
var sql = $"SELECT * FROM Invoices" +
$" WHERE TenantId = {tenantId}"
+ (string.IsNullOrEmpty(filter)
? ""
: $" AND Description LIKE" +
$" '%{filter}%'");
var invoices = await _context
.Invoices
.FromSqlRaw(sql)
.ToListAsync();
return Ok(invoices);
}
Drei Sicherheitsprobleme, keines ein Kompilierungsfehler. Erstens fehlt ein Authorize-Attribut, der Endpoint ist öffentlich erreichbar. Zweitens fehlt eine Prüfung, ob der Benutzer zum angegebenen Tenant gehört. Drittens erzeugt die String-Konkatenation im Filter-Parameter eine SQL-Injection-Lücke. Der Agent hat die fachliche Anforderung korrekt umgesetzt. Aber Sicherheit war nicht Teil der Aufgabenstellung, also ist sie nicht Teil der Lösung. Im Review: Authentifizierung vorhanden? Mandantenzugehörigkeit validiert? Eingaben parametrisiert?
Muster 5: Architekturverstöße
Das fünfte Muster verursacht keine unmittelbaren Fehler, aber es zerstört die Architektur. Agenten packen Geschäftslogik in Controller und greifen direkt auf den DbContext zu, weil das der kürzeste Weg ist:
[HttpPost("cancel")]
[Authorize]
public async Task<IActionResult>
CancelOrder(int orderId)
{
var order = await _context.Orders
.Include(o => o.Items)
.FirstAsync(
o => o.Id == orderId);
if (order.Status !=
OrderStatus.Pending)
return BadRequest(
"Nur offene Bestellungen");
order.Status =
OrderStatus.Cancelled;
foreach (var item in order.Items)
item.Product.Stock +=
item.Quantity;
order.CancelledAt = DateTime.Now;
await _context.SaveChangesAsync();
return Ok();
}
Funktional korrekt: Statusänderung, Bestandsbuchung, Zeitstempel. Aber die gesamte Stornologik steckt im Controller statt in einem Handler. Der Controller greift direkt auf den DbContext zu statt über ein Repository. DateTime.Now anstelle einer injizierten IDateTimeProvider macht die Methode untestbar. Die Bestandszurückbuchung ist nicht transaktionssicher, weil die Products nicht explizit geladen werden. In einem CQRS-Projekt mit MediatR würde das als CancelOrderCommand mit eigenem Handler implementiert. Im Review erkennt man dieses Muster daran, dass der Controller mehr als Routing und Delegation enthält.
Die Review-Checkliste
Fünf Muster, fünf Fragen für jeden Review von KI-generiertem Code. Queries: Wird über die richtige Entity verknüpft? Berechnungen: Was passiert bei negativen Werten und Extremfällen? Fehlerbehandlung: Maskiert der Catch-Block Teilausführungen? Endpoints: Sind Authentifizierung, Autorisierung und Eingabevalidierung vorhanden? Architektur: Liegt die Logik in der richtigen Schicht?
Diese Checkliste ersetzt keinen gründlichen Review, aber sie lenkt die Aufmerksamkeit auf die Stellen, an denen KI-generierter Code am zuverlässigsten versagt. Die Gemeinsamkeit aller fünf Muster: Der Fehler ist nicht im Code sichtbar, sondern erst im Abgleich mit der Geschäftslogik, den Sicherheitsanforderungen oder der Projektarchitektur. Genau das macht Review von KI-Code so anspruchsvoll: Es ist keine Syntaxprüfung, es ist eine Geschäftslogik-Prüfung. Der Hauptartikel dieser Serie [1] zeigt, wie sich die Rolle des Entwicklers verschiebt, vom Codeschreiber zum Qualitätssicherer, der versteht, was der Code tun soll, und prüfen kann, ob er es tut.
[1] Patrick Schnell, Der Stellenwert von Developern in Zeiten von KI, dotnetpro 4/2026, Seite 6 ff.