Codeverständnis
Code verstehen, um besseren Code zu generieren
Beim Aufbau von FiboCode wurde mir wichtiger, mein eigenes Projekt zu verstehen. Gedanken zu natürlicher Sprache, Architektur und der Arbeit mit AI.
Ich mag das Gefühl nicht, mein eigenes Projekt nicht zu verstehen.
Die Anforderungen kamen von mir. Die Funktionen waren die, die ich wollte. Nachdem AI den Code generiert hatte, habe ich auch geprüft, ob er funktioniert. Aber wenn mich jemand fragt, welche Module eine Funktion durchläuft, warum sie so umgesetzt wurde oder was eine weitere Änderung beeinflussen könnte, kann ich nicht immer eine klare Antwort geben.
Bei der Entwicklung von FiboCode ist mir das immer wichtiger geworden.
Natürlich möchte ich, dass AI mir mehr Code abnimmt. Aber ich will an diesem Projekt weiterarbeiten. Ist die heutige Funktion fertig, kommt morgen die nächste Idee. Ich möchte nicht vor jeder Änderung hoffen müssen, dass dabei nichts anderes kaputtgeht.
A ist repariert, dafür macht B Probleme. Nach der Reparatur von B taucht in A ein neuer Fehler auf. Dann liegt es nahe, die Symptome wieder an AI zu schicken und auf die nächste Erklärung und Korrektur zu warten.
Nach ein paar solchen Runden würde ich mich allerdings immer unsicherer fühlen. Jede einzelne Änderung klingt vernünftig. Was ist aus dem System geworden, wenn man sie zusammennimmt?
Das möchte ich verstehen. Je länger ich dieser Frage nachgehe, desto enger scheinen mir Codeverständnis und Codegenerierung zusammenzuhängen.
Programmieren in natürlicher Sprache hat mich auf einen Gedanken gebracht
Von Maschinensprache über Assembler bis zu höheren Programmiersprachen haben wir immer weiter abstrahiert.
Viele Details, um die wir uns früher selbst kümmern mussten, übernehmen heute Werkzeuge. Wir können mit weniger Code komplexere Funktionen ausdrücken und müssen nicht jedes Mal ganz unten anfangen.
Jetzt gehen wir noch einen Schritt weiter: Wir beschreiben Anforderungen in natürlicher Sprache und lassen AI den Code erzeugen.
Auf den ersten Blick sieht das vielleicht nur nach einer neuen Ausdrucksweise aus. Aber ich finde, dabei verändert sich etwas Wesentliches.
Assembler und Compiler übersetzen Code nach festgelegten Regeln. Programme können natürlich Fehler haben. Doch die Übersetzung muss nicht erraten, was ich eigentlich vorhabe.
Das Modell muss raten.
Aus meinen Worten, dem sichtbaren Code und dem Kontext leitet es ab, was ich möchte, und wählt eine Umsetzung. Derselbe Satz kann zu unterschiedlichen Ergebnissen führen. Selbst ein gleichbleibendes Ergebnis kann konsequent eine Anforderung umsetzen, die ich nie klar formuliert habe.
Später stieß ich auf einen Artikel von Birgitta Böckeler, der diese Unterscheidung ebenfalls aufgreift: Große Sprachmodelle erzeugen Ergebnisse durch Schlussfolgerungen; man kann sie nicht einfach als Compiler für natürliche Sprache behandeln. Das lag nah an meinen eigenen Überlegungen.
Was können wir also tun?
Klarere Anforderungen, festgehaltene Konventionen und Tests für wichtige Verhaltensweisen dürften einige Abweichungen verringern. Beschreibt der Entwurf bereits, wie Module zusammenarbeiten und welche Klassen und Methoden beteiligt sind, hat AI mehr Anhaltspunkte.
Die Dokumentation muss allerdings zum Code passen. Eine ausführliche Beschreibung eines längst geänderten Entwurfs kann mehr Schwierigkeiten machen als gar keine Dokumentation.
Ich glaube auch nicht, dass diese Maßnahmen jede Unsicherheit aus der Generierung nehmen. Aber sie lassen dem Modell weniger Dinge, die es spontan erraten muss.
Weiß ich überhaupt, was ich ändern will?
Wenn ich diesen Gedanken weiterverfolge, stoße ich auf ein anderes Problem: Auch Menschen tun sich schwer damit, Anforderungen klar zu beschreiben.
Besonders dann, wenn sie den Code nicht kennen.
Vielleicht weiß ich genau, wo ich eine zusätzliche Schaltfläche haben möchte oder welcher Vorgang einfacher werden soll. In welches vorhandene Modul das gehört, welche Verarbeitungsschritte nötig sind und welches bisherige Verhalten erhalten bleiben muss, weiß ich womöglich nicht.
Was soll AI in dieser Lage damit anfangen, wenn ich immer wieder sage: „Beeinflusse keine anderen Funktionen“?
„Vereinheitliche das hier“ kann zum Beispiel eine einheitliche Darstellung meinen oder das Zusammenführen der Datenverarbeitung. Je nach Auslegung ist der Umfang der Änderungen ein ganz anderer.
Wenn ich selbst die Grenzen nicht erklären kann, muss das Modell sie aus dem ableiten, was es sieht. Es macht vielleicht mehr als erwartet oder umgeht eine Konvention, von deren Existenz ich nichts wusste.
Dann fällt mir ein Problem auf und ich lasse es reparieren. Mit jeder weiteren Änderung passen die ursprüngliche Anforderung, später ergänzte Einschränkungen und bestehende Beziehungen im Code womöglich nicht mehr vollständig in den Kontext.
Ich fürchte, am Ende könnte Code entstehen, der stellenweise funktioniert, aber voller Überbleibsel früherer Reparaturen ist. Es wirkt, als wäre nur noch AI bereit, daran weiterzuarbeiten. Menschen trauen sich kaum noch heran.
Solcher Code erinnert mich an AI slop. Noch schwieriger ist, dass auch AI nicht zwangsläufig das gesamte System versteht, wenn wir es ihr wieder überlassen. Eine weitere Erklärung bedeutet nicht, dass wir es nun endlich verstanden haben.
Deshalb denke ich immer häufiger: Um eine Anforderung präzise zu formulieren, muss ich zunächst den groben Aufbau des Projekts kennen.
Ich muss nicht sofort jedes Detail verstehen. Aber ich sollte wissen, wo die gewünschte Änderung ansetzen wird.
Ich möchte auf der Architekturebene mit AI sprechen
Ich halte es nicht für eine Voraussetzung der Arbeit mit AI, jede generierte Zeile von Hand zu prüfen.
Wenn ein Projekt komplex wird, ist die eigene Aufmerksamkeit begrenzt. Ich möchte zuerst den Rahmen verstehen: Welche Kernmodule gibt es? Wofür sind sie zuständig? Wo beginnt eine Funktion, welche Verarbeitung durchläuft sie und wo verändert sie Daten und Zustände?
Über der Implementierung in einer höheren Programmiersprache möchte ich also eine weitere Ebene des Systemverständnisses aufbauen. Vorerst nenne ich sie „Architekturebene“. Am Namen hänge ich nicht besonders.
Wichtig ist, dass diese Ebene mit dem tatsächlichen Code verbunden bleibt.
Sehe ich ein Modul, kann ich seine Implementierung finden. Sehe ich einen Funktionsablauf, weiß ich, welche Klassen und Methoden er durchläuft. Will ich ein Verhalten ändern, kann ich den Beziehungen zu den relevanten Stellen folgen.
So habe ich konkretere Dinge, die ich mit AI besprechen kann.
Wenn ich etwa weiß, dass ein Vorgang bereits eine gemeinsame Aufgabenwarteschlange nutzt, kann ich besprechen, ob die neue Fähigkeit denselben Weg nehmen sollte. Fügt AI eine weitere Zustandsverwaltung hinzu, kann ich fragen: Warum reicht die vorhandene nicht? Können sich die beiden später gegenseitig beeinflussen?
Ohne diese Fragen würde ich vielleicht jede lauffähige Umsetzung akzeptieren. So habe ich wenigstens die Chance, früh zu erkennen, wenn etwas unnötig kompliziert wird.
Ich glaube, durch solche Vergleiche wird auch mein eigener Code besser.
Für eine Funktion gibt es mehrere Lösungswege. Warum hat dieses Projekt gerade diesen gewählt? Eine Abstraktion sieht elegant aus, aber wenn jede Änderung viele Stellen berührt, hilft sie dann wirklich? Ein anderes Modul ist schlicht geschrieben und hat mehrere neue Anforderungen überstanden, ohne andere Teile zu stören. Was wurde dort richtig gemacht?
Es lohnt sich, darüber nachzudenken. Mit eigenem Urteilsvermögen kann ich AI leichter sagen, was geändert werden sollte, und auch einen anderen Vorschlag besser verstehen.
Natürlich kann auch mein Architekturverständnis falsch sein. Verbindungen in einem Diagramm muss ich am Code überprüfen. Funktionen, die ich für unberührt halte, brauchen Tests und praktische Überprüfung. Bei wichtigen Zustandsänderungen und schwierigen Grenzfällen sehe ich mir die Implementierung genauer an.
Ich möchte allmählich dahin kommen, meistens zu wissen, wo ich nachsehen muss, und zugleich zu erkennen, was ich noch nicht verstanden habe.
Das ist einer der Gründe, warum ich die Codeanalyse in FiboCode entwickle.
Ich möchte Code, Dokumente und Beziehungsdiagramme verbinden. Sie sollen helfen, in einer fremden Implementierung die Struktur zu erkennen und dem Ablauf einer Funktion zu folgen. Mit diesem Verständnis würde ich Änderungen mit AI gelassener besprechen.
In What Is Code? beschreibt Unmesh Joshi ebenfalls, wie gute Abstraktionen, Typen, Tests und ausdrückliche Regeln einem Modell beim Verstehen und Ändern von Code helfen können. Diesen Gedanken finde ich gut: Unser Systemverständnis sollte möglichst im Projekt erhalten bleiben, damit die nächste Änderung darauf aufbauen kann.
Etwas Zeit möchte ich weiterhin mit Lesen von Code verbringen
AI schreibt schnell. Da ist es verlockend, ihr auch gleich die nächste Aufgabe zu geben.
Eine Funktion nach der anderen wird fertig, und das Projekt scheint voranzukommen. Aber ich frage mich auch, was ich nach all dieser Arbeit selbst besser verstanden habe.
Wenn ich nur Anforderungen abgebe und Ergebnisse annehme, bis das Projekt zu komplex wird, verliere ich später womöglich viel Zeit mit Reparaturen oder einem Neustart. Die eingesparte Zeit wäre wieder weg, dazu kämen Modellkosten und die Mühe, eine ganze Implementierung erneut kennenzulernen.
Einen Teil dieser Zeit würde ich lieber in Ruhe mit dem Code verbringen.
Es muss nicht viel auf einmal sein. Ich kann zunächst verstehen, warum ein Modul so aufgeteilt wurde, warum eine Funktion diese Schritte braucht oder welche mir unbekannte Technik AI diesmal verwendet hat. Verstehe ich etwas nicht, frage ich nach, suche Belege im Code und probiere eine kleine Änderung aus, um mein Verständnis zu prüfen.
Bei der Recherche bin ich auf ein Lernexperiment von Anthropic gestoßen. Nach unbekannten Programmieraufgaben mit AI erzielten die Teilnehmenden bei einem unmittelbar folgenden Test im Mittel schlechtere Ergebnisse als die Gruppe, die von Hand programmierte. Das kleine Experiment sagt nichts darüber aus, wie sich Fähigkeiten langfristig entwickeln. Aber es hat mich darin bestärkt, darauf zu achten, ob ich nach einer erledigten Aufgabe tatsächlich etwas gelernt habe.
Ich hoffe, dieses Verständnis sammelt sich nach und nach an. Bei einem ähnlichen Entwurf erkenne ich später vielleicht die Zusammenhänge. Schlägt AI eine Lösung vor, kann ich eine begründete Frage stellen.
Ob FiboCode dabei wirklich gut helfen kann, muss ich weiter herausfinden. Aber genau darin liegt ein großer Teil meiner Motivation für die Codeanalyse.
Ich möchte weiter mit AI programmieren. Und ich möchte auch dann noch erklären können, wie mein Projekt funktioniert, wenn es größer geworden ist.
Die Idee als einfache Darstellung:
Weitere passende Lektüre
- Googles Leitfaden für Code-Reviews: Besonders die Überlegungen zu Gesamtentwurf und Komplexität lohnen einen Vergleich mit dem eigenen Projekt.
- Thoughtworks über kognitive Schulden in Codebasen beschreibt, wie Implementierung und gemeinsames Verständnis auseinanderdriften können.
- Die Forschungsarbeit zum Lernexperiment von Anthropic erläutert die Methoden und Ergebnisse genauer.