null

Wie ich LLMs dazu gebracht habe, Software zu bauen, der ich vertrauen kann

Wie kann ich LLMs für die Entwicklung komplexer Software einsetzen, ohne ihre typischen Schwächen einfach zu akzeptieren?

Ich habe Pulse nicht gebaut, um zu zeigen, dass ein LLM viel Code schreiben kann.

Das ist inzwischen keine besonders interessante Aussage mehr.

Die interessantere Frage war für mich eine andere:

Wie kann ich LLMs für die Entwicklung komplexer Software einsetzen, ohne ihre typischen Schwächen einfach zu akzeptieren?

LLMs sind hervorragend darin, Code zu erzeugen. Sie sind aber nicht zuverlässig darin, über zehntausende Zeilen hinweg eine Architektur konsistent zu halten, bereits vorhandene Abstraktionen wiederzuverwenden, implizite Regeln dauerhaft einzuhalten oder zu erkennen, dass eine lokal plausible Lösung global falsch ist.

Genau diese Eigenschaften sind bei größeren Softwareprojekten entscheidend.

Pulse ist deshalb neben dem eigentlichen Produkt auch ein Experiment in AI-assisted software engineering. Ich habe den Entwicklungsprozess selbst als System entworfen: mit Specifications, Architecture, TDD, Verification Gates, festen Regeln, mehreren Modellen und später einem eigenen Retrieval-System.

Der größte Teil des Codes wurde von LLMs geschrieben. Meine Aufgabe war deshalb nicht, weniger technisch zu arbeiten. Im Gegenteil.

Ich musste sehr viel genauer festlegen, was gebaut werden soll, warum es so gebaut werden soll und welche Eigenschaften die Implementierung zwingend besitzen muss.

Das Ergebnis ist heute eine Codebasis von ungefähr 55.000 Zeilen.

Und das eigentlich Interessante daran ist nicht die Zahl der Zeilen.

Es ist die Frage, wie diese Codebasis entstanden ist.


Das eigentliche Problem

Pulse ist eine client-side PWA, die strukturierte Daten in gebrandete .docx-Reports verwandelt.

Das konkrete erste Anwendungsgebiet sind monatliche HR-Reports. Pulse ist aber nicht als HR-Report mit ein paar konfigurierbaren Feldern gebaut. Das System besitzt ein generisches Report-Modell: Datenmodell, Sections, Tables, Computed Fields, Charts, Validation, Prose und die DOCX-Ausgabe werden durch eine portable Report Definition beschrieben. Die konkrete HR-Anwendung ist nur eine Definition dieses Systems.

Von Anfang an war deshalb klar, dass ich keine Anwendung bauen wollte, die nur diesen einen Report produziert.

Ich wollte ein System bauen.

Das hat eine Konsequenz, die bei AI-assisted development leicht unterschätzt wird:

Die eigentliche Arbeit beginnt vor dem Schreiben des Codes.


1. Ich habe zuerst das System entworfen

Ich habe dem LLM nicht gesagt:

„Bau mir eine HR-Report-App.“

Ich habe beschrieben, wie das System funktionieren soll.

Das betraf Datenmodell, Module, Interfaces, Dependencies, Rendering, Persistence, Import/Export, LLM-Integration, Validation, Security und die Grenzen zwischen den einzelnen Teilen.

Ein wesentlicher Bestandteil davon war die hexagonal architecture.

Der Engine-Core sollte vollständig unabhängig von Browser und UI bleiben. src/engine/ darf weder DOM noch Browser APIs kennen. Alles Plattformabhängige wird über Ports abstrahiert und durch Adapter in src/shell/adapters/ implementiert.

Das war für mich nicht nur eine Frage von Testbarkeit.

Es war auch eine Frage des LLM context.

Wenn der Domain-Core sauber von IndexedDB, File System Access API, Canvas, React und Fetch getrennt ist, kann ich einem Modell eine wesentlich kleinere und präzisere Aufgabe geben.

Das Modell muss bei einer Änderung im Formula Evaluator nicht gleichzeitig verstehen, wie React rendert, wie IndexedDB funktioniert und wie ein DOCX exportiert wird.

Die Architecture schafft damit nicht nur technische Isolation.

Sie schafft Context Isolation.

Das ist bei LLMs besonders wertvoll.

Je weniger irrelevanten Kontext ein Modell für eine Aufgabe benötigt, desto leichter kann es die relevanten Regeln und Invarianten tatsächlich einhalten.


2. Specifications waren ein wesentlicher Teil der Entwicklung

Ein großer Teil meiner Arbeit bestand deshalb nicht aus Implementierung.

Ich habe Specifications geschrieben.

Bereits sehr früh wurde die Repository-Struktur um docs/specs/ erweitert. In der Git History findet sich dieser Schritt bereits am 23. Juni, zusammen mit der Architecture-Dokumentation und den ersten Tests.

Diese Specifications waren keine nachträgliche Dokumentation dessen, was das LLM ohnehin schon gebaut hatte.

Sie waren ein Designinstrument.

Bevor ein komplexer Teil implementiert wurde, musste feststehen:

  • welches Problem gelöst wird,
  • welches Verhalten erwartet wird,
  • welche Grenzen gelten,
  • welche Datenstrukturen verwendet werden,
  • wie das Modul mit anderen Modulen interagiert,
  • welche Fehlerfälle existieren,
  • und welche Eigenschaften später überprüft werden können.

Das ist ein fundamentaler Unterschied zu einem Workflow, bei dem man dem Modell einfach eine Aufgabe gibt und anschließend den erzeugten Code reviewt.

Ich wollte nicht nur Code reviewen.

Ich wollte die Vorstellung des Modells davon, was gebaut werden soll, kontrollieren, bevor es Code schreibt.


3. TDD hatte deshalb für mich eine andere Bedeutung

TDD war in diesem Projekt nicht einfach:

Test schreiben → Code schreiben → Test grün.

Der wichtige Teil war der Zustand vor der Implementierung.

Das Modell musste zuerst eine konkrete Vorstellung davon entwickeln, was es implementieren will.

Der Test zwingt diese Vorstellung in eine überprüfbare Form.

Dadurch entsteht gewissermaßen eine zusätzliche, lokale Specification:

Was glaube ich eigentlich, dass diese Funktion tun soll?

Erst danach durfte die Implementierung folgen.

Die Regeln in CLAUDE.md machen genau diesen Ablauf verbindlich. Tests müssen zuerst existieren und zunächst fehlschlagen; jeder Bug Fix benötigt einen Regression Test, der vor der Korrektur fehlschlägt und danach erfolgreich ist.

Das ist für ein LLM besonders interessant.

Denn ein Modell kann durchaus eine plausible Implementierung erzeugen, ohne vorher ein konsistentes mentales Modell des Problems zu besitzen.

Durch TDD muss es seine Annahmen früher festlegen.

Und danach kann ein zweites Modell nicht nur fragen:

„Ist dieser Code syntaktisch korrekt?“

Es kann fragen:

„Entspricht die Implementierung der Specification und dem Test, den das Modell selbst dafür geschrieben hat?“

Damit entstehen drei Ebenen:

Specification → Test → Implementation

Und genau diese drei Ebenen können voneinander unabhängig betrachtet werden.


4. Ich habe dem Modell außerdem ein Regelwerk gegeben

Ein weiteres Problem bei LLM-generated code ist nicht unbedingt, dass eine einzelne Funktion falsch ist.

Das viel gefährlichere Problem ist Drift.

Das Modell schreibt irgendwo eine Lösung.

Einige Stunden später braucht es etwas Ähnliches.

Es schreibt dieselbe Logik noch einmal.

Oder es verwendet eine Abstraktion nicht, obwohl sie bereits existiert.

Oder es löst ein Problem mit einem Regex, obwohl dafür bereits ein Parser existiert.

Oder es repariert ein Symptom, statt die Ursache zu beheben.

Das sind genau die Fehler, die bei wachsendem Codebestand teuer werden.

Deshalb enthält CLAUDE.md harte Regeln.

Zum Beispiel:

  • keine duplicate implementations,
  • keine any types,
  • keine neuen Dependencies ohne Zustimmung,
  • keine relative imports,
  • keine Regex für formula-shaped input,
  • keine Änderungen an ungelesenen Dateien,
  • keine symptomatischen Fixes ohne root cause analysis.

Das klingt zunächst wie Style Guide.

Ist es aber nicht.

Es ist ein Kontrollsystem für das LLM.

Das Modell bekommt damit nicht nur Informationen darüber, wie der Code aussehen soll.

Es bekommt Grenzen dafür, was es überhaupt tun darf.

Besonders wichtig ist dabei die Regel gegen duplicate logic.

Ein LLM hat kein zuverlässiges Gedächtnis über jede bereits existierende Implementierung. Wenn die Suche nach einer vorhandenen Lösung nicht explizit Teil des Workflows ist, ist es für das Modell sehr einfach, etwas funktional Ähnliches noch einmal zu implementieren.

Darum verlangt mein Workflow explizit, nach bestehenden Implementierungen zu suchen, und nicht nur nach demselben Namen.

Das reduziert eine der typischen Formen von LLM-induced architectural degradation.


5. Tests waren nicht das Ende der Prüfung

Auch ein grüner Test reicht mir nicht.

Das ist eine der wichtigsten Regeln des Projekts.

CLAUDE.md sagt ausdrücklich:

Ein passing unit test beweist, dass eine Funktion isoliert korrekt ist. Er beweist nicht, dass die Funktion aus dem Production Code erreichbar ist.

Deshalb muss das Modell bei einer Änderung unter anderem nachweisen, dass echte Caller existieren und dass der tatsächliche Flow ausgeführt wurde. Für user-visible oder pipeline-visible Änderungen muss beispielsweise bun generate, die echte UI-Interaktion oder der CLI-Flow ausgeführt werden.

Das ist eine wichtige Unterscheidung.

  • Ein Test kann beweisen: resolveField() funktioniert.
  • Er beweist nicht: Das Production-System benutzt resolveField().

Genau diese Art von Fehler ist während der Entwicklung tatsächlich relevant geworden.

Ein Beispiel war die Context-Struktur.

Tests des Evaluators konnten grün sein, obwohl sie mit einer flachen Teststruktur arbeiteten, während die reale Pipeline eine andere Struktur erzeugte. Erst als die Tests dieselbe Struktur verwendeten wie computeFields(), wurde der Fehler tatsächlich abgedeckt.

Das ist für mich der Unterschied zwischen testing code und testing the system.


6. Und dann kam der Punkt, an dem ich zum Bottleneck wurde

Am Anfang habe ich jede einzelne Zeile kontrolliert.

Das war notwendig.

Nicht weil ich dem LLM grundsätzlich misstraut hätte, sondern weil ich sicherstellen musste, dass die erzeugte Software tatsächlich meinen Anforderungen entspricht.

Aber das skaliert nicht.

Wenn ein Modell mehrere tausend Zeilen produziert und ich jede einzelne davon persönlich reviewen muss, ist nicht mehr das Modell der limitierende Faktor.

Ich bin es.

Die Lösung war aber nicht, weniger genau zu werden.

Ich habe den Review-Prozess selbst automatisiert.


7. Implementer und Reviewer

Ich habe den Workflow um zwei Rollen erweitert:

Implementer

und

Reviewer.

Das Entscheidende daran ist, dass diese Rollen nicht einfach zwei Prompts für dasselbe Modell sind.

Ich habe unterschiedliche Modelle eingesetzt, unter anderem DeepSeek, GLM 5.2, Qwen Coder Plus und Opus.

Und diese Modelle konnten beide Rollen übernehmen.

Ein Modell implementiert.

Ein anderes Modell reviewt.

Dann können die Rollen wechseln.

Damit entsteht ein gegenseitiger Kontrollmechanismus.

Der Reviewer bekommt Specification, Test und Implementation und kann die drei Ebenen gegeneinander prüfen.

Das ist wesentlich stärker als:

„Hier ist der Code. Sieht der gut aus?“

Denn der Reviewer kann fragen:

  • Was sollte laut Specification passieren?
  • Was verlangt der Test?
  • Was macht die Implementation tatsächlich?
  • Gibt es eine Abweichung?
  • Wurde eine bestehende Abstraktion umgangen?
  • Wird eine Dependency verletzt?
  • Gibt es eine einfachere Architektur, die bereits im System vorgesehen ist?
  • Ist der Test überhaupt repräsentativ für den Production Flow?

Das Modell muss damit nicht nur Code beurteilen.

Es muss Reasoning über mehrere Repräsentationen desselben Problems betreiben.


8. Ich musste nicht mehr jede Zeile reviewen

Das war der entscheidende Produktivitätsgewinn.

Ich musste nicht mehr jeden einzelnen erfolgreichen Fall selbst kontrollieren.

Stattdessen wurde mein Review-Prozess auf die Fälle konzentriert, in denen die automatischen Rollen nicht übereinstimmten.

Das verändert meine Rolle fundamental.

Ich bin nicht mehr der Mensch, der jede Codezeile absegnet.

Ich bin derjenige, der dort entscheidet, wo die automatisierten Kontrollmechanismen keine eindeutige Antwort liefern.

Und genau dort ist menschliches Urteil besonders wertvoll.

Denn die schwierigen Entscheidungen waren häufig keine Syntax- oder API-Fragen.

Sie waren Architekturentscheidungen.

Mehr als einmal hatte ich eine andere Lösung als die, die mir die Modelle vorgeschlagen hatten.

Das ist für mich ein wichtiger Punkt beim Einsatz von AI.

Ich wollte nicht das Modell davon überzeugen, dass seine Lösung richtig ist.

Ich wollte ein System, in dem das Modell so viel Arbeit wie möglich zuverlässig erledigt und ich dort eingreife, wo tatsächlich eine Entscheidung notwendig ist.


9. Verification Gates

Aus diesem Prinzip wurde schließlich ein weiterer Layer:

Verification Gates.

Nicht nur Tests.

Mehrere unabhängige Prüfungen.

Typecheck.

Tests.

Lint.

Spelling.

Weitere projektspezifische Checks.

Diese Checks werden nicht einfach irgendwo in CI ausgeführt und dann vergessen.

Sie sind Teil des Entwicklungsprozesses.

Heute wird beispielsweise ein PreToolUse Hook vor einem git commit ausgeführt und startet jig verify --json. Schlägt ein Gate fehl, wird der Commit blockiert. Ein nicht parsebares Verdict gilt bewusst als failure; ein bewusst gesetzter Bypass bleibt sichtbar.

Das ist wichtig:

Die Kontrolle darf nicht nur existieren. Sie muss den Workflow tatsächlich kontrollieren.

Ein Test, den niemand ausführt, ist keine Kontrollinstanz.

Ein Review, das theoretisch stattfinden sollte, aber bei Zeitdruck übersprungen wird, ist keine Kontrollinstanz.

Ein Gate muss an der Stelle sitzen, an der eine fehlerhafte Änderung sonst weiterlaufen würde.


10. Auch die Gates selbst müssen überprüft werden

Dabei ist etwas passiert, das ich für besonders lehrreich halte.

Ein Gate kann selbst falsch sein.

bun test meldete beispielsweise einen Exit Code 99, obwohl alle 714 Tests erfolgreich waren.

Der Grund war ein nicht geschlossenes PGlite-Instance.

Das Gate wertete deshalb einen erfolgreichen Testlauf als failure. Die Ursache wurde isoliert: Suites mit PGlite beendeten den Prozess mit 99, Suites ohne PGlite mit 0; ein einzelnes close() änderte den Exit Code. Anschließend wurden die 13 Erzeugungsstellen über einen gemeinsamen Test-Harness geführt.

Das ist kein nebensächliches Detail.

Ein Kontrollsystem muss selbst zuverlässig sein.

Ein Gate, das bei gutem Code rot wird, wird irgendwann ignoriert.

Und ein Gate, das nicht zuverlässig zwischen pass und fail unterscheiden kann, ist schlimmer als gar kein Gate, weil es falsches Vertrauen erzeugt.


11. Die Codebasis wuchs trotzdem weiter

Bis hierhin hatte ich ein Problem gelöst:

Wie verhindere ich, dass ich selbst jede Zeile reviewen muss?

Aber Pulse wurde weiterentwickelt.

Die Codebasis wurde größer.

Mehr Module.

Mehr Specifications.

Mehr Tests.

Mehr Architekturregeln.

Mehr historische Entscheidungen.

Mehr Abhängigkeiten zwischen Komponenten.

Und damit entstand ein anderes Problem.

Nicht mein Review-Bottleneck.

Context.


12. Ein LLM kann nicht einfach „alles wissen“

Irgendwann war das Regelwerk zusammen mit Source Code, Specifications und bestehender Architektur zu groß.

Das Problem ist dabei nicht nur die Anzahl der Tokens.

Mehr Context bedeutet nicht automatisch besseren Context.

Ein Modell kann zehntausende Zeilen Code bekommen und trotzdem genau die eine relevante Dependency übersehen.

Oder eine bestehende Implementation nicht berücksichtigen.

Oder eine Regel kennen, aber sie im relevanten Moment nicht in seine Entscheidung einbeziehen.

Genau diese Problematik beschreibt auch die heutige Dokumentation des Retrieval-Systems: Mehr generated code erhöht nicht automatisch die Softwarequalität; typische Probleme sind duplicated implementations, local rather than architectural reasoning, inconsistent abstractions und Fehler durch fehlenden Project Context. Entscheidend ist deshalb context quality, nicht einfach context quantity.

Damit war für mich klar:

Ich brauche kein System, das dem Modell mehr Code gibt.

Ich brauche ein System, das dem Modell den richtigen Code gibt.


13. Jig

An diesem Punkt entstand Jig.

Jig ist ein eigenes Retrieval-System für den Source Code eines Projekts.

Es ist nicht der Ersatz für TDD.

Es ist nicht der Ersatz für den Reviewer.

Und es ist auch nicht die Antwort auf mein persönliches Review-Bottleneck.

Jig löst das Context-Problem der wachsenden Codebasis.

Die Idee ist relativ einfach:

Der Source Code wird lokal analysiert und als semantisch durchsuchbarer Bestand verfügbar gemacht.

Jig verwendet lokale inference für embeddings und eine vector database. Ein Retrieval kann dadurch nicht nur nach Dateinamen oder exakten Begriffen suchen, sondern nach der Bedeutung einer Aufgabe.

Noch wichtiger ist die Form des Ergebnisses.

Jig soll nicht einfach sagen:

„Hier sind fünf Dateien.“

Es liefert gewichteten relevanten Source Context und kann abhängige beziehungsweise zusammenhängende Stellen einbeziehen.

Damit kann ein LLM beispielsweise bei einer Änderung nicht nur die Datei sehen, in der die gesuchte Funktion steht.

Es kann auch die relevanten Interfaces, Caller, Abhängigkeiten und angrenzenden Implementierungen erhalten.

Das Ziel ist ausdrücklich nicht, das gesamte Projekt in den Context zu laden.

Die Dokumentation formuliert es sehr deutlich:

The objective is not to put the entire project into the context window.

Sondern:

The objective is to provide better context.

Das ist der entscheidende Unterschied.


14. Jig wurde damit zu einer weiteren Kontrollschicht

Mit Jig konnte ich das Problem anders betrachten.

Vorher musste das Modell seine Architekturkenntnis aus dem erhaltenen Context rekonstruieren.

Jetzt kann ein Teil dieser Arbeit durch Retrieval unterstützt werden.

Die Architektur bleibt dabei bewusst lokal und austauschbar. Inference, embeddings, vector search und project retrieval sollen lokal laufen, Modelle und Retrieval-Komponenten sollen replaceable bleiben und die Entscheidungen sollen experimentell anhand beobachtbaren Verhaltens validiert werden.

Das ist wichtig, weil ich Jig nicht als magische AI-Komponente betrachte.

Es ist Infrastruktur.

Und wie jede Infrastruktur muss sie messbar und austauschbar sein.


15. Der Workflow ist damit mehr als „AI schreibt Code“

Der eigentliche Entwicklungsprozess sieht inzwischen eher so aus:

                  HUMAN
                    │
                    │ System Design
                    │ Specifications
                    │ Architecture
                    │ Constraints
                    ▼
             ┌──────────────┐
             │ Specification│
             └──────┬───────┘
                    │
                    ▼
              ┌─────────────┐
              │    TDD      │
              │ Test first  │
              └─────┬───────┘
                    │
                    ▼
              ┌─────────────┐
              │ Implementer │
              └─────┬───────┘
                    │
                    ▼
              ┌─────────────┐
              │  Reviewer   │
              └─────┬───────┘
                    │
             ┌──────┴──────┐
             │             │
          AGREE         DISAGREE
             │             │
             ▼             ▼
          proceed         HUMAN
                           │
                           ▼
                      architecture
                       decision

                    │
                    ▼
             Verification Gates
                    │
                    ▼
                  Commit

Und über diesem gesamten Prozess liegen die Regeln aus CLAUDE.md.

Bei wachsendem Project Context kommt Jig dazu:

                 Project
                    │
          ┌─────────┴─────────┐
          │                   │
       Source              Specs
          │                   │
          └─────────┬─────────┘
                    ▼
                   Jig
                    │
          relevant source context
          + weighted dependencies
                    │
                    ▼
             Implementer / Reviewer

Das ist kein einzelner Trick.

Es ist ein System aus Kontrollmechanismen.


16. Die Git History zeigt, dass das keine nachträgliche Erzählung ist

Das Entscheidende an Pulse ist für mich deshalb auch die Git History.

Die Architektur und die Engineering-Prinzipien stehen nicht nur in einer nachträglich geschriebenen Dokumentation.

Man kann ihre Entstehung im Repository nachvollziehen.

Pulse wurde am 23. Juni als hexagonal HR report generator gebootstrapped. Kurz danach kamen Architecture-Dokumentation, per-module Specifications, structural tests und output integrity verification.

Danach wurde das System bewusst vom konkreten HR Use Case weg in einen generic report engine umgebaut. Die History enthält unter anderem den generic bun generate CLI, Integration Tests für assembleDocx und generateReport, die Migration von Storage und File-IO auf das generic data model und das Entfernen der alten HR-specific schema.

Auch später wurde nicht einfach „weitergebaut“.

Specifications wurden weiterentwickelt, wenn sich das System änderte.

Ein gutes Beispiel ist die docx-assembly Specification: Sie wurde explizit auf definition-driven assembly umgeschrieben, bevor die Implementierung entsprechend weiterentwickelt wurde.

Das ist der Punkt, der mir bei der Betrachtung von AI-assisted development wichtig ist:

Die Architecture war nicht das Ergebnis dessen, was das LLM zufällig produziert hat.

Das LLM wurde innerhalb einer Architecture eingesetzt, die ich vorher definiert hatte.


17. Das bedeutet nicht, dass alles beim ersten Versuch funktioniert hat

Im Gegenteil.

Es gab Fehler.

Und genau das ist Teil des Experiments.

Ein gutes Beispiel ist die Migration des Datenmodells.

Die Tests waren grün, obwohl die Tests eine flache Fixture-Struktur verwendeten, während die reale Pipeline eine andere Context-Struktur erzeugte. Der Fehler konnte deshalb über längere Zeit bestehen bleiben. Die spätere Korrektur bestand nicht darin, einfach noch einen Test hinzuzufügen, sondern die Teststruktur an die reale Pipeline-Struktur anzupassen.

Ein anderes Beispiel war die Migration selbst.

Am 19. August wurde eine Änderung zurückgerollt, weil sie auf realen Reports beim Startup fehlschlug. Der Fehler ließ sich zunächst nicht reproduzieren. Statt eine unklare Diagnose weiter in die Codebasis zu tragen, wurde bewusst auf den letzten bekannten funktionierenden Stand zurückgegangen.

Später wurde dafür ein eigenes migrations gate gebaut, das tatsächlich den Upgrade Path einer installierten Anwendung durchläuft: Baseline-Schema, Migration, Startup-Sequenz und anschließend Lesen der Daten. Gleichzeitig wird ausdrücklich dokumentiert, dass dieses Gate eine bestimmte historische Failure noch nicht reproduziert. Es schließt also die konkrete Lücke, die es tatsächlich schließt — und behauptet nicht mehr.

Genau so soll ein Kontrollsystem funktionieren.

Nicht:

„Wir haben jetzt einen Test, Problem gelöst.“

Sondern:

„Dieser Kontrollmechanismus beweist genau X. Y ist weiterhin nicht abgedeckt.“

18. Das ist der eigentliche Unterschied

Wenn man sich nur die fertige Codebasis ansieht, sieht man:

  • TypeScript
  • React
  • PGlite
  • DOCX generation
  • Charts
  • Formula language
  • LLM integration
  • Validation
  • Import/export
  • Tests
  • Security
  • ungefähr 55.000 Zeilen Code

Was man nicht sieht, ist der größere Teil der eigentlichen Entwicklungsarbeit.

Man sieht nicht die Specifications.

Man sieht nicht die Architekturentscheidungen, die vor der Implementierung getroffen wurden.

Man sieht nicht die Regeln, die das LLM während der Implementierung begrenzen.

Man sieht nicht den TDD-Prozess.

Man sieht nicht, dass ein Modell eine Implementation erzeugt und ein anderes Modell sie anhand von Specification und Test überprüft.

Man sieht nicht die Fälle, in denen beide Modelle sich widersprechen und ich entscheiden muss.

Und man sieht nicht den Context-Engineering-Layer, der später notwendig wurde, als die Codebasis groß genug wurde, dass selbst ein gut definiertes Regelwerk zusammen mit dem Source Code nicht mehr zuverlässig in den Arbeitskontext eines Modells passte.

Das ist der eigentliche Entwicklungsprozess.


19. Ich habe nicht versucht, das LLM besser zu machen

Das war für mich eine wichtige Erkenntnis.

Ich musste das Modell nicht dazu bringen, plötzlich ein besserer Software Engineer zu werden.

Ich habe stattdessen den Entwicklungsprozess um seine Schwächen herum gebaut.

Wenn ein LLM dazu neigt, vorhandene Logik zu duplizieren, gibt es eine harte Regel und einen Workflow, der nach bestehenden Implementierungen sucht.

Wenn es lokal statt architektonisch denkt, bekommt es Specifications, Architecture und gezielten Context.

Wenn es eine plausible Implementierung schreibt, bevor es das Problem ausreichend verstanden hat, zwinge ich es durch TDD dazu, zuerst eine überprüfbare Vorstellung des gewünschten Verhaltens zu formulieren.

Wenn eine einzelne Modellinstanz ihre eigene Arbeit zu unkritisch beurteilt, bekommt sie einen Reviewer.

Wenn ein Reviewer ebenfalls falsch liegt, gibt es einen zweiten Modelltyp.

Wenn die Modelle sich widersprechen, landet die Entscheidung bei mir.

Wenn ein Test grün ist, aber der Production Flow nicht funktioniert, gibt es zusätzliche Verification.

Wenn die Codebasis zu groß wird, um zuverlässig relevanten Context bereitzustellen, gibt es Retrieval.

Jede Schicht hat damit eine konkrete Aufgabe.


20. Der Mensch verschwindet nicht aus dem Prozess

Das ist vielleicht der Punkt, der bei solchen Projekten am häufigsten falsch verstanden wird.

Ich habe nicht versucht, den Menschen aus dem Entwicklungsprozess zu entfernen.

Ich habe versucht, menschliche Aufmerksamkeit dort einzusetzen, wo sie den größten Wert hat.

Am Anfang war ich der Reviewer jeder Zeile.

Das war präzise, aber nicht skalierbar.

Mit dem Implementer/Reviewer-Modell musste ich hauptsächlich Disagreements untersuchen.

Das ist eine wesentlich bessere Verwendung meiner Zeit.

Denn wenn zwei Modelle, die jeweils Specification, Tests und Implementation kennen, beide dieselbe Lösung akzeptieren, gibt es wesentlich weniger Grund, jede einzelne Zeile noch einmal manuell zu überprüfen.

Wenn sie sich widersprechen, wird es interessant.

Dann muss ich entscheiden.

Und genau dort liegt weiterhin die schwierige Arbeit.

Nicht:

„Ist foo() richtig geschrieben?“

Sondern:

„Welche dieser beiden Architekturen ist für dieses System die richtige?“

Das kann mir ein LLM abnehmen.

Aber ich möchte diese Entscheidung nicht blind delegieren.


21. Was ich mit Pulse eigentlich beweisen wollte

Das Ziel war nie:

„Schaut her, ich kann mit Claude schnell 55.000 Zeilen schreiben.“

Das wäre kaum interessant.

Das Ziel war:

Kann ich LLMs so in einen Software-development workflow integrieren, dass ihre offensichtlichen Schwächen systematisch aus dem Prozess entfernt werden?

Ich glaube inzwischen, dass die Antwort darauf ja lautet.

Nicht weil die Modelle plötzlich zuverlässig geworden wären.

Sondern weil ich aufgehört habe, Zuverlässigkeit von einer einzelnen Modellantwort zu erwarten.

Ich verlasse mich nicht auf:

LLM → Code

sondern auf:

Human design
      ↓
Specification
      ↓
TDD
      ↓
Implementation
      ↓
Independent review
      ↓
Verification
      ↓
Commit

und bei wachsendem Context:

Project
   ↓
Jig
   ↓
Relevant context
   ↓
Implementation / Review

Das ist ein völlig anderes Modell von AI-assisted development.


22. Warum das plötzlich so schnell gehen kann

Damit lässt sich auch erklären, warum eine solche Codebasis in relativ kurzer Zeit entstehen konnte.

Ein LLM kann Code wesentlich schneller produzieren, als ein Mensch ihn schreiben kann.

Das allein führt aber nur zu mehr Code.

Damit daraus mehr Software entsteht, müssen die Kontrollmechanismen ebenfalls skalieren.

Genau das habe ich versucht.

Ich habe die Arbeit in verschiedene Arten von Entscheidungen zerlegt:

Das Modell kann sehr gut übernehmen

  • Boilerplate
  • Implementierung
  • Tests
  • Refactoring
  • lokale Fehleranalyse
  • Code navigation
  • Dokumentationsänderungen
  • repetitive Änderungen
  • Regression Tests
  • Verifikation

Der Workflow kontrolliert

  • Architekturgrenzen
  • bestehende Abstraktionen
  • Testbarkeit
  • tatsächliche Integration
  • Regelkonformität
  • Duplicate logic
  • Root causes
  • Regressionen
  • Commit-Grenzen

Bei mir bleiben

  • System Design
  • Requirements
  • Specifications
  • Architekturentscheidungen
  • Trade-offs
  • Entscheidungen bei Disagreement
  • die Frage, ob eine Lösung grundsätzlich die richtige ist

Das ist eine viel bessere Aufteilung der Arbeit.


23. Der vielleicht wichtigste Effekt

Die interessanteste Veränderung betrifft deshalb gar nicht die Geschwindigkeit, mit der Code geschrieben wird.

Sie betrifft die Skalierung menschlicher Aufmerksamkeit.

Ein Mensch kann nicht gleichzeitig 55.000 Zeilen Code im Kopf behalten.

Ein LLM kann zwar wesentlich mehr davon verarbeiten, aber auch ein LLM kann nicht zuverlässig garantieren, dass jeder relevante Zusammenhang berücksichtigt wird.

Deshalb brauche ich Strukturen, die dafür sorgen, dass die richtigen Informationen zur richtigen Zeit zusammenkommen.

Architecture reduziert den notwendigen Context.

Specifications definieren die Bedeutung.

TDD macht Erwartungen explizit.

CLAUDE.md hält Invarianten fest.

Implementer und Reviewer erzeugen gegenseitige Kontrolle.

Verification Gates verhindern, dass Fehler einfach weiterlaufen.

Jig verbessert den Context, wenn die Codebasis dafür zu groß wird.

Und ich selbst konzentriere mich auf die Entscheidungen, die diese Mechanismen nicht zuverlässig automatisieren können.

Das ist für mich der Kern von AI-assisted software engineering.


24. Ich habe kein LLM-Team simuliert

Man könnte den Prozess als eine Art virtuelles Entwicklerteam beschreiben.

Aber eigentlich trifft das den Punkt nicht ganz.

Ich habe nicht versucht, Menschen durch mehrere Chatbots zu imitieren.

Ich habe Kontrollmechanismen für probabilistische Softwareentwicklung gebaut.

Das ist ein Unterschied.

Ein klassischer Compiler ist deterministisch.

Ein LLM ist es nicht.

Ein klassischer Build-Prozess kann mit relativ wenigen festen Regeln beschreiben, was gültig ist.

Bei einem LLM muss ich zusätzlich kontrollieren, ob es das Problem überhaupt richtig verstanden hat.

Deshalb brauche ich mehrere unabhängige Ebenen.

Und genau deshalb ist die Kombination aus Specification, TDD und Review für mich so wichtig.


25. Pulse ist damit auch ein Experiment über die Grenzen von LLMs

Die Entwicklung hat mir nicht gezeigt, dass LLMs Software Engineering ersetzen.

Sie hat mir etwas Interessanteres gezeigt:

Die Grenze liegt weniger darin, wie viel Code ein LLM schreiben kann, sondern darin, wie zuverlässig man es dazu bringen kann, eine bestehende Softwarearchitektur über längere Zeit konsistent weiterzuentwickeln.

Das ist ein anderes Problem.

Und es ist ein lösbares Problem.

Nicht durch einen besseren Prompt.

Nicht durch „be more careful“.

Sondern durch Engineering.

Specifications.

Architecture.

Tests.

Gates.

Review.

Retrieval.

Und klare Zuständigkeiten.


Fazit

Pulse ist für mich deshalb nicht primär ein Beispiel dafür, wie schnell ein LLM Code erzeugen kann.

Es ist ein Beispiel dafür, wie man einen Entwicklungsprozess um ein LLM herum konstruiert.

Ich habe die Architektur nicht dem Modell überlassen.

Ich habe die Specifications nicht nachträglich aus dem Code geschrieben.

Ich habe nicht jede Modellentscheidung akzeptiert.

Ich habe das Modell gezwungen, vor der Implementation eine überprüfbare Vorstellung vom Problem zu entwickeln.

Ich habe TDD als Teil des Agent-Workflows verwendet.

Ich habe harte Regeln definiert, um typische LLM-Fehler wie duplicate implementations, lokale statt architektonische Lösungen und symptomatische Fixes zu verhindern.

Ich habe mehrere Modelle als Implementer und Reviewer gegeneinander eingesetzt.

Ich habe meinen eigenen Review-Aufwand von „jede Zeile kontrollieren“ auf „Disagreements untersuchen“ reduziert.

Und als die Codebasis so groß wurde, dass Source Code und Regeln selbst zum Context-Problem wurden, habe ich mit Jig eine weitere Schicht gebaut, die den relevanten Context aus dem Projekt zurückliefert.

Das Entscheidende daran ist:

Ich habe nicht versucht, ein LLM dazu zu bringen, so zuverlässig wie ein guter Software Engineer zu sein.

Ich habe einen Prozess gebaut, in dem es das nicht sein muss.

Es darf Fehler machen.

Es darf etwas übersehen.

Es darf sogar nur in einem großen Teil der Fälle richtig liegen.

Entscheidend ist, dass Fehler nicht unsichtbar bleiben.

Wenn 80 Prozent der Arbeit automatisch zuverlässig erledigt werden können und die verbleibenden 20 Prozent so durch den Prozess laufen, dass sie sichtbar werden, ist das bereits eine enorme Skalierung.

Denn dann besteht meine Arbeit nicht mehr darin, 100 Prozent der erzeugten Software selbst zu produzieren oder zu überprüfen.

Meine Arbeit besteht darin, das System zu entwerfen, das die 100 Prozent kontrolliert.

Und genau das war bei Pulse von Anfang an das eigentliche Ziel.


Manfred Michaelis / Pulse · GitLab
GitLab Community Edition

Pulse Repository