Wer Zeile 14 nicht erklären kann, besitzt den Code nicht

Von hyretic

Freitagnachmittag, ein letzter Utility-Task, 40 Zeilen elegantes TypeScript aus dem Chatfenster. Die Pipeline läuft grün durch, weil die Testsuite im Kern prüft, ob die Anwendung startet. Der Code geht raus. Was Zeile 14 tut, kann anschließend niemand im Team erklären.

Key Takeaways (TL;DR)

  • Zero Trust für Modell-Output: Behandle jede generierte Datei wie den Pull Request eines fremden Entwicklers und nicht wie geprüften Code.
  • Erst der Vertrag, dann die Logik: Lass Typen, Signaturen und Datenstrukturen festlegen, bevor du eine einzige Zeile Implementierung anforderst.
  • Kleine Einheiten statt großer Würfe: Fordere isolierte, seiteneffektfreie Funktionen an, weil sich in ihnen kein Fehler hinter einer Textwand verstecken kann.
  • Grüne Tests sind kein Eigentumsnachweis: Prüfe Grenzfälle selbst, wenn Implementierung und Test aus derselben Quelle stammen.

Basiert auf “Stop Being an AI Babysitter: How to Use LLMs Without Losing Code Ownership” von Sergio Lema. Das Original findest du hier: https://sergiolema.dev/2026/08/03/stop-being-an-ai-babysitter-how-to-use-llms-without-losing-code-ownership/

Drei Sätze, mit denen Ownership verloren geht

Sergio Lema, Software Architect und Autor des Blogs The Dev World, sammelt drei Ausreden, die in jedem Team fallen. „Ist doch nur Boilerplate” ist die erste. Boilerplate liegt aber nicht neben dem System, sondern bestimmt, wie deine Daten strukturiert sind, und läuft im heißen Pfad der Anwendung mit. „Ich refactore das später” ist die zweite. Am Montag liegen drei neue Tickets im Sprint, und aus dem Provisorium wird die Grundlage, auf der zwei Jahre lang weitergebaut wird.

Die dritte Ausrede ist die gefährlichste, weil sie nach Prozess klingt: „Die Tests sind grün.” Wenn das Modell die Implementierung geschrieben hat und die Tests dazu, sind zwei grüne Häkchen aus derselben Quelle kein Beleg, sondern eine Selbstauskunft. Genau dieses Durchwinken beschreibt auch der Praxisbericht über 1.690 Zeilen weggeworfenen Kubernetes-Code: sieben Monate Diffs überflogen, Build geprüft, Happy Path getestet, den Code selbst nie gelesen.

Wie der Standardfall aussieht, zeigt eine Rabattberechnung. Der Prompt fordert eine Funktion mit Preis, Tier und Promo-Codes, 20 Prozent für GOLD, 10 für SILVER, 5 Prozent Aufschlag ab 100 Euro, dazu der Code SAVE10. Ergänzt um die Anweisung No comments. Die Beispiele des Originals sind in Java geschrieben, hier zur besseren Lesbarkeit nach TypeScript übertragen:

// TypeScript 5.x, aus dem Java-Original übertragen
export function getPrice(p: number, t: string, codes: string[] | null): number {
  let d = 0;
  if (t === "GOLD") {
    d = 0.2;
  } else if (t === "SILVER") {
    d = 0.1;
  }
  if (p > 100) {
    d += 0.05;
  }
  if (codes !== null) {
    for (const code of codes) {
      if (code === "SAVE10") {
        d += 0.1;
      }
    }
  }
  return p - p * d;
}

Das ist im engsten akademischen Sinn korrekt und erfüllt jede Zeile der Anforderung. Der Tier kommt als String herein, ein Tippfehler im Aufrufer liefert also still einen Rabatt von null statt eines Compile-Fehlers. Der Rabattwert ist eine veränderliche Variable, auf die aus drei Richtungen addiert wird. Und weil die Schleife jeden Treffer zählt statt jeden Code, kippt der Rückgabewert bei einer GOLD-Bestellung über 100 Euro ab dem achten identischen SAVE10-Eintrag ins Negative. Nichts davon fällt auf, solange der Test die typische Bestellung prüft.

Fünf Dateien für ein Zehn-Zeilen-Problem

Die zweite Fehlerart sieht aus wie Handwerk und ist teurer. Wer dem Modell „Clean Code” und „Design Patterns” in den Prompt schreibt, bekommt sie zuverlässig geliefert:

// TypeScript 5.x, aus dem Java-Original übertragen, gekürzt um SilverDiscount und NoDiscount
interface DiscountStrategy {
  calculate(price: number): number;
}

class GoldDiscount implements DiscountStrategy {
  calculate(price: number): number {
    return price * 0.2;
  }
}

class DiscountFactory {
  static getStrategy(tier: string): DiscountStrategy {
    if (tier === "GOLD") return new GoldDiscount();
    if (tier === "SILVER") return new SilverDiscount();
    return new NoDiscount();
  }
}

Aus einem Zehn-Zeilen-Problem sind fünf Dateien geworden. Der entscheidende Punkt steht in der Factory: Sie vergleicht den Tier weiterhin per String. Vier zusätzliche Klassen, drei Indirektionsebenen, und der ursprüngliche Konstruktionsfehler ist unangetastet durch das Refactoring gewandert. Das nennt Lema Resume-Driven Development, und es erzeugt keine Wartbarkeit, sondern Komplexitätsschulden: Die Logik ist nicht klarer geworden, sie ist nur schwerer auffindbar.

Zero Trust heißt: erst der Vertrag, dann die Logik

Der Gegenentwurf behandelt Modell-Output wie den Pull Request eines Entwicklers, dessen Lebenslauf man nicht überprüft hat. Praktisch heißt das, die Aufgabe in drei Prompts zu zerlegen, statt einen großen abzusetzen. Schritt eins fordert ausschließlich die Datenstrukturen an, ausdrücklich ohne Logik. Schritt zwei verlangt isolierte, seiteneffektfreie Funktionen, jede mit genau einer Zuständigkeit:

// TypeScript 5.x, aus dem Java-Original übertragen
export type UserTier = "GOLD" | "SILVER" | "BRONZE";

export interface PricingContext {
  readonly basePrice: number;
  readonly userTier: UserTier;
  readonly promoCodes: readonly string[];
}

const TIER_DISCOUNTS: Record<UserTier, number> = {
  GOLD: 0.2,
  SILVER: 0.1,
  BRONZE: 0,
};

const VALID_CODES: Record<string, number> = { SAVE10: 0.1 };

export function calculateTierDiscount(price: number, tier: UserTier): number {
  return price * TIER_DISCOUNTS[tier];
}

export function calculateBulkDiscount(price: number): number {
  return price > 100 ? price * 0.05 : 0;
}

export function calculatePromoDiscount(price: number, codes: readonly string[]): number {
  return codes.reduce((sum, code) => sum + price * (VALID_CODES[code] ?? 0), 0);
}

Der String-Tier ist verschwunden, ein ungültiger Wert ist jetzt ein Compile-Fehler statt eines stillen Nullrabatts. Der Null-Check aus der ersten Fassung entfällt, weil der Typ ihn überflüssig macht, und Record<UserTier, number> erzwingt bei einer neuen Stufe den passenden Eintrag in der Tabelle. Vor allem lässt sich calculateBulkDiscount in einem Test mit 99,99 und 100,01 prüfen, ohne eine Factory zu mocken. Erst der dritte Schritt setzt die Teile zusammen und behandelt dabei explizit die Grenzfälle:

// TypeScript 5.x, aus dem Java-Original übertragen
export function calculateFinalPrice(context: PricingContext): number {
  if (context.basePrice <= 0) return 0;

  const totalDiscount = [
    calculateTierDiscount(context.basePrice, context.userTier),
    calculateBulkDiscount(context.basePrice),
    calculatePromoDiscount(context.basePrice, context.promoCodes),
  ].reduce((sum, discount) => sum + discount, 0);

  return Math.max(0, context.basePrice - totalDiscount);
}

Das Math.max fängt den negativen Preis ab, den die erste Fassung noch ausgeliefert hätte. Es beantwortet aber nicht die eigentliche Frage, die kein Modell für dich beantworten kann: ob ein Promo-Code mehrfach in derselben Bestellung zählen darf. Diese Entscheidung ist fachlich, sie steht in keinem Prompt, und sie bleibt auch nach dem Refactoring offen. Genau an solchen Stellen entscheidet sich, wer den Code besitzt.

Verifizieren ist teurer als Schreiben

Der Grund für das Abnicken ist untersucht und heißt Cognitive Offloading. Sobald eine fertige Antwort auf dem Bildschirm steht, wechselt das Gehirn aus dem Problem Solving Mode in den Verification Mode, und dieser zweite Modus ist der schwierigere von beiden. Beim Schreiben entsteht die Lösung Schritt für Schritt im Kopf, beim Prüfen musst du fremden Code mental simulieren und dabei ausgerechnet das finden, was nicht dasteht: den fehlenden Grenzfall, die nicht getroffene Annahme.

Zehn Sekunden Generierung plus null Sekunden Audit sind deshalb keine gesparte Zeit, sondern eine Verschiebung in die Debugging-Phase, wo dieselbe Analyse unter Zeitdruck und mit Produktionsdaten stattfindet. Wo diese Verschiebung endet, beschreibt der Fall aus der Ich weiß nicht, Claude hat das geschrieben-Pandemie: ein Reviewer stellt eine Architekturfrage, und der Autor des Pull Requests kann sie nicht beantworten.

Das stärkste Gegenargument: Die Modelle werden besser

Der berechtigte Einwand lautet, dass sich dieses Problem von selbst erledigt. Die Modellgenerationen liefern messbar saubereren Code als vor zwei Jahren, und was heute noch Nachkontrolle braucht, ist morgen Standardqualität. Für die Syntax stimmt das bereits. Für das eigentliche Problem stimmt es nicht, weil Code nicht das Produkt ist. Das Produkt ist ein gelöstes fachliches Problem, und kein Modell weiß, dass eure GOLD-Kunden nächsten Monat auf ein neues System migriert werden oder dass eure Datenbank ein bestimmtes Query-Muster nicht verkraftet.

“Architects don’t complain about the quality of their hammers. They just don’t let the hammer design the house.”

Wer seinen Beruf als Codeschreiben definiert, konkurriert mit einem Werkzeug, das im Sekundentakt Code schreibt. Wer ihn als Problemlösen mit Code als Werkzeug definiert, konkurriert mit niemandem.

4 Schritte zur Umsetzung

  1. AGENTS.md im Projektwurzelverzeichnis anlegen: Halte Architekturregeln, Stilvorgaben und nicht verhandelbare Einschränkungen schriftlich fest. Ohne diese Datei behandelt das Modell deine getypte TypeScript-Codebasis nach dem Durchschnitt seiner Trainingsdaten. Wie du solche Regeln aus konkreten Fehlern ableitest, statt sie zu erfinden, zeigt das Ratchet-Prinzip im Harness Engineering.
  2. Definition of Done vor den Prompt setzen: Lege Eingaben, Ausgaben und Grenzfälle fest, bevor du formulierst. Ein Prompt ohne definierte Problemgrenzen ist eine Einladung, die Grenzen zu erfinden.
  3. Vollständigen Kontext liefern statt Tunnelblick: Gib alle betroffenen Komponenten und deren Abhängigkeiten mit. Der Unterschied zwischen zwei Minuten Refactoring und zwei Stunden Fehlersuche liegt fast immer in dem, was du weggelassen hast.
  4. Plan Mode vor jeder Änderung erzwingen: Lass die betroffenen Komponenten scannen und einen Ausführungsplan schreiben, bevor eine Zeile Code entsteht. Eine schlechte Architekturentscheidung in einer Stichpunktliste zu kippen dauert Sekunden, dieselbe Entscheidung in einem 500-Zeilen-Diff zu finden dauert den Nachmittag.

Im Git-Blame steht nie der Name des Modells.

Häufig gestellte Fragen (FAQ)

Was bedeutet Code Ownership bei KI-generiertem Code?

Ownership bedeutet, dass du jede Zeile in einer Datei fachlich begründen kannst: warum sie existiert, welche Grenzfälle sie abdeckt und welche Annahme sie über die Geschäftslogik trifft. Lauffähigkeit und eine grüne Pipeline sind kein Ersatz dafür. Wenn Implementierung und zugehörige Tests aus derselben Generierung stammen, belegen bestandene Tests nur Konsistenz, nicht Korrektheit.

Wie steuert man ein LLM, statt seinen Output nur zu beaufsichtigen?

In drei getrennten Schritten. Zuerst nur die Datenstrukturen anfordern, also Union-Types, Interfaces und Konstanten-Tabellen ohne jede Logik. Danach isolierte, seiteneffektfreie Funktionen, die sich einzeln testen lassen. Zuletzt einen Orchestrator, der diese Funktionen zusammensetzt und explizit die Grenzfälle behandelt. Jeder Schritt ist einzeln prüfbar, bevor der nächste darauf aufbaut.

Warum ist eine AGENTS.md für KI-gestützte Entwicklung sinnvoll?

Ohne dokumentierte Leitplanken behandelt ein Modell jede Codebasis nach dem Durchschnitt seiner Trainingsdaten. Eine AGENTS.md im Projektwurzelverzeichnis hält Architekturregeln, Stilvorgaben und nicht verhandelbare Einschränkungen fest, etwa das Sprachlevel oder verbotene Muster. Sie wirkt bei jeder Interaktion, statt in jedem Prompt neu formuliert werden zu müssen.

Autor hyretic

Senior Full-Stack Developer mit Fokus auf stabiler Software-Architektur, pragmatischem Engineering und der Realität von KI im Entwickler-Alltag. Seine Wurzeln liegen im praktischen Lösen komplexer Probleme unter realen Bedingungen.

github.com/hyretic-dev

Ähnliche Artikel