Nicolai Wolko
{{imageAlt}}

Vibe Coding vs. Clean Code: Warum KI-Code nicht trägt

Veröffentlicht am 17. September 2025 - ⏱️ Ca. (wird berechnet) min. Lesezeit

Vibe Coden ist noch immer der letzte Schrei. Doch langsam zeigt sich, dass das Ergebnis selten über den Status eines Prototyps hinauskommt. Abhelfen sollen nun neue Jobbezeichnugen, wie die sogenannten Vibe Coding Cleanup Specialists. Ironisch, denn im Kern ist das nichts anderes als klassische Software-Entwicklung – genau die Disziplin, die angeblich durch KI ersetzt werden sollte.

Warum also scheitert dieser Ansatz? Wieso entsteht Code, der schon nach kurzer Zeit brüchig wird? Und gibt es Aussicht, dass sich das bald ändert? Ist das nur ein Zwischenstadium, auf dem unaufhaltbaren Siegeszug des Vibe Codens?

Um das zu verstehen, lohnt sich ein Blick auf einen altbekannten Begriff: Clean Code. Jahrzehntelang haben wir in der Software-Entwicklung darüber gesprochen und gestritten. Nicht ohne Grund.

Zurück auf Anfang: Clean Code

Der Begriff „Clean Code“ wird seit Jahren strapaziert und oft missverstanden. Gemeint ist nicht ästhetischer Code, der besonders kurz oder besonders elegant wirkt. Clean Code beschreibt eine Haltung: Quelltext soll so geschrieben sein, dass er von Menschen verstanden und über lange Zeit hinweg sicher verändert werden kann.

Damit rückt ein zentrales Ziel in den Vordergrund: Wartbarkeit. Die eigentlichen Kosten in der Software-Entwicklung entstehen nicht beim Neubau, sondern im Betrieb. Anpassungen an bestehendem Code machen den grössten Teil der Arbeit aus. Jede Zeile, die heute schnell und unsauber entsteht, erhöht morgen die Aufwände exponentiell. Clean Code ist deshalb kein Luxus, sondern ein ökonomisches Prinzip.

Und das ist keine Meinung. Das ist ein gut untersuchter Befund, den zahlreiche Studien der letzten Jahrzehnte untermauern. Bereits klassische Analysen aus den 1980er- und 1990er-Jahren kamen zu dem Ergebnis, dass zwischen 50 und 80 Prozent der Gesamtkosten eines Softwaresystems auf Wartung entfallen. Neuere Arbeiten bestätigen diese Grössenordnung oder gehen sogar noch darüber hinaus.

Eine Untersuchung von Dehaghani et al. (2013) schätzt, dass bis zu 90 % der Lebenszykluskosten auf Wartung und Weiterentwicklung entfallen (PMC Study). Auch eine Metaanalyse verschiedener Modelle zur Aufwandsschätzung kommt auf denselben Nenner: der Betrieb verschlingt die meiste Zeit und die meisten Ressourcen (ResearchGate).

Noch deutlicher formuliert es ein Report der Universität Joensuu: In einzelnen Projekten entfielen über 90 % der Aufwände auf Wartung und Evolution – Neubau spielte dagegen nur eine untergeordnete Rolle (SM Costs PDF). Selbst praxisnahe Industriequellen wie O’Reilly oder Vention veranschlagen den Wartungsanteil routinemässig auf 60 % und mehr.

Die Botschaft ist eindeutig: Wer Clean Code ignoriert, spart vielleicht Tage im Projektstart, zahlt aber Jahre in der Wartung. Je unsauberer die Basis, desto steiler steigen die Folgekosten – nicht linear, sondern exponentiell. Denn schlechte Strukturen breiten sich aus. Jede neue Änderung muss gegen Altschulden ankämpfen, jede Erweiterung verschärft die Kopplung. Am Ende wird nicht das Feature teuer, sondern das Verstehen.

Was sind also die Kernprinzipien von Clean Code, die genau diese Wartungskosten reduzieren sollen?:

  • Lesbarkeit vor Cleverness: Code ist in erster Linie Kommunikation. Ein klarer, verständlicher Ansatz ist wertvoller als eine trickreiche Abkürzung, die niemand mehr durchschaut.
  • Kleine, fokussierte Einheiten: Klassen, Module und Funktionen sollten genau eine Aufgabe haben. Das erleichtert das Testen und verringert das Risiko unbeabsichtigter Nebenwirkungen.
  • Explizite Logik: Was passiert, muss im Code sichtbar sein. Versteckte Abhängigkeiten und implizite Seiteneffekte sind Hauptquellen für Fehler.
  • Konsistenz: Gemeinsame Regeln und Konventionen verhindern, dass ein Projekt wie ein Flickenteppich wirkt. Wer eine Codebasis liest, sollte den Eindruck haben, sie stamme von einer einzigen Hand.
  • Refactoring als Daueraufgabe: Sauberer Code entsteht nicht beim ersten Schreiben, sondern durch kontinuierliches Überarbeiten. Pflege ist ein Prozess, kein einmaliges Ereignis.

Diese Prinzipien sind entscheidend für die Lebensdauer von Software. In Projekten, die Clean-Code-Praktiken ernst nehmen, bleiben Änderungen lokalisierbar. Risiken sind überschaubar. Neue Entwickler finden sich schneller ein. In Projekten ohne diese Sorgfalt wächst dagegen ein fragiles Gebilde, das schon bei kleinen Eingriffen bricht.

Warum KI das nicht liefert

KI-gestütztes Coden optimiert nicht auf Wartbarkeit, sondern auf unmittelbare Funktionsfähigkeit. Das Ziel der Modelle ist es, eine plausible, syntaktisch korrekte Lösung zu erzeugen. Architektur, Intentionalität und langfristige Verständlichkeit spielen dabei keine Rolle. Clean Code ist ein menschenzentriertes Konzept – es reduziert kognitive Last und schafft Orientierung für künftige Änderungen.

Hinzu kommt die eingeschränkte Sicht. Agenten arbeiten lokal, mit dem Kontext eines Prompts oder einzelner Dateien. Systeme bestehen jedoch aus globalen Entscheidungen: Schnittstellen, Abhängigkeiten, Teamkonventionen, regulatorische Vorgaben. Diese Informationen sind weder im Trainingsmaterial vollständig enthalten noch im Einzelfall für die KI verfügbar. Das Ergebnis ist funktionaler, aber fragiler Code, der oft schon bei kleinen Änderungen bricht.

Ein Beispiel verdeutlicht den Unterschied. Angenommen, ein Agent beschäftigt sich via Vibe Coding in mehrere Iterationen mit der Gestaltung eines Rabatts in einem Shop. Typischerweise sieht das sehr schnell dann so aus:

# pricing.py
import time
import json
import logging
from decimal import Decimal

LOGGER = logging.getLogger("pricing")
LOGGER.setLevel(logging.DEBUG)

# TODO: proper cache later?
# naive process-wide cache (never cleared)
_DISCOUNT_CACHE = {}

# legacy helper (kept "just in case")
def _normalize_price(p):
    try:
        return Decimal(str(p))
    except Exception:
        return Decimal(0)  # should never happen

# unused: was for a future A/B test
def _exp_giveaway_code(user_id: str) -> str | None:
    if len(user_id) % 7 == 0:
        return "WOW7"
    return None

# copied from an older notebook (slightly different rounding!)
def _apply_percent(price: Decimal, percent: Decimal) -> Decimal:
    return (price - (price * percent / Decimal(100))).quantize(Decimal("0.01"))

# another variant introduced later (no quantize, different rounding)
def _apply_percent2(price: Decimal, percent: Decimal) -> Decimal:
    return price * (Decimal(1) - percent / Decimal(100))

# feature flag – kept global for now
ENABLE_COUPONS = True

def calc_final_price(item_id, base_price, percent_discount=None, coupon_code: str | None = None):
    """
    Compute final price with optional percent and coupon.
    NOTE: currencies not handled yet
    """
    # quick win: cache by (item_id, percent_discount, coupon_code) – stringify for dict key
    key = json.dumps([str(item_id), str(percent_discount), str(coupon_code)])
    if key in _DISCOUNT_CACHE:
        LOGGER.debug("cache hit for %s", key)
        return _DISCOUNT_CACHE[key]  # returns Decimal sometimes, float other times

    p = _normalize_price(base_price)

    # hotfix: negative price guard
    if p < 0:
        LOGGER.warning("negative base price %s - forcing to zero", p)
        p = Decimal(0)

    # apply percent if present (use newer impl but keep old for reference)
    if percent_discount:
        try:
            if Decimal(percent_discount) > 100:
                LOGGER.info("capping percent at 100")
                percent_discount = Decimal(100)
            # mixed rounding between variants – kept due to a prod issue last sprint
            p = _apply_percent2(p, Decimal(percent_discount))
            # p = _apply_percent(p, Decimal(percent_discount))  # legacy, do not delete yet
        except Exception as e:
            LOGGER.error("percent error: %s", e)

    # coupons (temporary list, should move to DB later)
    if ENABLE_COUPONS and coupon_code:
        if coupon_code.upper() in ["SAVE10", "WELCOME", "WOW7"]:  # TODO move to config
            p = p - Decimal("10.00")
        elif coupon_code.startswith("XMAS"):
            # special deal, magic number from marketing deck
            p = p * Decimal("0.85")

    # hotfix for negative totals after coupons
    if p < 0:
        p = Decimal("0.00")

    # log time (debugging prod latency spike)
    t0 = time.time()
    # simulate some slow IO that used to be here
    if str(item_id).endswith("9"):
        time.sleep(0.001)
    LOGGER.debug("calc took %.2fms", (time.time() - t0) * 1000)

    # NOTE: currency handling TBD; quantize sometimes applied earlier
    result = p  # may be unquantized Decimal

    _DISCOUNT_CACHE[key] = result  # cache forever (no invalidation)
    return result

# dead convenience alias (used once in tests long ago)
def get_price(x, d=None, c=None):
    return calc_final_price(x, x, d, c)

Das ist nicht übertrieben, das ist Agenten Code in der Realtität. Besser wirds nur, wenn ein Software Entwickler dem klare Leitplanken vorgibt und aktiv den Code liest und mit entwickelt. Was genau läuft denn hier schief? Schauen wir uns vlt zuerst den Code an, den wir eigentlich gewollt hätten:


from decimal import Decimal

def apply_discount(price: Decimal, percent: Decimal) -> Decimal:
    """Apply a percentage discount to a price. Percent must be between 0 and 100."""
    if not (Decimal(0) <= percent <= Decimal(100)):
        raise ValueError("Discount percent must be between 0 and 100")
    return price * (Decimal(1) - percent / Decimal(100))

Das ist alles. Und das ist typisch. KI erzeugt MASSEN von Code und schlingert um das herum, was eigentlich gewollt war. Und oftmals sieht man noch Fragmente von Ideen, oder anderen Features die mal angedacht oder ausprobiert wurden. Versuchen wir das Durcheinander systematisch einzuordnen:

  • Duplikate und Fragmentierung: _apply_percent und _apply_percent2 berechnen dasselbe mit unterschiedlicher Rundung. Solche Divergenzen führen zu nicht reproduzierbaren Preisen und schwer erklärbaren Abweichungen.
  • Code-Leichen: _exp_giveaway_code, der Alias get_price und kommentierte „Legacy“-Zeilen bleiben aus Unsicherheit liegen. Leichen erhöhen Lesekosten und verbergen fachliche Absichten.
  • Globaler Zustand: ENABLE_COUPONS als Feature-Flag und der Prozess-Cache _DISCOUNT_CACHE sind global. Beides erschwert Tests, erzeugt Reihenfolge-Abhängigkeiten und verhindert gezielte Invalidierung.
  • Magische Zahlen und Inline-Konfiguration: Gutscheinbeträge und Codes im Code statt in einer Konfiguration ("SAVE10", "0.85"). Änderungen erfordern Code-Deploys, nicht Settings-Updates.
  • Inkonsequente Typen und Rundung: Rückgaben wechseln zwischen Decimal und float; mal wird quantisiert, mal nicht. Preise müssen deterministisch rundbar sein, sonst kippen Summen über Warenkörbe hinweg.
  • Gemischte Verantwortlichkeiten: Logging, Timing-Messung, Feature-Flags, Caching und Fachlogik in einer Funktion. Das verhindert gezielte Tests und Wiederverwendung.
  • Schwammige Domäne: Währung wird ignoriert, Grenzwerte für Rabatte sind nur teilweise abgesichert. Invarianten fehlen.
  • Still und heimlich gewachsene Pfade: Sonderfälle wie item_id.endswith("9") stammen aus Debug-Phasen und bleiben hängen. Solche Artefakte sind Produktionsrisiko.
  • Fehlende Testbarkeit: Es gibt keinen definierten, schmalen Kern, den Tests abdecken könnten. Ohne klare API-Kanten entstehen Lücken in der Absicherung.

Trimmen ist möglich – aber begrenzt

Natürlich lassen sich Agenten ein Stück weit zähmen. Mit Linter, Formatter und Styleguides verschwindet ein Teil der Oberflächenprobleme. Tests und Metriken wie zyklomatische Komplexität oder Duplication finden Schwachstellen, bevor sie in Produktion kippen. Architekturtests können verhindern, dass ein Datenbank-Layer plötzlich direkt auf die UI zugreift. All das hebt die Basis und reduziert den Wildwuchs.

Trotzdem bleibt die Grenze klar: Werkzeuge prüfen formale Eigenschaften, nicht Absichten. Ob ein Rabatt in einer Preisfunktion auf zwei Dezimalstellen gerundet oder in einem zentralen Money-Objekt gekapselt werden sollte, ist keine Frage für Linter-Regeln. Ob ein Feature-Flag global erlaubt ist oder in einem Feature-Toggle-System liegen muss, entscheidet die Architektur – nicht ein Prompt. Agenten kennen diese Kontexte nicht.

Praktisch bedeutet das: Mit klaren Leitplanken lassen sich viele Fallstricke vermeiden. Ohne Leitplanken wird Vibe Code zum Flickenteppich, der schon nach kurzer Zeit teure Korrekturen erzwingt. KI bleibt damit ein Praktikant, der Fleissarbeit zuverlässig übernimmt, aber keine Architektur trägt.

"Aber KI erfindet sich täglich neu und wird diese Probleme bald lösen"

Bleibt noch die Frage vom Anfang: Handelt es sich nur um eine Übergangsphase, die sich mit besserer Technologie von selbst erledigt? Oder zeigt sich hier ein Grundproblem von KI in der Softwareentwicklung?

Kurzfristig wird die Situation besser. Grössere Kontextfenster, Projektspeicher, bessere Integrationen in Entwicklungsumgebungen und automatisierte Quality Gates reduzieren einige der sichtbarsten Schwächen. Oberflächliche Probleme wie inkonsistente Formatierung oder einfache Duplikate lassen sich technisch beheben.

Die Grundsatzfrage bleibt jedoch bestehen. Wartbarkeit ist ein menschenzentriertes Ziel: Sie reduziert kognitive Last, macht Absichten sichtbar und erhält Entscheidungen. Ein Sprachmodell hat dafür weder eigenes Verständnis noch einen Anreiz. Es optimiert auf unmittelbare Plausibilität, nicht auf langfristige Architektur. Genau darin liegt die Grenze.

Die Rolle des Entwicklers

Entwickeln bedeutet mehr als Code erzeugen. Systeme brauchen klare Linien, sprechende Namen und Grenzen, die auch unter Druck halten. Sie brauchen Abstraktionen, die nicht nur heute, sondern auch in einem Jahr tragfähig sind. Diese Entscheidungen sind das Kerngeschäft des Entwicklers. Sie lassen sich nicht an ein Sprachmodell delegieren.

Agenten sind stark, wenn Fleissarbeit dominiert: Boilerplate-Code, einfache Refactorings, Tests für offensichtliche Randfälle, Generierung von Datenstrukturen oder Migrationsskripten. Sie helfen bei Recherchen in Dokumentationen oder beim Verdichten langer Diskussionen. Genau hier liegt der Nutzen.

Was sie nicht leisten können, ist die Rolle des Software Entwicklers. Clean Code bleibt damit der Massstab – nicht als nostalgisches Dogma, sondern als ökonomische Notwendigkeit. Vibe Coden ist einstweilen eine Totgeburt. Zu Scheitern verurteilt.

Daraus ergibt sich ein einfaches Muster: Prompt zu Commit ist tabu. Jeder Vorschlag der KI muss gelesen, geprüft und in den Kontext eingeordnet werden. Das bedeutet kleine Schritte: Vorschlag generieren, prüfen, vereinfachen, benennen, Nebenwirkungen sichtbar machen, Tests ergänzen, Entscheidung dokumentieren. Erst danach gehört der Code ins Repository.

Dieser Zyklus ist nicht bürokratisch, sondern ökonomisch. Er verhindert, dass Systeme in kürzester Zeit in technische Schuld kippen. Und er sorgt dafür, dass Geschwindigkeit nicht auf Kosten der Wartbarkeit erkauft wird. Und ja. Damit endet auch der Traum, dass KI die Softwareentwicklung revolutioniert. Sie verbessert. Aber sie stellt es nicht auf den Kopf.

Fazit

Vibe Coding liefert schnelle Ergebnisse, aber selten tragfähige Systeme. Was in Demos beeindruckt, erweist sich in der Praxis als Quelle technischer Schuld. Dass es inzwischen eigene „Cleanup Specialists“ braucht, ist weniger ein Beweis für eine neue Disziplin, sondern eine Erinnerung an alte Prinzipien: Clean Code bleibt das Fundament professioneller Softwareentwicklung.

Kurzfristig wird die Technik besser. Die Grundsatzfrage bleibt jedoch bestehen: Wartbarkeit ist ein menschenzentriertes Ziel. Ein Sprachmodell kennt diese Dimensionen nicht und hat keinen Anreiz, sie einzuhalten.

Damit beantwortet sich die Eingangsfrage: Vibe Coding ist keine Episode, die sich von selbst auswächst. Die Grenzen liegen im Prinzip der Technologie. KI kann unterstützen, beschleunigen und Fleissarbeit übernehmen – mehr nicht. Wer sie als Werkzeug versteht, gewinnt viel. Wer sie als Ersatz einsetzt, produziert Systeme, die laufen, aber nicht tragen.