Die Verbindung von TDD (Test-Driven Development) und LLMs
- TDD ist eine Entwicklungsmethodik, bei der vor dem Beginn der eigentlichen Programmierung zunächst umfassende Unit-Tests geschrieben werden.
- Da die Tests faktisch die Rolle einer Spezifikation übernehmen, kann man die Korrektheit des Codes bis zu einem gewissen Grad nachweisen, wenn am Ende alle Tests erfolgreich durchlaufen.
- Traditionell wurde TDD auch dafür kritisiert, die Produktivität zu bremsen oder ineffizient zu sein.
- Mit dem Aufkommen von LLMs sind das Schreiben von Tests und die iterative Überarbeitung von Code jedoch deutlich einfacher geworden.
Wie ich LLMs normalerweise nutze
- Ich habe Tools wie GitHub Copilot aktiv genutzt.
- LLMs sind gut darin, wiederkehrende Muster zu erkennen und die nächsten paar Zeilen automatisch zu vervollständigen, haben aber oft Schwierigkeiten, ein Problem in seiner Gesamtheit tief zu verstehen und auf Anhieb ein in sich abgeschlossenes Modul zu erzeugen.
- Wenn man zu viel Kontext bereitstellt, neigt das Modell eher dazu, vom Thema abzuweichen.
- Wenn man Informationen je nach Bedarf nur teilweise bereitstellt, etwa Fehlermeldungen, kann das Modell auch beim Debugging hervorragend helfen.
- Ich habe gespürt, dass durch das wiederholte Copy-and-Paste zwischen IDE, Terminal und Chat-Interface Reibung entsteht.
Lässt sich das automatisieren?
- Um diesen Prozess zu automatisieren, habe ich selbst das Konzept einer event loop eingeführt.
- Gibt man im ersten Prompt die Spezifikation der zu implementierenden Funktion und ihre Funktionssignatur an, liefert das Modell einen Entwurf für Unit-Tests und Code.
- Dieser Code wird im Verzeichnis
sandbox gespeichert und anschließend automatisch go test ausgeführt.
- Wenn die Tests fehlschlagen, werden im zweiten Prompt — der Schleife — der vorhandene Code und die Testergebnisse zusammen übermittelt, also Compilerfehler oder Fehlerinformationen.
- Auf dieser Grundlage schlägt das Modell überarbeitete Tests und Implementierungscode erneut vor.
- Dieser Vorgang wird wiederholt, bis alle Tests erfolgreich durchlaufen.
- Dieser Ansatz ermöglicht schrittweise Verbesserungen, ohne übermäßig viel Kontext anzusammeln.
- Es kann vorkommen, dass das Modell am selben Testfall wiederholt scheitert; dann weist ein Mensch direkt auf die problematische Stelle hin und gibt einen Hinweis.
- Man muss sich des Problems einer „fehlenden Aufsicht“ bewusst sein: Es ist fraglich, ob die vom LLM erzeugten Tests streng genug sind.
- Code und Tests könnten denselben Fehler oder dasselbe unvollständige Design gemeinsam teilen.
- Deshalb ist es wichtig, dass ein Mensch zusätzliche Testfälle ergänzt und verstärkt.
- Falls nötig, kann man auch Techniken wie Mutation Testing mithilfe von AI erproben.
LLM-basierte Entwicklung und kognitive Belastung (cognitive load)
- Bei der Anwendung von TDD zusammen mit LLMs ist zu erwarten, dass dies nicht nur bei allgemeinen Algorithmusproblemen funktioniert, sondern auch in realen Codebases mit tatsächlichen Abhängigkeiten.
- Voraussetzung ist allerdings, dass die Projektstruktur in kleinere Einheiten aufgeteilt wird, um die Wartbarkeit zu erhöhen, und dass jedes Verzeichnis bzw. Package unabhängig testbar ist.
- Um die kognitive Belastung zu verringern, wird empfohlen, jedes Package in eine Datei mit den zentralen Typdefinitionen (
shared.go), in Dateien für die jeweilige Logik (x.go) sowie in Tests (x_test.go) aufzuteilen.
- Statt dem Modell bei der Nutzung von AI jedes Mal den gesamten Code zu geben, sollte man gezielt nur bestimmte Teile einbeziehen, damit sich das Modell fokussieren kann.
- Das erhöht die Testabdeckung, verringert zugleich die Kopplung zwischen Modulen und bringt auch Vorteile für die langfristige Wartung.
- Selbst bei großen Projekten sollte eine Struktur angestrebt werden, die sie in kleine, klar umrissene Einheiten zerlegt, den Kern der Logik jeder Einheit gut abbildet und ihren Umfang minimal hält.
Zum Schluss
- Wenn man das Tempo der AI-Entwicklung betrachtet, könnte schon morgen eine neue Architektur auftauchen und die Grenzen von LLMs überwinden.
- Deshalb ist es empfehlenswert, nicht überstürzt umfangreichen Legacy-Code mit mehr als 100.000 Zeilen zu refaktorisieren, sondern zunächst im kleineren Maßstab die Möglichkeiten der Verbindung von TDD und LLMs auszuloten.
- Es ist zu erwarten, dass die Verschmelzung von TDD und LLMs sowohl bei der automatischen Codegenerierung als auch beim Management der Testqualität positive Veränderungen bewirken kann.
5 Kommentare
Da fragt man sich unweigerlich, welche Pipelines andere KI-Services speziell für Entwickler wohl verwenden.
(Wenn man sich so etwas ansieht,) scheint es nur eine Frage der Zeit zu sein, bis wir für ein Gehirn-Computer-Interface elektrische Stimulationsleitungen ins Gehirn einsetzen.
Es scheint zwar gut zu sein, Testcode einzubauen, aber ich glaube nicht, dass das von dieser Person entwickelte Programm besondere Vorteile hat.
Auch in cline oder aider kann man Befehle über die Kommandozeile ausführen und die Ergebnisse erhalten, daher scheint es mir besser zu sein, in diesem Programm einfach die Prompts gut zu formulieren, wenn man die sonstige Benutzerfreundlichkeit berücksichtigt.
Der von Builder.io entwickelte Micro-Agent verfolgt einen ähnlichen Ansatz. https://github.com/BuilderIO/micro-agent Ich habe LLM und TDD ebenfalls schon mehrfach zusammen ausprobiert; dabei muss man auch die Abstraktion, etwa über ein Design System, gut hinbekommen. Außerdem müssen Konventionen und Muster sauber etabliert sein. Die Testfälle schreibe ich normalerweise selbst. (Wenn nötig auch in menschlicher Sprache?) Vor allem muss man, wie auch in diesem Text gesagt wird, Module mit geringer Kopplung und hoher Kohäsion gut entwerfen, damit sich der Kontext in das begrenzte Kontextfenster hineindrücken lässt.
LLMs können kleinen, eng begrenzten Code gut prüfen, aber bei der Gesamtarchitektur und dem Blick fürs große Ganze gibt es noch Luft nach oben.
In Kombination mit TDD wirkt der Ansatz, Schritt für Schritt Verbesserungen vorzunehmen, ziemlich gut.