Werkstattbericht LLM-Enabled Development#
Wie entwickelt man mit LLMs?#
Ziel dieser Übung war es, methodische Erfahrungen über LLM-gestütztes Coding zu gewinnen:
- Wie funkionieren Spezifikationsprozess für komplexere Software?
- Welche Architekturentscheidungen sind erforderlich, um Komplexität handhabbar zu halten?
- Wo liegen die praktischen Grenzen in Bezug auf Code-Umfang und Komplexität?
- Wie geht man systematisch mit Fehlern der LLMs um?
Aufgrund einer Fragestellung aus einem anderen Projekt wurde ein Code-Analyse-Tool für Java-Code gewählt. Ein solches Tool erforderte verschiedene technische Komponenten - Datei-Scanning, LLM-Integration, asynchrone Verarbeitung, strukturierte Datenspeicherung und Web-Interfaces.
Als funktionale Anforderungen für das Lernprojekt wurden definiert:
- Analyse der architektonischen Struktur und Layer-Zuordnung
- Identifikation von Business-Funktionalitäten
- Erkennung von Sicherheits- und Qualitätsprobleme sowie technischen Schulden
- Katalogisierung von API-Schnittstellen
- Analyse von Abhängigkeiten zwischen Komponenten
- Textliche Beschreibung dieser Aspekte und Lesbarkeit der Ausgabe
Diese Anforderungen wurden bewusst gewählt, um verschiedene Aspekte der LLM-Integration zu testen: strukturierte Datenextraktion, natürlichsprachliche Beschreibungen, Mustererkennung und Kategorisierung.
Technische Konzeption und Architektur#
Grundsätzliche Designentscheidungen#
Die Entwicklung des Analyse-Tools basierte auf mehreren bewussten Architekturentscheidungen, die sich aus den Anforderungen und den Möglichkeiten von Large Language Models ergaben.
Hierarchischer Analyseansatz#
Ein zentrales Designprinzip war die Erkenntnis, dass bei großen Projekten eine vollständige Analyse nicht in einem einzelnen LLM-Kontext verarbeitet werden kann. Daher wurde ein hierarchischer Ansatz gewählt, bei dem Analysen auf verschiedenen Ebenen durchgeführt und aggregiert wurden:
- File-Level: Detaillierte Analyse einzelner Java-Dateien
- Package-Level: Aggregation und Zusammenfassung auf Package-Ebene
- Module-Level: Modul-basierte Betrachtung
- System-Level: Gesamtsystem-Architektur und übergreifende Patterns
Diese Struktur ermöglichte es, dass jede Analyseebene selbst wieder als Input für weitere LLM-basierte Analysen dienen konnte, ohne dass Kontextgrenzen überschritten wurden.
Speicherformat und Datenstruktur#
Als Speicherformat wurde YAML gewählt, da es gegenüber XML einfacher zu handhaben ist und sich gut für die weitere maschinelle Verarbeitung eignet. Die Analyseergebnisse wurden in einer Package-basierten Verzeichnisstruktur abgelegt, die die logische Organisation des Quellcodes widerspiegelt:
analysis/
├── project_structure.yaml
├── summary.yaml
└── files/
└── com/example/package/
└── ClassName.yamlTechnologie-Stack#
Die Wahl von Python und Gradio als Basis-Technologien erfolgte aus pragmatischen Gründen:
- Python bietet eine breite Unterstützung für LLM-Integration und asynchrone Verarbeitung
- Gradio ermöglichte die schnelle Entwicklung von Web-Interfaces, war leicht deploybar und wurde von LLMs gut “verstanden”, was die KI-gestützte Entwicklung erleichterte
- Asyncio erlaubte effiziente parallele Verarbeitung bei kontrollierbarer Ressourcennutzung
Für die LLM-Integration kam Mistral Small 2506 als lokal gehostetes Modell zum Einsatz. Ein Designkriterium war die Verfügbarkeit einer lokalen LLM ohne Einschränkungen beim Token-Umfang für Anfragen.
Architekturkomponenten#
Das Tool bestand aus 6 Python-Dateien mit insgesamt 5000-6000 Zeilen Code und implementierte eine modulare Architektur:
- File Scanner: Identifizierte alle relevanten Java-Dateien basierend auf Include- und Exclude-Patterns
- Structure Analyzer: Analysierte die Package-Hierarchie, identifizierte Architekturstile (MVC, Layered, Hexagonal) und ordnete Packages zu Layern zu
- Code Analyzer: Führte LLM-basierte Analysen auf Datei-Ebene durch
- Storage Manager: Verwaltete die persistente Speicherung der Analyseergebnisse in YAML-Dateien
- Aggregator: Fasste Einzelergebnisse zu höheren Abstraktionsebenen zusammen
- Report Builder: Generierte verschiedene Sichten auf die Analysedaten
Zweistufiges Interface-Design#
Eine bewusste Architekturentscheidung war die Trennung in zwei separate Gradio-Interfaces:
- Analyzer Interface (Port 7860): Führte die eigentliche Analyse durch, die mehrere Stunden dauern konnte
- Analysis Dashboard (Port 7861): Ermöglichte die interaktive Exploration der bereits erzeugten Analysedaten
Diese Trennung ermöglichte die einfache Trennung zwischen Code-Analyse und Daten-Exploration.
LLM-basierte Code-Analyse: Implementierungsdetails#
Multi-Prompt-Strategie#
Ein wesentliches Implementierungsdetail, das sich iterativ entwickelte, war die Aufteilung der Analyse in spezialisierte LLM-Prompts pro Datei:
- Business Logic Analysis: Identifizierte den Geschäftszweck, Capabilities und kritische Methoden
- Technical Aspects Analysis: Erkannte Frameworks, Design Patterns und Dependencies
- Interface Analysis: Extrahierte REST-Endpoints und SOAP-Services
- Issue Detection: Identifizierte Security-Probleme, Code Smells und Technical Debt
Diese Spezialisierung erwies sich als hilfreich, da ein einzelner Prompt nicht ausreichend fokussiert war, um alle relevanten Aspekte gleichzeitig zu erfassen.
Concurrency und Stabilität#
Die Analyse großer Projekte erforderte den Umgang mit mehreren technischen Herausforderungen:
- Rate Limiting: Ein Semaphore-basierter Mechanismus steuerte die parallele Ausführung (konfigurierbar 1-10 Worker), um das LLM nicht zu überlasten
- Error Handling: Jede Datei-Analyse war isoliert, Fehler führten nicht zum Abbruch des Gesamtprozesses
- JSON-Extraktion: LLM-Responses wurden mittels Regex-Parsing auf JSON-Strukturen durchsucht, um robuste Ergebnisse auch bei inkonsistenten Response-Formaten der LLM zu gewährleisten
Beispiel einer LLM-Analyse#
Die Business Logic Analysis nutzte einen strukturierten Prompt, der das LLM zu JSON-formatierten Antworten anleitete:
Analyze this Java class and identify its business purpose and capabilities.
Class: UserService
Code: [first 3000 characters]
Focus on:
1. What is the business purpose of this class?
2. What business capabilities does it provide?
3. Which public methods are business-critical?
Respond in JSON format:
{
"purpose": "Brief description",
"capabilities": [
{
"name": "capability name",
"description": "what it does",
"methods": ["method1", "method2"]
}
],
"critical_methods": ["method1", "method2"]
}Entwicklungsprozess mit LLM-Unterstützung#
Spezifikationsphase (1 Stunde)#
Der Entwicklungsprozess begann mit einer ausführlichen Spezifikationsphase, in der mit verschiedenen LLMs die Requirements und Constraints erarbeitet wurden. Diese Phase war bewusst als Dialog gestaltet, bei dem die LLM nicht nur Anforderungen umsetzen, sondern auch Fragen stellen und Klarstellungen einfordern sollte.
Durch konsequente Anwendung des KISS-Prinzips (Keep It Small and Simple) wurde sichergestellt, dass die Spezifikation realistisch umsetzbar blieb.
Implementierungsphase (2 Stunden)#
Die eigentliche Code-Generierung erfolgte in 2 Stunden über 4-5 Haupt-Iterationen. Dabei wurde ein linearer Entwicklungsprozess ohne späteres Refactoring verfolgt. Die Strategie war:
- Funktionale Anforderungen klären: Was sollte das Tool leisten?
- UI-Struktur definieren: Welche Interfaces wurden benötigt?
- Technologien festlegen: Welche Bibliotheken und Frameworks?
- Architektur gestalten: Wie spielten die Komponenten zusammen?
Technische Rahmenbedingungen wurden teilweise vorgegeben, teilweise in Diskussion mit der LLM erarbeitet. Die Gradio-Interfaces wurden in ihrem Aufbau spezifiziert, während die detaillierte Implementierung dem LLM überlassen wurde.
Rolle der Entwickelnden#
Die Rolle als Nicht-Entwickler lag primär in der:
- Anforderungsklärung: Was sollte das Tool konkret lösen?
- Architekturentscheidungen: Welche grundsätzliche Struktur war angemessen?
- Qualitätskontrolle: Entsprach der Code der Spezifikation?
- Komplexitätskontrolle: War die Lösung so einfach wie möglich?
Das LLM übernahm die eigentliche Code-Implementierung, Syntax-Details und die konkrete Umsetzung von Algorithmen.
Methodische Erkenntnisse#
Spezifikation als Erfolgsfaktor#
Eine zentrale Erkenntnis aus diesem Projekt war, dass LLM-gestütztes Coding über gute Spezifikationen funktioniert. Je klarer, widerspruchsfreier und vollständiger die Spezifikation, desto besser der generierte Code. Diese Klarheit wurde in einem Dialog mit dem LLM erarbeitet, da das LLM durch gezielte Fragen Lücken und Widersprüche aufdecken konnte.
Möglichkeiten und Grenzen der Code-Generierung#
Während LLMs beeindruckende Fähigkeiten in der Code-Generierung zeigten, gab es realistische Grenzen:
Scope-Limitation: LLMs konnten Code zuverlässig generieren bis etwa 1000-1500 Zeilen pro Datei. Darüber hinaus nahm die Qualität ab und die Wahrscheinlichkeit von Inkonsistenzen stieg.
Overengineering-Tendenz: LLMs schlugen häufig zu komplexe Lösungen vor, die zwar technisch korrekt, aber schwer wartbar waren. Dies erforderte aktive Steuerung. Zum Einsatz von LLMs für Tools lagen nicht genug Informationen vor. LLMs setzten daher oft komplizierte Heuristiken ein, was vermieden wurde.
Architektur-Verständnis erforderlich: Auch ohne selbst zu codieren, war ein fundiertes Verständnis von Software-Architektur erforderlich, um realistische und kontrollierbare Lösungen zu spezifizieren.
Hintergrundwissen für Deployments und IT-Sicherheitsaspekte musste vorliegen oder wurde aufgebaut.
Veränderter Entwicklungs-Workflow#
Der Entwicklungsprozess mit LLMs veränderte die Prioritäten:
- Anforderungsklärung wurde zum Kern-Skill
- UI/UX-Design erfolgte vor der Implementierung
- Architektur-Entscheidungen waren zentral
- Implementierungs-Details wurden delegiert
Dies ermöglichte es Fachexperten ohne tiefe Programmierkenntnisse, spezialisierte Tools für ihre Domäne zu entwickeln.
Validierung des Lernprojekts#
Funktionstest am realen Beispiel#
Um die Funktionsfähigkeit des entwickelten Tools zu validieren, wurde es auf ein reales Java-Projekt angewendet - ein System mit ca. 1,2 Millionen Zeilen Code, einschließlich Java-Dateien, XML-Konfigurationen und XSD-Definitionen. Der Analyse-Durchlauf erfolgte über Nacht.
Das Ziel war nicht, ein produktives Werkzeug zu schaffen, sondern zu verstehen:
- Funktionierte der entwickelte Ansatz technisch?
- Waren die LLM-Analysen inhaltlich sinnvoll?
- Skalierte die Architektur auf große Codebasen?
- Wo zeigten sich Probleme in der Umsetzung?
Beobachtete Ergebnisse#
Das Tool erzeugte eine umfassende strukturierte Dokumentation:
- Projekt-Struktur-Übersicht mit erkanntem Architekturstil
- Package-Hierarchie mit automatischer Layer-Zuordnung
- Detaillierte Datei-Analysen mit Business- und Technical-Aspects
- Katalog aller REST- und SOAP-Schnittstellen
- Kategorisierte Liste identifizierter Issues nach Severity
- Aggregierte Statistiken auf verschiedenen Abstraktionsebenen
Technische Validierung: Der Code funktionierte ohne Probleme, die Analyse lief komplett durch, die YAML-Ausgaben waren valide und die Datenstrukturen konsistent.
Inhaltliche Qualität: Die LLM-generierten Beschreibungen waren sprachlich lesbar, erkannte Architekturmuster erschienen plausibel, und die Issue-Detection identifizierte sichtbare Problemfelder. Die Diskussion mit dem Entwicklungsteam bestätigte Kernergebnisse.
Fokus: Methodische Erkenntnisse#
Der Wert des Experiments lag nicht in dem erzeugten Tool selbst, sondern in den gewonnenen Erkenntnissen über LLM-gestütztes Coding:
- Spezifikations-Methodik: Wie strukturierte man den Dialog mit LLMs?
- Architektur-Patterns: Welche Strukturen funktionierten bei LLM-generiertem Code?
- Praktische Grenzen: Wo lagen die realistischen Limits?
- Workflow-Änderungen: Wie veränderte sich die Rolle der Entwickelnden?
Diese methodischen Erkenntnisse sind womöglich auf andere Projekte übertragbar, unabhängig von der konkreten Anwendung.
Fazit: Methodische Erkenntnisse aus dem Lernprojekt#
Das Experiment mit LLM-gestützter Software-Entwicklung war als Lernprojekt konzipiert und erfüllte diesen Zweck. In 3 Stunden Entwicklungszeit (1 Stunde Spezifikation, 2 Stunden Implementierung) entstand ein funktionsfähiges Tool mit 5000-6000 Zeilen Code, das technisch arbeitete und ein Projekt mit 1,2 Millionen Zeilen Code analysieren konnte.
Der Wert lag nicht im Tool selbst, sondern in den methodischen Erkenntnissen über LLM-gestütztes Coding bei realistischer Komplexität.
Kernerkenntnisse zur Spezifikations-Methodik#
Spezifikation als Erfolgsfaktor: LLM-gestütztes Coding funktionierte über klare, widerspruchsfreie Spezifikationen. Je präziser die Anforderungen, desto besser der generierte Code. Diese Klarheit wurde in einem strukturierten Dialog mit dem LLM erarbeitet, da das LLM durch gezielte Fragen Lücken und Inkonsistenzen aufdecken konnte.
Dialog-Struktur: Die investierte Zeit in die Spezifikationsphase (1 Stunde) zahlte sich aus. Der Dialog folgte einem Muster:
- Funktionale Anforderungen klären
- UI-Struktur definieren
- Technologie-Entscheidungen treffen
- Architektur-Komponenten spezifizieren
KISS-Prinzip durchsetzen: LLMs tendierten zu Overengineering. Aktive Steuerung zur Einfachheit war erforderlich und erfolgte kontinuierlich. Komplexe Lösungen, die das LLM vorschlug, wurden hinterfragt und vereinfacht.
Erkenntnisse zu Architektur-Patterns#
Hierarchische Strukturierung: Der hierarchische Analyseansatz (File → Package → Module → System) erwies sich als tragfähiges Pattern für LLM-generierten Code. Jede Ebene produzierte Ergebnisse, die selbst wieder als Input dienen konnten.
Modularisierung: Die Aufteilung in 6 separate Python-Dateien mit klaren Verantwortlichkeiten (Scanner, Analyzer, Storage, etc.) funktionierte gut. Dataclasses als Datenmodelle erleichterten die Strukturierung.
Interface-Trennung: Die bewusste Trennung in zwei Gradio-Interfaces (Analyse vs. Exploration) spiegelte unterschiedliche Nutzungsmuster wider und reduzierte die Komplexität der einzelnen Komponenten.
Praktische Grenzen von LLM-Coding#
Das Experiment zeigte realistische Limits auf:
Scope-Limitation: LLMs konnten Code bis etwa 1000-1500 Zeilen pro Komponente generieren. Darüber hinaus nahm die Kohärenz ab. Die 6 Dateien mit insgesamt 5000-6000 Zeilen bewegten sich in diesem Rahmen durch geschickte Modularisierung.
Architektur-Kompetenz erforderlich: LLMs implementierten Details, aber die grundsätzliche Architektur musste verstanden und spezifiziert werden. Ohne Verständnis für Software-Architektur waren komplexere Projekte nicht steuerbar.
Iterative Korrekturen: 4-5 Haupt-Iterationen waren erforderlich, um Inkonsistenzen zu beseitigen und die Spezifikation zu präzisieren.
Technische Herausforderungen: Stabilität über lange Analyseläufe und saubere JSON-Generierung waren initial problematisch. Robustes Error-Handling und Regex-basiertes JSON-Parsing waren hilfreiche Anpassungen.
Veränderter Entwicklungs-Workflow#
LLM-gestütztes Coding veränderte die Prioritäten:
Anforderungsklärung wurde zum Kern-Skill: Die Fähigkeit, präzise zu spezifizieren, zeigte sich als wichtiger als Syntax-Kenntnisse.
Architektur vor Implementierung: Strukturentscheidungen wurden vor der Code-Generierung getroffen. Das LLM konnte bei der Architektur beraten, aber die Entscheidung lag bei den Entwickelnden.
Neue Rolle: Die Entwickelnden wurden zu Spezifikations-Autoren, Architekten und Quality-Gate. Die Code-Implementierung wurde delegiert.
Empowerment von Nicht-Entwicklern: Mit Architektur-Verständnis konnten Fachexperten ohne tiefe Coding-Kenntnisse funktionale Prototypen erstellen.