Ich will wieder Code von Hand schreiben
(blog.k10s.dev)- k10s war ein GPU-aware Kubernetes-TUI, das per Vibe-Coding mit Claude schnell gebaut wurde, aber nach dem Hinzufügen der Fleet-Ansicht mehrere Bildschirmzustände beschädigte
model.gowuchs auf ein einzelnesModelmit 1690 Zeilen und einerUpdate()-Funktion mit 500 Zeilen an und trug dadurch gleichzeitig UI, Client, Cache, Navigation und View-Status- Die KI fügte Features zwar schnell hinzu, vergrößerte aber zugleich ein god object und einen globalen Key-Handler, sodass mit jeder neuen View weitere Branches in bestehenden Handlern entstanden
- Positionsbasierte
[]string-Daten und direkte Mutation in Hintergrund-tea.Cmdkonnten Spaltenfehler und offensichtliche Data Races verursachen - Das neue k10s wird in Rust neu geschrieben; noch vor dem ersten Prompt sollen Interface, Message-Typen, Ownership-Regeln und Scope in CLAUDE.md festgeschrieben werden
Warum k10s neu geschrieben wird
- k10s begann als GPU-aware Kubernetes-Dashboard und war als TUI-Tool dafür gedacht, dass Betreiber von NVIDIA-Clustern Informationen wie GPU-Auslastung, DCGM-Metriken, idle Nodes und Kosten von
$32/hrsofort sehen können - Es wurde mit Go und Bubble Tea geschrieben und in etwa 7 Monaten, 234 Commits und ungefähr 30 Wochenenden in Vibe-Coding-Sessions mit Claude erstellt
- Anfangs funktionierten grundlegende k9s-Klon-Features wie Pods, Nodes, Deployments, Services, Command Palette, Watch-basierte Live-Updates und Vim-Keybindings schon nach etwa 3 Wochenenden
- Das Kernfeature, die GPU-Fleet-Ansicht, war ein Bildschirm, der pro Node GPU-Zuweisung, Auslastung, DCGM-basierte Metriken, Temperatur, Stromverbrauch, Speicher und farbbasierte Zustände zeigte; Claude erzeugte dabei in einem Schritt die
FleetView-Struktur, GPU-/CPU-/All-Tab-Filterung und sogar das Rendering der Allocation Bars - Nach dem Hinzufügen der Fleet-Ansicht war beim Zurückkehren zur Pods-Ansicht mit
:rs podsdie Tabelle leer, Live-Updates stoppten, in der Nodes-Ansicht erschienen veraltete Daten aus dem Fleet-Filter und auch die Fleet-Tab-Zählung war falsch - Beim Nachverfolgen des Problems wurde erstmals die gesamte von Claude erzeugte
model.gomit 1690 Zeilen gelesen; eine einzelneModel-Struktur hielt UI-Widgets, Kubernetes-Client, Logs-/Describe-/Fleet-Status, Navigationshistorie, Cache und Mouse-Handling zugleich - Die Methode
Update()war einemsg.(type)-Dispatch-Funktion mit 500 Zeilen und 110 switch/case-Branches - KI kann Features schnell bauen, aber wenn man ihr ohne Einschränkungen immer weiter Aufgaben überlässt, kollabiert die Architektur, und das Geschwindigkeitsgefühl sieht bis kurz vor dem gleichzeitigen Zusammenbruch des Ganzen wie Erfolg aus
Fünf Prinzipien aus den Trümmern
-
Prinzip 1: KI baut Features, aber keine Architektur
- Claude konnte einzelne Features wie Fleet-Ansicht, Log-Streaming und Mouse-Support gut bauen, aber jedes Feature wurde im Kontext „es muss jetzt funktionieren“ umgesetzt und ohne die Beziehungen zu anderen Features zu berücksichtigen, die denselben Zustand teilen
- Im Handler für
resourcesLoadedMsgstanden Bedingungen wiemsg.gvr.Resource == k8s.ResourceNodes && m.fleetView != nil, wodurch Fleet-spezifische Logik in den generischen Pfad zum Laden von Ressourcen gemischt wurde - Immer wenn eine neue View spezielles Verhalten brauchte, kam ein weiterer Branch in denselben Handler, und damit keine Daten aus alten Views in neue Views ausliefen, mussten mehrere Felder manuell geleert werden
- In
model.gowaren 9 manuelle Cleanups wiem.logLines = nil,m.allResources = nil,m.resources = nilverstreut; wenn nur eines fehlte, blieben Ghost-Daten der vorherigen View zurück - Die Alternative ist, vor dem Schreiben von Code konkrete Interfaces, Message-Typen und Ownership-Regeln selbst zu definieren und als Architektur-Invarianten in
CLAUDE.mdfestzuhalten - Beispielregeln wären etwa: Jede View implementiert ein
View-Trait/Interface, keine View greift auf den State einer anderen View zu, asynchrone Daten kommen nur überAppMsg-Varianten herein, und dieApp-Struktur ist nur für Navigation und Message-Dispatch zuständig
-
Prinzip 2: Das god object ist das Standardprodukt von KI
- KI neigte zu einer Struktur, in der ein einziges Struct alles enthält, weil damit der unmittelbare Prompt mit dem geringsten Zeremoniell erfüllt werden konnte
- Auch das Key-Handling war nicht pro View getrennt; dieselbe Taste
sbedeutete in der Logs-View Autoscroll, in der Pods-View Shell und in der Containers-View Container-Shell - Die Anfrage „Shell-Support zu Pods hinzufügen“ wurde umgesetzt, indem in der Nähe des bestehenden globalen Key-Handlers einfach ein Branch eingefügt wurde
- Auch die
Enter-Taste verzweigte zwischen Contexts-View, Namespaces-View, Logs-View und generischer Drill-down-Logik innerhalb eines einzigen flachen Dispatches über String-Vergleiche mitm.currentGVR.Resource - Innerhalb der einen Datei
model.gowurdem.currentGVR.Resource ==mehr als 20 Mal wie ein Typ-Diskriminator verwendet, und jedes Hinzufügen einer neuen View machte Änderungen in mehreren Handlern nötig - Die Alternative ist, keine view-spezifischen State-Felder in
App/Modelhinzuzufügen, jede View als eigenes Struct zu bauen und auch die Keybindings in der Keymap der aktiven View zu halten; diese Regel gehört inCLAUDE.md - Es braucht Guardrails wie „Eine neue View muss ein neues File sein; wenn dafür bestehende Views geändert werden müssen, stoppen und nachfragen“, damit die KI nicht einfach den kürzesten Weg über weitere Branches nimmt
-
Prinzip 3: Die Illusion von Geschwindigkeit erweitert den Scope
- k10s war ursprünglich ein Tool für eine enge Zielgruppe, nämlich Betreiber von GPU-Training-Clustern, aber Vibe-Coding ließ Features wie Pods, Deployments, Services, Command Palette, Mouse-Support, Contexts und Namespaces wie „kostenlos“ wirken
- Dadurch weitete sich das Projekt von einem GPU-fokussierten Tool zu einem General-Purpose-TUI für alle Kubernetes-Nutzer aus, also faktisch in Richtung eines erneuten k9s-Nachbaus
- In einer flachen
keyMapwaren viele View-spezifische Bindings wieFullscreen,Autoscroll,ToggleTime,WrapText,CopyLogs,ToggleLineNums,Describe,YamlView,Edit,Shell,FilterLogs,FleetTabNext,FleetTabPrevin einer einzigen Struktur vermischt AutoscrollundShelllagen beide aufs; weil der Dispatch die aktuelle Resource prüfte, „funktionierte“ es zwar, aber Keybindings ließen sich nicht mehr lokal verstehen- Die Geschwindigkeit beim Schreiben des Codes sah wie „Shipping“ aus, aber jedes Feature verursachte die Kosten eines weiteren Branches im god object
- Die Alternative ist, in
CLAUDE.mdeine Scope-Grenze festzulegen: k10s ist für GPU-Cluster-Operatoren gedacht, unterstützte Views sind auf Fleet, Node-Detail, GPU-Detail und Workload begrenzt, und generische Resource-Views oder k9s-duplizierende Funktionen werden nicht hinzugefügt - KI kann ein unendliches Line-Budget liefern, aber das Complexity-Budget bleibt endlich, daher muss Scope vorab aktiv abgelehnt werden
-
Prinzip 4: Positionsbasierte Daten sind eine Zeitbombe
- k10s flachte Resources aus der Kubernetes-API sofort in die Form
type OrderedResourceFields []stringab - Die Sortierfunktion der Fleet-Ansicht behandelte
ra[3]als Alloc,ra[2]als Compute undra[0]als Name; die Identität der Spalten hing damit nur an Kommentaren und an der Spaltenreihenfolge inresource.views.json - Wenn in
resource.views.jsonzwischen Instance und Compute eine Spalte eingefügt wurde, konnten Sortierung, bedingtes Rendering und Drill-Targets, die aufra[2]undra[3]zugreifen, stillschweigend falsch werden - Der Compiler kennt die Bedeutung von
[]stringnicht, und auch die JSON-Konfiguration konnte weder Sortierverhalten noch bedingtes Rendering oder benutzerdefinierte Drill-Targets ausdrücken, sodass der Go-Code positionsbasierte Annahmen hart codierte - KI wählt leicht
[]stringoderVec<String>, weil sie sich direkt in Table-Widgets einspeisen lassen; typisierte Structs haben anfangs mehr Zeremoniell und fallen deshalb auf dem schnellen Pfad oft weg - Die Alternative ist, strukturierte Daten bis unmittelbar vor dem Rendering als typisierte Structs wie
FleetNodeoderPodInfozu behalten und Sortierung nicht über positionsbasierten Zugriff wierow[3], sondern über benannte Felder auszuführen - Eine Beispielstruktur wie
FleetNode { name, instance_type, compute_class, alloc }drückt Spaltenidentität im Typ aus, sodass unmögliche Zustände wie eine falsche Spaltensortierung gar nicht erst erzeugt werden können - „Making impossible states impossible“ ist ein Ausdruck aus der Elm-/Rust-Community und bedeutet, Typen so zu entwerfen, dass ungültige Zustände gar nicht konstruiert werden können, statt sie erst zur Laufzeit zu prüfen
- k10s flachte Resources aus der Kubernetes-API sofort in die Form
-
Prinzip 5: KI besitzt keine State-Transitions
- Der Kern des Bubble-Tea-Modells ist, dass State nur in einem message-getriebenen
Update()verändert wird, aber k10s verletzte genau das - Der Handler
updateTableMsggab einetea.Cmd-Closure zurück, und innerhalb dieser Closure wurden mit Aufrufen wiem.updateColumns(m.viewWidth),m.updateTableData()undm.table.SetCursor(savedCursor)Felder desModelverändert - Bubble Tea führt
tea.Cmdin einer separaten Goroutine aus, daher konnte die Closurem.resources,m.tableundm.viewWidthlesen und schreiben, während die Haupt-Goroutine inView()dieselben Felder las - Es gab weder Lock noch Mutex, und
<-m.updateTableChanwartete nur auf ein Update-Signal, verhinderte aber nicht, dassView()halb geschriebene Zustände las - Diese Struktur war ein offensichtliches Data Race und zeigte sich meist dadurch, dass zwar „meistens alles funktioniert“, aber gelegentlich die Darstellung kaputtgeht
- Die Alternative ist, dass Background-Worker den UI-State nicht direkt mutieren, sondern typisierte Messages über einen Channel senden und der Main Event Loop nach Empfang dieser Messages die State-Mutationen anwendet
- Die Concurrency-Regel lautet: Background-Tasks dürfen UI-State nicht direkt ändern, sondern senden Ergebnisse als typisierte Message; außerdem muss
render()/view()eine pure Funktion ohne Side Effects, I/O oder Channel-Operationen sein
- Der Kern des Bubble-Tea-Modells ist, dass State nur in einem message-getriebenen
Schutzregeln für CLAUDE.md und agents.md
-
Architektur-Invarianten
- Jede View muss ein
View-Trait/Interface implementieren und darf nicht auf den State anderer Views zugreifen - Alle asynchronen Daten müssen über
AppMsg-Varianten hereinkommen, und Background-Tasks dürfen keine Felder direkt mutieren - Das Hinzufügen einer neuen View darf keine Änderungen an bestehenden Views erfordern
- Die
App-Struktur soll ein schlanker Router sein, der nur Navigation und Message-Dispatch übernimmt
- Jede View muss ein
-
Regeln zur State-Ownership
- View-spezifische State-Felder dürfen nicht zur
App/Model-Struktur hinzugefügt werden - Jede View soll als eigenes Struct existieren und ihre eigenen Keybindings deklarieren
- Die App soll Keys an die aktive View dispatchen, und neue Keybindings gehören in die Keymap der jeweiligen View statt in einen globalen Handler
- Wenn das Hinzufügen einer View Änderungen an bestehenden Views verlangt, muss angehalten und nach Bestätigung gefragt werden
- View-spezifische State-Felder dürfen nicht zur
-
Scope
- k10s soll ein Tool für GPU-Cluster-Operatoren sein, nicht für alle Kubernetes-Nutzer
- Unterstützte Views sollen auf Fleet, Node-Detail, GPU-Detail und Workload begrenzt sein
- Generische Resource-Views wie Pods, Deployments oder Services sollen nicht hinzugefügt werden
- Features, die Funktionen von k9s duplizieren, sollen nicht hinzugefügt werden
- Feature-Requests, die Betreibern von GPU-Training-Jobs nicht helfen, sollen abgelehnt werden
-
Datenrepräsentation
- Strukturierte Daten dürfen nicht in
[]string,Vec<String>oder positionsbasierte Arrays abgeflacht werden - Daten sollen bis unmittelbar vor dem Render-Aufruf als typisierte Structs fließen
- Spaltenidentität soll aus Struct-Feldnamen kommen, nicht aus Array-Indizes
- Sortierfunktionen sollen auf typisierten Feldern arbeiten, nicht über positionsbasierten Zugriff wie
row[3] - Strings für die Anzeige sollen nur in
render()/view()-Funktionen erzeugt werden
- Strukturierte Daten dürfen nicht in
-
Nebenläufigkeitsregeln
- Background-Tasks wie Watcher, Scraper oder API-Calls dürfen den UI-State nicht direkt mutieren
- Background-Tasks sollen ihre Ergebnisse als typisierte Messages über einen Channel senden
- Nur der Main Event Loop darf State-Mutationen auf Basis empfangener Messages anwenden
render()/view()muss eine pure Funktion ohne Side Effects, I/O oder Channel-Operationen sein- Wenn asynchrone Arbeit den State ändern soll, muss eine neue
AppMsg-Variante definiert werden
Wie der Neuaufbau erfolgen soll
- k10s soll in Rust neu geschrieben werden, nicht weil Rust objektiv besser wäre, sondern weil es sich wie eine Sprache anfühlt, die direkt steuerbar ist
- In einer Sprache, mit der man ausreichend vertraut ist, spürt man oft schon vor einer verbalen Erklärung, dass etwas falsch läuft, und dieses Gespür kann Vibe-Coding nicht ersetzen
- Wenn KI plausibel wirkenden Code ausgibt, braucht man die Fähigkeit zu erkennen, ob er in Wahrheit Müll ist
- In der neuen Version wird die gestalterische Arbeit wie konkrete Interfaces, Message-Typen und Ownership-Regeln von Menschen zuerst von Hand erledigt, bevor überhaupt Code geschrieben wird
- Statt Architekturentscheidungen wieder von der KI falsch treffen zu lassen, werden sie künftig noch vor dem ersten Prompt als Dokument festgelegt
- Links zum bisherigen TUI und zum Projekt finden sich auf k10s Github und K10S.DEV
Nachtrag
- Bubble Tea ist ein Go-TUI-Framework auf Basis der The Elm Architecture; die Architekturprobleme von k10s entstanden nicht durch Bubble Tea, sondern in der k10s-Implementierung
- „Making impossible states impossible“ ist ein Ausdruck aus der Elm-/Rust-Community und meint, ungültige Zustände nicht zur Laufzeit zu prüfen, sondern sie durch Typdesign gar nicht erst konstruierbar zu machen
- So wie bei KI-Texten der „em-dash“ als Geruchsspur auftaucht, kann beim KI-Coding das „god object“ ein solcher Geruch sein, und Vibe-Coding lässt Implementierung billig erscheinen, was zu Fokusverlust und Bloat führen kann
1 Kommentare
Hacker-News-Kommentare
Diejenigen, die sagen, generierter Code sei okay, waren meist Leute, die diesen Code nicht lesen
Auch die im Artikel vorgeschlagenen Gegenmaßnahmen halten wohl nicht lange. Wenn man Systeme oder Komponenten entwirft, entstehen Invarianten wie „Eine View greift nicht auf den Zustand einer anderen View zu“, aber irgendwann muss man eine Funktion hinzufügen, die mit dieser Bedingung kollidiert
Dann gibt man die Funktion meist entweder auf, setzt sie unbeholfen und ineffizient auf die Invariante drauf oder man ändert die Invariante selbst. Diese Entscheidung ist nicht bloß eine Frage des Kontexts, sondern des Urteilsvermögens, und aktuelle Modelle liegen dabei viel zu oft falsch
Wenn man Architektur-Constraints explizit macht, verbiegt sich der Agent auch dann daran entlang und produziert komplexen, nicht wartbaren Code, wenn diese Constraints eigentlich geändert werden müssten. Wenn man ihn nicht noch genauer liest als von Menschen geschriebenen Code, entsteht am Ende „Code, der sich selbst auffrisst“, und man merkt es erst viel zu spät
Der Schlüssel ist, die Stellen zu erkennen, an denen AI Probleme hat, und sie einfacher zu machen. Dafür braucht man zum Beispiel extrem kleinen Kontext, klar abgegrenzte Modularisierung, pure Module, die von Ein- und Ausgabe getrennt sind, Verstecken hinter Interfaces, 100 Tests, die in unter einer Sekunde laufen, Benchmarks usw.
AI funktioniert gut, wenn es Grenzen und kleinen Kontext gibt. Gibt man ihr das nicht, wird sie schlechter, und die Verantwortung dafür liegt bei der Person, die das Tool benutzt
Keine Spezifikation hält der Realität stand, und selbst nach gründlicher Untersuchung und Planung stellt sich irgendeine Invariante in der Spezifikation am Ende als falsch heraus
Wenn Menschen in der Entwicklung auf so eine Situation stoßen, können sie einen Schritt zurücktreten und neu überlegen, ob die Invariante falsch ist und welche Auswirkungen eine Änderung hätte. AI dagegen bastelt unter falschen Annahmen oder Designs oft irgendwie eine gehackte Lösung zusammen und hat zu wenig Einsicht, um das Ganze neu zu bewerten
Mit gutem Workflow und Validierung kann es besser werden, aber es ist kein Bereich, den Tools wie Claude Code standardmäßig gut beherrschen; da gibt es Grenzen
Anfangs haben wir starke Prinzipien aufgestellt und ein paar Einsatzfälle von Hand migriert, um Vertrauen zu gewinnen. Die komplette Migration war so groß und teuer, dass sie fast 10 Jahre lang verschoben wurde, also wollten wir sie mit AI beschleunigen, um die Kosten zu senken
Für die mechanischen, einfachen 80 % war AI okay. Bei den restlichen 20 % musste das Framework geändert werden; meistens waren das kleine Änderungen wie zusätzliche API-Felder, aber ein oder zwei Fälle erforderten ein konzeptionelles Redesign
Das Backend eines bestimmten Systems konnte in 99 % der Fälle bestimmte Daten erzeugen, in einigen wichtigen Fällen aber logisch nicht, sodass sie extern gemeldet werden mussten. Eine wichtige Optimierung beruhte jedoch auf der Annahme „das ist unmöglich“
Das AI-Tool erkannte diese Situation nicht und fügte Migrationslogik hinzu, die so aussah, als würde sie korrekt funktionieren. Dank unserer Deployment-Methode war es noch kein Produktionsbug, aber als wir dem Partnerteam die richtigen Fragen stellten, stellten wir fest, dass derselbe Bedarf auch anderswo bestand
Am Ende wurde es nur deshalb kein großes Problem, weil ein Mensch tief genug drinsteckte. Mit Validierungswerkzeugen und schlaueren Modellen könnten solche Migrationen in Zukunft leichter werden, aber im Moment ist generierter Code zwar manchmal schön, aber auch brüchig, sodass man ihn ständig eng begleiten muss
Ich hatte ein seltsames Architektur-Pattern etwa zwei Monate lang benutzt, und jedes Mal fühlte es sich ein wenig unbequem an. Erst gestern Abend wurde mir klar, dass es keine gute Abstraktion war und dass man es besser aufteilen könnte
Wenn ich den Code von einem LLM generieren ließ, spürte ich dieses Unbehagen viel weniger deutlich, wodurch ich das Problem später erkannte und länger brauchte, um eine Lösung zu finden. Den Randbereich kann man ruhig generieren lassen, aber die Kernfunktionalität sollte man weiterhin größtenteils selbst schreiben
Selbst wenn man sie in einer präzisen formalen Sprache ausdrückt, fehlt dem LLM unter dem Agenten die Fähigkeit zu verstehen, warum diese Invarianten nötig sind und warum sie wichtig sind. Es mag irgendwann LLMs geben, die Tokens mit formalen Spezifikationen verknüpfen und sogar Beweise schreiben, aber seltsamer Code aus den informellen Teilen des Prompts wird weiter auftauchen
Man kann das nicht verhindern, indem man Constraints und Prompts nur als technische Liste oder Spezifikation ergänzt. Auch mit besseren Fallen entkommen die Lebewesen
Das Problem ist Code-Aufblähung, also Code, der hinzugefügt wird, um Prompt oder Aufgabe irgendwie zu erfüllen. Oft ist weniger Code besser, und man braucht Menschen, die vorhersagen können, was andere wollen und erwarten. Generatoren sind gut, aber man sollte sie etwas zurückhaltender einsetzen, eher nicht wie einen Feuerwehrschlauch
Als Copilot noch nur eine Zeile automatisch vervollständigte, hieß es: „Trotzdem musst du noch die ganze Funktion schreiben“, und als die Funktion vervollständigt wurde: „Die Logik um die Funktion herum musst du selbst schreiben“, und als auch diese Logik vervollständigt war: „Das Feature musst du selbst schreiben“
Jetzt, wo sogar Features fertig werden, heißt es: „Aber die Architektur musst du immer noch selbst schreiben.“ Ich weiß nicht, ob diese Modelle Architektur lösen können, aber es ist interessant zu sehen, wie sich die Erwartungen ständig verschieben
Auch wenn AI eine Zeile fertigstellt, eine ganze Funktion oder ein Feature samt Ticket, muss man den Code immer noch lesen und verstehen
Ich benutze AI ständig, und sie wird besser, aber ich reviewe immer noch jede einzelne Zeile. Selbst auf Zeilenebene ist sie heute meiner Meinung nach nicht klar besser als die Tab-Autovervollständigung vom letzten Jahr; manchmal ist sie sehr gut, manchmal wirklich schlecht
LLMs sind großartig für Softwareentwicklung, aber nur, wenn man sie nicht die Architektur schreiben lässt. Module, Structs und Enums sollte man selbst anlegen, und möglichst auch Felder und Varianten selbst hinzufügen
Es ist sinnvoll, jedes Struct, Enum, Feld und Modul mit Doc-Comments zu versehen und dem LLM dann diese Module und Datenstrukturen zu zeigen, damit es die nötigen Funktionskörper usw. ausfüllt
Selbst wenn man mehrfach sagt „Auf dem kritischen Pfad niemals blockieren“, baut das LLM Blockierung in den kritischen Pfad ein, und wenn man sagt „Wenn du X machst, brauchst du Tests vom Typ Y“, macht es X und vergisst die Tests
Menschen befolgen Anweisungen auch nicht zu 100 %, aber LLMs sind zufälliger. Menschliche Fehler bestehen relativ selten darin, exakt das Gegenteil des Gewünschten zu tun
LLMs sehen wichtige Invarianten im Code und bauen trotzdem Umgehungen, schreiben Tests, die Fehlschläge wie Erfolge aussehen lassen, behaupten dann, sie hätten genau das Geforderte getan, und vergraben es in einem 5000-Zeilen-Commit
Ich halte LLMs für großartig und bin sicher, dass sie die Zukunft sind. Deshalb baue ich auch eine Sprache namens https://GitHub.com/Cuzzo/clear, damit wir über Sprachprobleme hinauskommen, bei denen globaler Kontext dort nötig ist, wo er nicht nötig sein sollte. Dann wird Zusammenarbeit leichter
Es gab Erfolge, aber manchmal war es so frustrierend, dass ich mich gefragt habe, ob es den Verstand wert war, den ich hineingesteckt habe
Nicht, weil Architektur unwichtig wäre, sondern weil eine Architektur, die gestern gut gepasst hat, heute nicht zwingend immer noch passen muss
Ich habe mir beim Einsatz von Coding-Agenten ein paar Regeln gesetzt
Erstens: Wenn ich mit einem Agenten Code generieren lasse, dann nur für etwas, bei dem ich absolut sicher bin, dass ich es mit genug Zeit selbst korrekt schreiben könnte
Zweitens: Wenn das nicht der Fall ist, gehe ich nicht weiter, bis ich das Generierte vollständig verstehe und selbst reproduzieren könnte
Drittens: Wenn ich Regel zwei breche, kann ich kognitive Schulden aufbauen, aber ich muss sie vollständig zurückzahlen, bevor ich ein Projekt für abgeschlossen erkläre
Je mehr Schulden sich ansammeln, desto wahrscheinlicher wird es, dass die Qualität später generierten Codes sinkt, und es fühlt sich an, als würde das mit Zinseszins wachsen. Für private Projekte ist das angenehm, man lernt viel, und am Ende bleibt eine Codebasis, die man entspannt verstehen kann
Man braucht einen Gleichgewichtspunkt, an dem man mit der Codebasis verbunden bleibt, ohne zum Bottleneck des Teams zu werden
Claude ist ein Mathematiker auf PhD-Niveau, ich nicht, aber ich wusste genau, welche Eigenschaften die gewünschte Lösung haben sollte und wie man testet, ob sie korrekt ist. Also habe ich Claudes Lösung statt meiner einfachen, naiven Lösung übernommen, das im Pull Request vermerkt, und alle hielten das für die richtige Entscheidung
Ich frage mich, ob man für solche Fälle Ausnahmen machen sollte. Noch interessanter ist die Frage, ob man ganz mit dem Handschreiben von Code aufhören würde, falls AI nicht nur in höherer Mathematik, sondern auch beim Programmieren viel besser wäre als man selbst, unter der Annahme, dass man dann vielleicht die Tests noch beurteilen kann, auch wenn man den Code selbst nicht mehr beurteilen kann
Denn die auflaufenden Schulden bestehen ganz konkret in mangelndem Verständnis des Codes; das ist präziser
Ich verstehe nicht, warum ausgerechnet AI plötzlich völlig anders behandelt werden sollte
Letztlich muss man es als Risiko-Ertrags-Abwägung betrachten. Was verliert man, wenn es falsch ist, wie wahrscheinlich ist es, dass Tests und Reviews es finden, und was gewinnt man, wenn es klappt? Das gilt genauso für Libraries und externe Services
Komplexe Finanzregeln in einem nicht testbaren Krypto-Vertrag ohne Updates sind nicht annähernd dasselbe Risiko wie ein Viewer zur Visualisierung interner Logdaten
Theoretisch klingt sie gut, aber in der Praxis nimmt man immer mentale Abkürzungen, ohne es zu merken
Wenn ich in einer unbekannten Codebasis einen Fehler selbst behebe und das mit einem Fall vergleiche, in dem ich glaubte, die Arbeit des Agenten „vollständig verstanden“ zu haben, bleibt eine Woche später unterschiedlich viel im Kopf hängen. Wenn ich es selbst mache, wird es zu allgemeinem Wissen, und die wichtigen Teile bleiben meist erhalten; wenn ich versuche, das vom Agenten Geschriebene als mein eigenes zu übernehmen, fühlt es sich in dem Moment zwar verstanden an, aber ich vergesse es sehr schnell wieder
Deshalb bin ich zu dem Schluss gekommen, dass LLM-Hilfe in solchen Fällen meinen Zielen meistens schadet, selbst wenn man andere Sorgen wie Zeit- und Business-Druck außen vor lässt
Mir ist genau dasselbe passiert
Der Betrug läuft so ab: In einer guten Codebasis kann AI viele Features bauen, und es wirkt schneller, sicherer und präziser. Besonders in Bereichen, die man selbst nicht gut kennt, fühlt es sich noch stärker so an
Mit der Zeit wächst die Codebasis, die Navigationszeit wird länger und die Fehlerrate steigt. Man will es nicht wahrhaben und drückt noch stärker aufs Gas, bis Änderungen praktisch unmöglich werden und man erst dann aufhört
Wenn man den Code wieder ansieht, reicht „Spaghetti“ als Beschreibung nicht mehr aus; es ist eher die Chinesische Mauer
Am Ende habe ich 75.000 von 140.000 Zeilen gelöscht, und die drei Monate, in denen ich mich intensiv auf Agent-Coding eingelassen hatte, fühlten sich wie verschwendet an. Ich habe nutzlose Features gebaut, mehr Bugs produziert, mein mentales Modell des Codes verloren, schwierige Entscheidungen übersehen, die man nur erkennt, wenn man im Code steckt, und letztlich auch die Nutzer enttäuscht
Nicht spöttisch gemeint, aber ich frage mich wirklich, was die ursprüngliche Erwartung war und woher sie kam
An LLMs scheint man andere Erwartungen zu haben. Wenn man einem zufälligen „Entwickler“, den man nur online getroffen hat, eine zusammengefasste Feature-Beschreibung gibt und dann einen halb kaputten Haufen Implementierung zurückbekommt, wäre niemand überrascht
Trotzdem erwarten Menschen von Maschinen, die gelegentlich wortreiche Halluzinationen produzieren, manchmal Wunder, die sie nicht einmal von Menschen erwarten würden. Ich frage mich, woher dieses Vertrauen kommt
So wie eine große Stadt aus kleinen Städten besteht, hat man eine Karte, zoomt in einen lokalen Bereich hinein und arbeitet dort. Man muss nicht jedes Detail von New York kennen, nur um einen Kaffee zu trinken
Eine wartbare gesunde Architektur zu schaffen, ist die Verantwortung der Person, die das Tool benutzt. AI verhindert das nicht, und wenn man das Tool richtig einsetzt, kann sie sogar helfen
Zum Beispiel könnte man den von AI generierten Code sofort als Legacy-Code behandeln, starke Kapselungsgrenzen und klar definierte Interfaces ziehen und ihn dann in einen passiveren Workflow integrieren
Es gibt ein Spektrum vom Einmal-Prompt bis zur Inline-Codegenerierung, und je nach Problem und Position im Codebase ist eine andere Form passend
Einmalige Generierung eignet sich eher für Prototypenphasen, in denen man die Spezifikation oft wiederholt; sobald der Prototyp Form annimmt, geht man auf Modul- oder Dateiebene herunter und arbeitet systematischer, während man auf dieser Ebene weiterhin ein brauchbares mentales Modell behält
Wenn er gelesen, aber nicht verstanden wurde, hätte man sich zu jedem Output detaillierte Kommentare geben lassen können. Wenn man weiß, dass Modelle mit wachsender Codebasis Probleme bekommen, muss man den Output umso strenger prüfen, je höher die Komplexität wird
Also Inseln mit höherer Codequalität schaffen, AI helfen lassen, Entwicklerintentionen und Business-Regeln zu rekonstruieren, und im Zielmodul Seams und Unit-Tests anlegen
AI muss nicht zwingend nur zur Steigerung des Durchsatzes dienen; sie kann auch ein flexibles Werkzeug für Exploration und Refactoring sein, das späteres Handschreiben oder Agent-Implementierungen unterstützt
Jedes Mal, wenn ich solche Texte sehe, vergleiche ich die Geschwindigkeit, die Leute mit AI angeblich erreichen, mit der Geschwindigkeit, die ich erreiche, wenn ich einfach selbst programmiere
Zufällig arbeite ich seit 7 Monaten an einem 3D-MMO-Projekt, das inzwischen spielbar ist, Leuten Spaß macht, okay aussieht und problemlos Hunderte Nutzer auf den Server bringt. Die Architektur ist ziemlich gut, Features lassen sich leicht erweitern, und nach etwa einem Jahr Entwicklung könnte man es veröffentlichen
Im Originalartikel hat man dagegen nach 7 Monaten Vibe-Coding nicht einmal ein grundlegendes TUI gebaut. Die Feature-Geschwindigkeit kann sich hoch anfühlen, aber für so ein Basis-UI ist das unglaublich langsam. Es gibt viele gute TUI-Libraries, und wenn man nur Tabellen mit den nötigen Daten füllen muss, kann man das von Hand in ein paar Wochen bauen
Mit AI hat man stark das Gefühl, schnell viel voranzukommen, aber in Wirklichkeit scheint es oft viel langsamer zu sein als manuelles Coden. Produktivitätsdaten scheinen ebenfalls eher zu stützen, dass AI-Nutzer sich schneller fühlen, tatsächlich aber weniger Output haben
Der größte Zeitfresser in der Softwareentwicklung sind Meetings, in denen Erwartungen von Stakeholdern und Lösungsansätze abgeglichen werden. Aus dieser Perspektive hilft AI fast gar nicht; wenn man also Personenstunden vom Vorschlag bis zum Einstieg in die Testschleife vergleicht, bekommt man ernüchternde Ergebnisse
Aber bei Problemlösung, Bugfixing und der Umsetzung bereits freigegebener Lösungen bin ich im Vergleich zu früher mindestens um den Faktor 10 besser geworden. Es geht nicht nur um reine Zeit, sondern auch um die Fähigkeit, beobachtetes Verhalten zu interpretieren und Probleme zu untersuchen
Allerdings gibt es auch Leute, die mit AI keine wertvollen präzisen Ergebnisse erzeugen können. Wenn man genau weiß, was man will und wie man es will, ist AI eine große Hilfe. Wenn ich ihr etwas gebe, das ich ohnehin gemacht hätte, macht sie es schneller. Wenn ich aber selbst nicht genau weiß, was ich will, schadet AI dem Fortschritt
Wenn Leute zeigen, was sie mit LLMs gebaut haben, beeindruckt mich das selten, weil es meist Dinge sind, die man auch von Hand in sehr kurzer Zeit bauen könnte
Ich beobachte auch nicht, dass wirklich beeindruckende Software häufiger wird, was dazu passt, dass LLMs derzeit eher einfache als wichtige Probleme lösen
Außerdem wird Codequalität zu wenig erwähnt
Eine per Vibe-Coding entstandene Codebasis ist ein großartiges Beispiel dafür, dass LLMs im Code-Schreiben gar nicht so gut sind. Sie beheben eigene Fehler und bauen sie sofort wieder ein, und selbst die Musterverwendung ist inkonsistent
In letzter Zeit trifft Claude auch „interessante“ Stilentscheidungen, die nicht zum Stil der bestehenden Codebasis passen
Solche Wiederholungen muss man mit Sprache im Stil eines „Senior Developers“ unterbinden
Der Teil „Bevor ich Code schreibe, entwerfe ich konkrete Interfaces, Nachrichtentypen und Ownership-Regeln selbst“ ist genau der schwierige Teil am Programmieren
Wenn die Architektur da ist, ist das Schreiben von Code sehr leicht. Wenn man den Code nicht selbst schreibt, merkt man schwerer, dass man etwa eine API mit nullable Werten entworfen hat, die Datenbank das aber nicht erlaubt — oder dass sie es zwar erlaubt, man aber andere kleine Probleme übersehen hat
Ich verstehe nicht, wie man diesen Text schreiben und trotzdem nicht merken kann, dass das Problem AI ist. Nicht nur, weil man AI die Architektur überlassen hat, sondern weil man nicht genau darauf geachtet hat, was AI überhaupt macht
AI ist ein verherrlichter Codegenerator, und man muss alles kontrollieren, was sie tut. Der schwierige Teil der Softwaretechnik war nie das Tippen von Code, sondern alles drum herum
Entwickler, die Coden für schwierig halten, lieben AI-Coding wirklich, weil etwas, das früher schwer war, jetzt leicht geworden ist
Für diejenigen, die Coden für leicht halten, geht es beim Coden um Abstraktion, Wartbarkeit und Erweiterbarkeit. Schwierig ist, ein sinnvolles Fundament zu legen, auf dem Software wachsen kann, und wenn man die richtige Abstraktion gefunden hat, ist der Rest vergleichsweise einfach
Für diese Leute ist AI-Coding ein nützliches Werkzeug, aber kein magisches. Der Autor des Originalposts hat die Grenzen von AI erkannt und gehört deshalb zur zweiten Gruppe; er hat den schwierigen Teil gesehen, den AI nicht kann
Auf der einen Seite gibt es Leute, die starke Tab-Autovervollständigung oder Chatbots im Nebefenster nutzen und trotzdem alles gründlich reviewen, und auf der anderen Seite neue Editoren, die damit werben, Dutzende Agenten zu orchestrieren, sodass man den Großteil des Codes angeblich gar nicht mehr lesen werde, wie Steve Yegge hier: https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16d...
Die erste Gruppe denkt weiterhin tief über Design, Interfaces und Datenstrukturen nach und reviewt streng. Die zweite Gruppe tut das nicht, und das macht mir mehr Sorgen
Ich folge dem Ansatz plan → red/green/refactor, und die Planung selbst wirkt oft ziemlich plausibel und fundiert, weil alle Dokus und Forendiskussionen eingesaugt werden
Das Problem ist, dass beim tatsächlichen Arbeiten zwangsläufig Stellen auftauchen, an denen Doku und Implementierung voneinander abweichen. Vielleicht wurde eine Tool-Kombination nie so verwendet, vielleicht ist die Doku veraltet, vielleicht ist es einfach ein Bug
Wenn Projekt- oder Featureziel aber klar genug sind und man lokal ausführen und testen kann, kann sich ein Agent aus architektonischen Sackgassen iterativ wieder herausarbeiten. Er schaut sogar in Dependencies und Library-Code und schlägt Upstream-Fixes vor; das ähnelt dem, was ich in tiefen Debugging-Sessions selbst tue
Deshalb bin ich ziemlich zufrieden damit, langweilige Arbeit lieber zu delegieren und zu überwachen, statt sie selbst zu machen. Allerdings graben viele Teammitglieder Architekturprobleme nicht so tief auf und „eskalieren an den Architekten“, und das scheint langfristig nicht gut zu sein
Das Zeitfenster, in dem man alles ausführen und verstehen kann, schließt sich schnell. Vielleicht passen wir uns aber an, indem wir neue Tools und Frameworks bauen — so wie wir auch Compiler benutzen, ohne jeden Schritt bis zum Maschinencode oder Branch Prediction und Caching moderner CPUs vollständig zu verstehen
Als jemand mit nicht allzu viel Code-Erfahrung lerne ich mehr als je zuvor, indem ich Ergebnisse überprüfe und sehe, was richtig und falsch ist
Deshalb glaube ich auch nicht, dass es bald dramatisch besser wird. Wenn Leute mich fragen: „Wie bekommst du aus Claude so gute Ergebnisse?“, lautet meine Antwort immer: „Ich habe genau hingeschaut, Probleme gefunden und Claude gebeten, sie zu beheben.“ Das ist wirklich alles, aber schon bei dieser Antwort wird der Blick oft glasig
Das ist ähnlich wie bei Google: Es hat das Finden von Informationen erleichtert, aber den menschlichen Anteil nicht entfernt, der gute von schlechten Informationen unterscheiden muss
Erst denke ich über das Problem nach und entwerfe Struktur und APIs, und erst dann lasse ich AI die Implementierung übernehmen
Der Titel lautet zwar „Ich bin zum Handschreiben von Code zurückgekehrt“, aber tatsächlich macht die Person eher „die Design-Arbeit von Hand, bevor Code geschrieben wird“
Dann scheint der Code immer noch von Claude generiert zu werden
Noch gravierender ist, dass man offenbar 7 Monate lang glaubte, das Vibe-Coding-Projekt funktioniere gut, ohne den generierten Quellcode überhaupt anzuschauen, und sogar schon eine Domain dafür gekauft hatte
Wenn es ein Side-Project ist und man Diffs verfolgt und schrittweise prüft, ist es nicht völlig absurd, den Code nicht bis in die Tiefe zu lesen. Sicher eine andere Arbeitsweise, aber nicht völlig verrückt
Es fühlt sich an, als würde man Entwicklern dabei zusehen, wie sie die Lektionen aus Projektmanagement und Produktmanagement im Speedrun nachholen
Jetzt sehen sie, dass Spezifikationen nützlich sind und dass ein Projekt nicht schneller wird, nur weil man viel falschen Code schreiben lässt. Entwickler ärgern sich oft über Meetings und Diskussionen als Hindernis fürs Coden, aber genau solche Prozesse existieren häufig, um zu verhindern, dass alle noch mehr falsche Dinge bauen
Man hat auch erkannt, dass Aufgabenmanagement nützlich ist, und jetzt, wo ständig davon die Rede ist, dass man das gesamte Design im Voraus machen müsse, bewegt man sich in Richtung Wasserfall
Als Nächstes wird man Prototyping benennen, über inkrementelle Features sprechen, bei denen alte und neue Anforderungen gleichzeitig verwaltet werden, und irgendwann sagen, dass Kunden stärker eingebunden werden müssen
Es lohnt sich, sich anzusehen, was Projektmanager und Produktmanager tatsächlich tun. Sie führen ein Produkt namens Code, ohne dass von ihnen erwartet wird, Code zu lesen, und müssen das alles allein in natürlicher Sprache erreichen
Glauben sie, Menschen würden nichts Kaputtes schreiben? Glauben sie, Teams würden nie eine Woche oder mehrere Monate auf dem falschen Weg verbringen? Jetzt kann man all das mit Vibe-Coding in 30 Minuten erleben. Als ehemaliger technischer Produktmanager fühlt sich das exakt gleich an
Ich bin verwirrt von der Lücke zwischen Titel und Fazit, weil es in Wirklichkeit gar nicht so aussieht, als würde hier jemand wieder von Hand Code schreiben