LLMs als Compiler: Warum grosse Sprachmodelle keine klassischen Compiler sein sollten


Ich denke schon seit einiger Zeit immer wieder über eine bestimmte Diskussion rund um LLMs nach: Sind sie wirklich vergleichbar mit Compilern? Steuern wir auf eine Welt zu, in der Menschen den zugrunde liegenden Code ihrer Programme gar nicht mehr betrachten?

Versionen dieses Arguments gibt es seit der Aussage "English is the hottest new programming language". Informatik hat die Sprachentwicklung immer weiter vorangetrieben, indem höhere Abstraktionsebenen geschaffen wurden. Das ist die neueste Iteration: Vielleicht brauchen wir keine eigene Sprache mehr, um mit Maschinen zu kommunizieren. Vielleicht können wir einfach unsere natürlichen Sprachen verwenden.

Meine Haltung war lange ziemlich strikt: LLMs halluzinieren und sind daher keine verlässlichen Bausteine. Wenn man sich auf den Übersetzungsschritt nicht verlassen kann, kann man ihn nicht als ernsthafte Abstraktionsschicht behandeln, weil er keine stabilen Garantien über das zugrunde liegende System liefert.

Mit besseren Modellen werden Halluzinationen weniger zentral, auch wenn Modelle weiterhin Fehler machen. In letzter Zeit beschäftigt mich eine andere Frage: Stellen wir uns ein LLM vor, das nie halluziniert, also zuverlässig eine plausible Implementierung dessen liefert, was man verlangt. Wäre das dann die nächste Generation eines Compilers? Und was würde das für Programmierung und Software Engineering insgesamt bedeuten?

Dieser Beitrag ist ein Versuch, diese Frage zu beantworten. Das Kernargument ist einfach:
Systeme zu spezifizieren ist schwierig und wir sind bequem.

Bevor ich darauf eingehe, möchte ich etwas anderes klären: Was bedeutet es eigentlich, dass eine Sprache eine höhere Abstraktionsebene hat?

Programmieren ist im Kern der Akt, einen Computer dazu zu bringen, etwas zu tun. Computer sind aus menschlicher Sicht sehr simpel. Man muss ihnen exakt sagen, was sie tun sollen. Es gibt keine implizite Ableitung. Ein Computer kennt nicht einmal Werte, Typen oder Konzepte. Alles sind Bitfolgen, die verarbeitet werden, um andere Bitfolgen zu erzeugen. Bedeutung geben wir diesem Prozess erst.

Sehr früh begann man damit, arithmetische und logische Instruktionen in Computer einzubauen. Zwei Bitfolgen stehen jeweils für Zahlen, man kann sie addieren, subtrahieren oder multiplizieren. Um einen Computer etwas tun zu lassen, konnte man Daten als Zahlen darstellen, logische Operationen auf ALU Instruktionen abbilden und das Ergebnis am Ende im eigenen Fachkontext interpretieren. Danach lassen sich Operationen auf dieser Domäne definieren, die wieder auf diese kleineren ALU Instruktionen kompiliert werden. Und damit hat man einen Compiler.

Dieser Compiler ist jedoch in gewisser Weise redundant. Er tut nichts, was man nicht auch direkt tun könnte, da eine direkte Abbildung zwischen zwei Sprachen besteht. Die höhere Sprache wird in eine Menge von ALU Instruktionen übersetzt. Jeder könnte diese Abbildung relativ einfach selbst implementieren oder direkt die ALU Instruktionen schreiben.

Echte höhere Programmiersprachen schaffen etwas anderes. Sie geben eine völlig neue Sprache, die über nicht triviale Mechanismen auf die zugrunde liegenden Instruktionen abgebildet wird, um die mentale Komplexität für Programmierer zu reduzieren. Instruktionssätze kennen keine Variablen, keine Schleifen, keine Datenstrukturen. Man kann zwar eine Instruktionsfolge schreiben, die einem binären Suchbaum entspricht, aber die mentale Belastung ist um ein Vielfaches höher als in klassischen Programmiersprachen. Structs, Enums, Klassen, Schleifen, Bedingungen, Exceptions, Variablen und Funktionen existieren auf höheren Ebenen und werden beim Kompilieren nach unten aufgelöst.

Ein entscheidender Aspekt von Kompilierung ist, dass Programmierer einen Teil der Kontrolle abgeben. Genau das reduziert die mentale Belastung. Wenn eine Programmiersprache keine Kontrolle abnimmt, ist sie kaum eine sinnvolle Abstraktionsschicht, da sie keine Verantwortung reduziert.

Ein frühes Beispiel dafür ist das Layout von Code. In handgeschriebenem Assembly kontrolliert man genau, wo sich Code im Programmspeicher befindet. In einer Sprache mit strukturiertem Kontrollfluss und aufrufbaren Prozeduren hat man keine exakte Kontrolle mehr darüber, wann Instruktionen geladen werden oder wie Speicherblöcke angeordnet sind. Weitere Beispiele sind Laufzeitumgebungen, die Aufgaben wie manuelle Speicherverwaltung übernehmen. Diese wiederum abstrahieren, wie Daten im Speicher organisiert sind.

Dieser Kontrollverlust führt zu einer wichtigen Frage: Woher wissen wir, dass die Abstraktion korrekt implementiert ist? Und was bedeutet überhaupt korrekt in diesem Kontext?

Die Antwort hat mehrere Ebenen. Reife Abstraktionen sind durch Semantik definiert: welche Verhaltensweisen erlaubt sind, welche verboten sind und welche Garantien man erwarten kann. In C liefert malloc einen Zeiger auf einen Speicherblock mit mindestens der angeforderten Größe oder NULL, passend ausgerichtet und später freigebbar. Es definiert einen Vertrag, gegen den man programmieren kann.

Implementierungen werden durch Tests und manchmal durch Beweise validiert, weil diese Garantien prinzipiell überprüfbar sind. In der Praxis sind Garantien kontextabhängig. Die meisten Programme brauchen funktionierende Speicherzuweisung, nur wenige interessieren sich stark für Performance, Fragmentierung oder Konkurrenz. In solchen Fällen tauscht man den Allocator aus oder arbeitet auf einer niedrigeren Ebene.

Ein zentraler Punkt wird hier sichtbar: Abstraktionsgarantien sind nicht universell, sondern kontextabhängig. Meist dominiert funktionale Korrektheit: Tut es, was es verspricht? Programmiersprachen haben enorme Fortschritte gemacht, indem sie Abstraktionen mit präzise definierbarem Verhalten und intensiven Tests ermöglicht haben. Man kann davon ausgehen, dass push und pop in einer Python Liste semantisch ähnlich funktionieren wie ein Vector in C++, auch wenn Implementierungen stark variieren.

LLM basierte Programmierung stellt diese Dominanz infrage, weil die Sprache, also natürliche Sprache, keine präzise Semantik besitzt. Dadurch wird es schwieriger zu definieren, was funktionale Korrektheit überhaupt bedeutet, ohne zusätzliche Validierungsmechanismen wie Tests, Typen, Verträge oder formale Spezifikationen.

Damit komme ich zum Kernpunkt. Was sich mit LLMs verändert, ist nicht primär Nichtdeterminismus oder Halluzination. Es ist, dass die Programmierschnittstelle standardmäßig funktional unzureichend spezifiziert ist. Natürliche Sprache lässt Lücken. Viele verschiedene Programme können denselben Prompt erfüllen. Das LLM muss diese Lücken füllen.

So wie eine Laufzeit mit Garbage Collection entscheidet, wann Speicher freigegeben wird, bedeutet Programmieren in natürlicher Sprache, Kontrolle darüber abzugeben, welches konkrete Programm tatsächlich erzeugt wird. Diese Unterbestimmtheit zwingt das Modell, Annahmen über Datenmodelle, Randfälle, Fehlerverhalten, Sicherheit und Performance zu treffen.

Dadurch entsteht eine neue Gefahr in der Art, wie Programme geschrieben werden.

Menschen haben schon immer vage Anforderungen formuliert. Neu ist, wie direkt ein LLM diese Vagheit in ausführbaren Code umwandelt und uns dazu einlädt, funktionale Präzision selbst auszulagern. Wir können bedeutende Verhaltensentscheidungen einem Generator überlassen und erst auf das Ergebnis reagieren.

Wenn man sagt gib mir eine Notiz App, beschreibt man kein einzelnes Programm, sondern einen riesigen Raum möglicher Programme. Das LLM kann eine von unzähligen sinnvollen Implementierungen liefern. Der kritische Punkt ist, dass sinnvolle Entscheidungen trotzdem falsch für die eigentliche Absicht sein können und man erst später erkennt, welche Annahmen getroffen wurden.

Das verschiebt Entwicklung in einen iterativen Verfeinerungsprozess. Man schreibt eine ungenaue Spezifikation, erhält eine mögliche Implementierung, prüft sie, verfeinert die Spezifikation und wiederholt den Prozess. In diesem Modus wird man eher zum Konsumenten generierter Artefakte als zum Produzenten, der gezielt konstruiert.

Dabei geht etwas Subtiles verloren. Beim manuellen Aufbau erkundet man den Raum der Möglichkeiten durch eigene Designentscheidungen. Mit einem Generator werden diese Entscheidungen im Hintergrund getroffen und man sieht nur das Ergebnis.

Ich glaube, dieser Punkt ist noch nicht weit verbreitet: Halluzinationen sind nicht das einzige Problem. Selbst ohne Halluzinationen kann die einfache Möglichkeit, Spezifikation zu umgehen, eine gefährliche Bequemlichkeit fördern. Man sieht das in kleinen Gewohnheiten: alle Änderungen akzeptieren, noch ein Prompt, dann passt es schon, und ein langsames Abdriften in Software, die man nicht wirklich versteht.

Deshalb wird der Wille zur Spezifikation immer wichtiger. LLMs sind besonders stark, wenn klare Einschränkungen vorliegen: Optimierung, Refactoring, Übersetzung, Migration. Aufgaben, die früher extrem zeitaufwendig waren, werden machbar, wenn Zielverhalten klar definiert und durch Tests abgesichert ist.

Es gilt schon lange, dass Spezifikation oft schwieriger ist als Implementierung. Wir könnten in eine Welt eintreten, in der gilt: Wenn du spezifizieren kannst, kannst du bauen. Dann werden Spezifikation und Verifikation zum Engpass und damit zur zentralen Fähigkeit.

Dieser Beitrag ist nicht perfekt ausgearbeitet, aber ich wollte die Idee teilen. Es ist möglich, LLMs in einem lockeren Sinne als compilerähnlich zu betrachten, da sie Spezifikationen in ausführbare Artefakte übersetzen. Doch die Kontrolle, die wir an diese Übersetzungsschicht abgeben, ist größer als je zuvor.

Klassische Compiler reduzieren die Notwendigkeit, niedrige Ebenen zu betrachten, indem sie definierte Semantik und testbare Garantien liefern. LLMs reduzieren ebenfalls in vielen Kontexten die Notwendigkeit, Quellcode zu lesen. Doch der Kontrollverlust ist nicht durch eine formale Sprachdefinition begrenzt. Man kann so weit die Kontrolle verlieren, dass man zum Konsumenten von Software wird, die man eigentlich selbst erzeugen wollte, und es ist erschreckend leicht, dieses Abdriften nicht zu bemerken.

Deshalb sollten wir die Compiler Analogie nicht unkritisch übernehmen. Wenn LLMs zu einem zentralen Bestandteil der Toolchains werden, brauchen wir Wege, den Willen zur Spezifikation zu stärken und Spezifikation sowie Verifikation so selbstverständlich zu machen, wie es das Schreiben von Code lange war.