Plausibler Code statt korrektem Code: Warum du KI-generiertem Code nicht blind vertrauen solltest
LLMs schreiben Code, der kompiliert und auf den ersten Blick funktioniert – aber systematisch falsch ist. Wie du Acceptance Criteria nutzt, um das zu verhindern.
Ein LLM-generiertes SQLite-Rewrite in Rust. 576.000 Zeilen Code, saubere Architektur, alle Tests grün, das README verspricht MVCC und Dateikompatibilität. Auf den ersten Blick ein solides Projekt. Der Benchmark zeigt dann: 20.171-mal langsamer als das Original bei einer simplen Primary-Key-Abfrage. Kein Tippfehler. Zwanzigtausend Mal.
Dieser Fall ging diese Woche auf Hacker News viral und trifft einen Nerv, den viele Entwickler spüren, aber selten aussprechen: LLMs schreiben plausiblen Code – nicht korrekten Code. Und der Unterschied zwischen plausibel und korrekt kann den Unterschied zwischen einem funktionierenden Produkt und einer Katastrophe ausmachen.
Als Webentwickler, der KI-Tools täglich im Einsatz hat, muss ich sagen: Dieser Weckruf kommt zur richtigen Zeit. Denn die Verlockung, dem Output zu vertrauen, wird mit jedem Modell-Update größer – und die Risiken auch.
Das Kernproblem: Plausibilität ist nicht Korrektheit
LLMs sind statistische Sprachmodelle. Sie optimieren auf die wahrscheinlichste nächste Token-Sequenz. Das bedeutet: Sie produzieren Code, der aussieht wie korrekter Code, der kompiliert und sogar einfache Tests besteht.
Aber „sieht aus wie korrekt” ist fundamental etwas anderes als „ist korrekt”. Im SQLite-Rewrite hatte das LLM eine B-Tree-Implementierung generiert, die alle richtigen Methodennamen hatte und sogar das SQLite-Dateiformat korrekt lesen konnte. Nur: Die Indexierung war komplett kaputt. Statt O(log n) Lookups machte das System lineare Scans – unsichtbar unter einer korrekten API-Oberfläche.
Das ist kein Bug im herkömmlichen Sinne. Das ist ein Architekturversagen hinter einer plausiblen Fassade. Du siehst es nicht, wenn du nicht gezielt danach suchst.
Warum LLMs systematisch diesen Fehler machen
Der Grund liegt in der Natur des Trainings. LLMs haben Millionen von Code-Beispielen gesehen, aber sie verstehen nicht, warum Code funktioniert. Sie erkennen Patterns: Eine B-Tree-Klasse hat Methoden wie insert, delete, search. Ein Datenbank-Pager verwaltet Seiten mit fester Größe. WAL-Implementierungen haben einen Header und Frames.
Diese Pattern-Erkennung reicht für strukturell korrekt aussehenden Code. Aber die subtilen Invarianten – dass ein B-Tree seine Balance halten muss, dass Cache-Eviction die Performance bestimmt, dass WAL-Checkpointing atomisch sein muss – das „versteht” kein LLM. Es repliziert die Form, nicht die Funktion.
In meinem Artikel über Vibe Coding habe ich schon die Grenzen beschrieben. Aber was wir jetzt sehen, geht darüber hinaus: nicht nur „KI macht Fehler”, sondern systematische Fehler, die durch die Natur der Technologie entstehen.
Die Acceptance-Criteria-Methode: Erst definieren, dann generieren
Die vielleicht wichtigste Erkenntnis aus der aktuellen Debatte ist simpel, aber wird konsequent ignoriert: Definiere deine Akzeptanzkriterien, bevor die erste Zeile Code generiert wird.
Das klingt nach Projektmanagement-Binsenweisheit, aber im Kontext von KI-generiertem Code bekommt es neue Bedeutung. Sagst du „Schreib mir eine Caching-Lösung”, bekommst du etwas, das nach Caching aussieht. Sagst du stattdessen:
- Response-Zeiten unter 50ms für gecachte Anfragen
- Cache-Hit-Rate über 90 % nach Warmup
- Korrektes Invalidieren bei Daten-Updates
- Maximaler Speicherverbrauch 512 MB
- Kein Stale Data älter als 30 Sekunden
…dann hast du messbare Kriterien, gegen die du den generierten Code verifizieren kannst. Und genau darum geht es: Von „sieht gut aus” zu „beweisbar funktionsfähig” zu kommen.
So sieht das in der Praxis aus
Ich habe meine eigene Arbeitsweise in den letzten Monaten angepasst. Bevor ich ein LLM Code schreiben lasse, erstelle ich ein kurzes Acceptance-Criteria-Dokument. Bei einem aktuellen WordPress-Plugin-Projekt sah das so aus:
Feature: REST-API-Endpoint für Produktsuche
Akzeptanzkriterien:
- Antwortzeit unter 200ms bei 10.000 Produkten
- Korrekte Pagination mit
total,page,per_pageHeadern - SQL-Injection-sicher durch Prepared Statements
- Keine N+1 Queries – maximal 3 Datenbankabfragen pro Request
- Korrekte HTTP-Status-Codes (200, 400, 404, 500)
- Rate-Limiting: Maximal 60 Requests pro Minute pro API-Key
Jedes dieser Kriterien ist testbar. Und tatsächlich: Als ich den LLM-generierten Code gegen diese Kriterien geprüft habe, hat er bei Punkt 4 versagt. Der Code nutzte eine Schleife mit einzelnen Queries pro Produkt für Metadaten statt eines JOINs. Plausibel? Absolut – der Code las sich sauber. Korrekt? Nicht im Ansatz performant.
Ohne die definierten Kriterien hätte ich das wahrscheinlich beim Code-Review übersehen, weil der Code richtig aussah. Und genau das ist der Punkt.
Die gefährlichsten Bereiche für plausiblen Code
Nicht alle Code-Bereiche sind gleich anfällig. In meiner Erfahrung gibt es klare Risikozonen, in denen LLM-generierter Code besonders häufig plausibel, aber falsch ist:
1. Performance-kritischer Code
LLMs optimieren nicht auf Performance, sondern auf Lesbarkeit. Sie generieren Code, der funktional korrekt ist, aber algorithmisch suboptimal. Das SQLite-Beispiel ist ein Extrem, aber in der Webentwicklung sehe ich das ständig: O(n²) Schleifen statt Hash-Maps, unnötige Datenbankabfragen, fehlende Indizes in Migrations.
2. Sicherheitsrelevanter Code
Hier wird es richtig gefährlich. Ein LLM generiert Authentifizierung, die sicher aussieht, aber subtile Timing-Angriffe ermöglicht. Oder CSRF-Schutz, der in 95 % der Fälle funktioniert, aber bei einem Edge Case umgangen werden kann. OpenAI hat das Problem erkannt: Ihr neuer Codex Security Agent scannt Repositories nach genau solchen subtilen Schwachstellen. In der Betaphase reduzierte er False Positives um über 50 Prozent und fand 792 kritische Schwachstellen in einer Million Commits. Selbst die KI-Hersteller wissen, dass ihr Code-Output nicht vertrauenswürdig genug ist.
3. Nebenläufigkeit und State-Management
Concurrency-Bugs sind die Hölle für Menschen, und LLMs machen es nicht besser. Race Conditions, Deadlocks, Stale State – alles Dinge, die bei einzelnen Tests nicht auffallen, aber unter Last explodieren. Bei Node.js-Backends mit async/await sehe ich regelmäßig Code, der bei einem Request perfekt funktioniert, aber bei zehn gleichzeitigen inkonsistente Daten liefert.
4. Edge Cases und Fehlerbehandlung
LLMs sind trainiert auf den Happy Path. Fehlerbehandlung wird als Nachgedanke generiert: generisches try/catch, Standardfehlermeldungen, keine Retry-Logik. Aber genau die Edge Cases machen in Produktion Probleme. Was passiert bei Timeout? Bei ungültiger Eingabe? Bei einem Netzwerkfehler mitten in einer Transaktion?
Werkzeuge und Strategien für die Verifikation
Acceptance Criteria sind der erste Schritt. Aber sie nützen nichts ohne die Werkzeuge, um sie zu überprüfen. Hier ist mein aktueller Stack für die Verifikation von KI-generiertem Code, den ich nach und nach aufgebaut habe:
Automatisierte Benchmarks
Für jede Performance-Anforderung schreibe ich vor dem eigentlichen Code einen Benchmark. In Laravel nutze ich PHPBench, in Node.js benchmark.js oder die eingebaute performance.mark() API. Der Benchmark ist das Akzeptanzkriterium in ausführbarer Form.
Property-Based Testing
Statt nur einzelne Beispiele zu testen, definiere ich Eigenschaften, die für alle Eingaben gelten müssen. Tools wie fast-check für JavaScript oder PHPUnit mit Daten-Providern helfen dabei. Wenn das LLM eine Sortier-Funktion generiert, teste ich nicht nur [3,1,2] → [1,2,3], sondern: „Für jede beliebige Liste ist das Ergebnis sortiert und enthält dieselben Elemente.”
Mutation Testing
Ein unterschätztes Werkzeug: Mutation Testing ändert bewusst Code-Stellen und prüft, ob die Tests das erkennen. Wenn du eine Bedingung von < auf <= ändern kannst und kein Test fehlschlägt, sind deine Tests nicht gut genug. Für PHP gibt es Infection, für JavaScript Stryker.
Code-Review mit Fokus auf Invarianten
Wenn ich LLM-generierten Code reviewe, frage ich mich gezielt: Welche Invariante muss hier gelten? Ist sie garantiert? Was passiert im Fehlerfall? Wie skaliert das? In meinem Post über KI-Agenten im Code-Review habe ich beschrieben, wie automatisierte Tools dabei helfen – aber die menschliche Prüfung auf Architektur-Ebene bleibt unersetzbar.
Was heißt das für die Zukunft von AI Coding?
Die aktuelle Diskussion ist kein Argument gegen KI im Coding – sie ist ein Argument für einen reiferen Umgang damit. Die Honeymoon-Phase ist vorbei. Was jetzt kommt, ist die Phase der Professionalisierung.
In meinem Artikel über KI-Agenten für Testing und Debugging habe ich bereits beschrieben, wie KI-Tools selbst Teil der Qualitätssicherung werden. Ein Agent schreibt Code, ein anderer testet ihn, ein dritter prüft Security – das funktioniert schon heute mit Tools wie Cursor, Copilot Workspace oder Claude Code.
Die Rolle des Entwicklers verändert sich
Was sich fundamental ändert, ist nicht ob wir KI beim Programmieren nutzen, sondern wie. Der naive Ansatz – „generiere mir das Feature” – funktioniert für Prototypen. Für Produktion brauchst du einen systematischeren Ansatz:
- Spezifikation schreiben (Acceptance Criteria, Constraints, Edge Cases)
- Tests vorher definieren (TDD, aber der Mensch schreibt die Tests, die KI den Code)
- Code generieren lassen (mit den Spezifikationen als Kontext)
- Automatisiert verifizieren (Benchmarks, Tests, Mutation Testing)
- Architektur-Review (Mensch prüft die nicht-funktionalen Eigenschaften)
Dieser Workflow ist aufwändiger als blindes Copy-Paste aus ChatGPT. Aber er liefert zuverlässigere Ergebnisse als manuelles Programmieren allein und schnellere als traditionelle Entwicklung ohne KI.
Mein persönliches Fazit
Ich habe meinen Workflow in den letzten Wochen bewusst umgestellt: KI nicht als „schnellerer Programmierer”, sondern als „erster Entwurf mit Pflichtverifikation”. Der mentale Shift klingt klein, macht aber einen gewaltigen Unterschied.
Das SQLite-Rewrite ist ein Extremfall. In der täglichen Webentwicklung sind die Fehler subtiler: ein vergessener Index, fehlerhaftes Caching, eine Race Condition beim Checkout. Aber das Muster ist dasselbe – der Code sieht richtig aus, weil das LLM gelernt hat, wie richtiger Code aussieht, nicht weil es versteht, warum er richtig ist.
Die gute Nachricht: Wir haben die Werkzeuge, um das zu kompensieren. Vertraue dem Code nicht, weil er gut aussieht. Vertraue ihm, weil er bewiesen hat, dass er funktioniert. Das ist der Unterschied zwischen Vibe Coding und professioneller Softwareentwicklung mit KI-Unterstützung.
Der Hype um KI-generiertes Programmieren ist berechtigt. Aber Hype ohne Verifikation ist nur ein weiterer Weg, technische Schulden aufzubauen. Und die zahlen wir alle zurück – mit Zinsen.