Über unsMediaKontaktImpressum
Semjon Mössinger & Bastian Weinlich 28. Januar 2025

KI-Coding-Assistenten in der Praxis: Ein Fallbeispiel (Teil 2)

Im ersten Teil des Artikels haben wir den Github Copilot mit dem AI Assistant verglichen. Im zweiten Teil geben wir anhand eines Fallbeispiels Tipps zur praktischen Arbeit mit den beiden KI-Coding-Assistenten.

Als Fallbeispiel wollen wir eine Applikation betrachten, mit der man eine Liste von Helden verwalten kann. Als neues Feature soll unsere Applikation beim Klick auf einen Button automatisch einen neuen Helden in die Datenbank hinzufügen. Dabei soll der Name durch Rekombination vorhandener Vor- und Nachnamen zufällig entstehen. Aus den vorhandenen Namen "Mickey Mouse" und "Daisy Duck" könnte also bspw. der neue Name "Mickey Duck" entstehen. In unserem Beispiel betrachten wir dafür nur das Java-Backend.

Künstliche Intelligenz in der Software-Entwicklung: Tests und Auto Completion

Wo fängt man mit AI Assisted Coding an? Im Prinzip genauso wie ohne Assistenz: Wir entscheiden uns dazu, testgetrieben zu entwickeln und starten mit den Unit-Tests. Die Tests implementieren wir "per Hand", nutzen dafür aber die Auto Completion von einem KI-Coding-Assistenten. Welcher der beiden ist in dem Fall zweitrangig, da sich diese Funktion bei Copilot und AI Assistant kaum in der Bedienung unterscheidet. Bei Features, bei denen es zwischen den zwei Produkten wesentliche Unterschiede gibt, erwähnen wir explizit Copilot oder AI Assistant.

Das folgende Beispiel eines einfachen Tests zeigt, welcher Code noch per Hand geschrieben werden muss und welcher Code sich aus dem Kontext ergibt:

@Test
void generateHeroName() {
    // Arrange
    final var heroNames = List.of("Mickey Mouse", "Donald Duck", "Goofy Goof");
    final var indexFirstName = 0;
    final var indexLastName = 1;

    // Act
    final var result = heroService.generateHeroName(heroNames, indexFirstName, indexLastName);
  
    // Assert
    assertThat(result).isEqualTo("Mickey Mouse");   
 }

Fettgedruckter Code muss vom Entwickler noch getippt werden, der restliche Code wird automatisch vom KI-Coding-Assistent vorgeschlagen.

Wenn man einen weiteren Test schreibt, beispielsweise generateHeroName_withEmptyList_returnsMaxMustermann, wird man feststellen, dass tatsächlich der komplette Test durch den Assistenten korrekt vorgeschlagen wird. Der KI-Assistent nimmt den bereits geschriebenen Test als Kontext und kann daraus weitere Tests ziemlich gut ableiten.

Welche Tendenz lässt sich noch beobachten?

Man muss weniger scrollen und nachschlagen, hat damit weniger menschlichen Kontextwechsel und kann sich damit besser auf die eigentliche Arbeit fokussieren. Im konkreten Fall muss man nicht nachschauen, dass die Klassenvariable heroService heißt und man muss auch nicht googeln oder ausprobieren, wie noch mal die genaue Syntax für assertThat oder das Initialisieren einer Liste (List.of) funktioniert.

Mit KI coden: Varianten des Inline-Promptings

Bevor wir die eigentliche Funktionalität implementieren, wollen wir eine Methode schreiben, die überprüft, dass jeder Held tatsächlich genau einen Vor- und einen Nachnamen hat. Dazu erweitern wir die nachfolgende, bereits bestehende Funktion:

1    public boolean checkEveryNameStartsWithACapitalLetter() {
2        final var name = heroRepository.findAll().stream()
3                .map(Hero::getName).toList();
4        return name.stream().allMatch(it -> Character.isUpperCase(it.charAt(0)));
5    }

Die folgenden drei Varianten geben ein Gefühl dafür, wie man "on the fly" promptet, ohne umständlich mit einem separaten Chat-Tool zu arbeiten:

  • Variante 1: Wir markieren die gesamte Funktion, öffnen den Inline-Chat und geben die folgenden Prompts ein: change this function so that it also checks whether every name consists of two words. Wenn wir uns das Ergebnis anschauen, stellen wir vermutlich fest, dass leider kein regulärer Ausdruck für den Check verwendet wurde. Wir können aber interaktiv per Chat nachtragen, indem wir den nächsten Befehl absetzen, um uns dem Ziel iterativ zu nähern: use exactly one regex to do the check. Formulieren wir den Prompt auf Deutsch, so bekommen wir auch eine Antwort auf Deutsch. Wir können keine Qualitätsunterschiede zwischen deutschen und englischen Prompts erkennen – auf der sicheren Seite ist man aber mit Englisch, da die LLMs hauptsächlich in dieser Sprache trainiert wurden.
  • Variante 2: Wir löschen Zeile 4, ändern den Funktionsnamen auf checkEveryNameStartsWithACapitalLetterAndConsistsOfTwoWords und fügen den Hilfs-Kommentar // use regex in Zeile 4 hinzu.
  • Variante 3: Wir löschen Zeile 4 und schreiben anstatt Code unseren Prompt direkt in das Sourcefile der Java-Klasse. Ein recht neues Feature der KI-Coding-Assistenten ist, dass sie natürlichsprachlichen Text automatisch erkennen und ähnlich wie einen Inline-Chat behandeln. Dieses Feature nennt sich beim AI Assistant "Inline AI Prompt", Copilot nennt es "Inline Chat Completion Trigger" (im Beta-Status).

Gerade bei regulären Ausdrücken (und auch bei SQL) kann ein KI-Coding-Assistent sehr leistungsfähig sein. In allen Varianten sollte Zeile 4 dann in etwa so generiert werden:

1   return name.stream().allMatch(n -> n.matches("^[A-Z][a-z]* [A-Z][a-z]*$"));

Jede Variante hat ihre Daseinsberechtigung. Variante 1 ist besonders gut für iteratives Arbeiten geeignet; leider muss man ggf. im Prompt noch explizit sagen, dass die vorhandene Methode geändert werden soll (sonst wird ggf. eine zusätzliche erzeugt). Variante 2 arbeitet am engsten mit dem vorhandenen Code zusammen. Wenn man Code-Kommentare aber nur als temporäres Hilfsmittel verwendet, läuft man Gefahr, diese hinterher nicht mehr zu löschen. Dieses Problem löst dann Variante 3, die sogar eine komfortable "Rückgängig"-Funktion hat.

Wir können Copilot übrigens auch dadurch unterstützen, indem wir relevante Code-Files in anderen Tabs geöffnet lassen. Denn dann werden diese Files eher als zusätzlicher Kontext berücksichtigt. Damit uns die Auto Completion die Funktion generateHeroName(…) im HeroService vorschlägt, müssen wir also zusätzlich den Tab HeroServiceTest.java geöffnet haben. Doch wir wollen diesmal einen anderen Weg gehen, um generateHeroName(…) zu erzeugen: die Chatfunktion.

KI-Assistenten: Code verbessern im Chat

Um von den Tests nun zu produktivem Code zu kommen, können wir den separaten Chat des KI-Assistenten verwenden. Wir können die Tests im Editor selektieren und dem Chat den folgenden Prompt mitgeben: #selection create a function that passes the given test. Dadurch wird die Methode generateHeroName(…) erzeugt. Je nach KI-Coding-Assistent wird der generierte Code nur im Chat (AI Assistant) angezeigt oder direkt an die passende Stelle im Produktivcode hinzugefügt (Copilot).

Im Prompt haben wir #selection verwendet, um explizit auf den selektierten Code hinzuweisen. Es gibt hier noch weitere Schlüsselwörter, beispielsweise #file oder #symbol, allerdings unterstützt Copilot diese aktuell noch nicht in den JetBrains IDEs.

Mit dem Chat oder alternativ der Inline-Chatfunktion könnte man dann einen der sogenannten "Commands" ausführen und beispielsweise mit /doc die Methode dokumentieren lassen oder mit /test Tests generieren lassen.

Nun wollen wir unsere neu erstellte Funktion generateHeroName(…) über einen Controller-Endpunkt für ein Frontend verfügbar machen. Hier könnte uns der Chat durch ein Beispiel helfen: Generate a simple example of a rest endpoint with java spring-boot. Die Ergebnisse sind, zumindest für verbreitete Programmiersprachen und Frameworks, meist sehr gut und man spart sich damit auch hier das Blättern in einer Dokumentation.

Die Chatfunktion ist also sehr flexibel und erlaubt es, sich eigene kleine Workflows aufzubauen. So könnte man sich etwa mit dem AI Assistant den Prompt #localChanges give me the list of variable names which should be improved with file name and line in die Prompt-Library legen, um explizit Variablennamen zu reviewen.

KI-Assistenten: Arbeiten über mehrere Files hinweg

Der Copilot bietet unter VS Code die Möglichkeit, ein Feature über mehrere Dateien hinweg mit einem einzigen Prompt zu implementieren. Dafür gibt es den sogenannten "Copilot-Edits"-Chat, der speziell dafür vorgesehen ist, den vorhandenen Code zu editieren oder neuen Code in die passenden Files hinzuzufügen. Wenn wir nun beispielsweise die Funktionalität brauchen, alle Heroes zu löschen, könnten wir Controller und Service (und ggf. das Repository) in die Copilot-Edits ziehen und den Prompt add a functionality which deletes all heroes ausführen. Dadurch würde die Funktionalität einmal quer durch die verschiedenen Schichten hinzugefügt – sogar bis ins Frontend, wenn man die entsprechenden Dateien angibt.

Beim Arbeiten über mehrere Dateien hinweg kann man natürlich auch Fragen zur existierenden Codebase stellen. Wenn wir ein Feature abgeschlossen haben und testen wollen, könnte uns zum Beispiel interessieren, auf welchem Port unser Projekt läuft. Mit @Workspace kann man im Chat Fragen zur gesamten Codebase stellen, ohne den Kontext – also bestimmte Codestellen – explizit angeben zu müssen. So kann der Prompt @workspace on which port does this project run? mit konkreten Werten beantwortet werden.

Künstliche Intelligenz: Hilfe bei der Fehleranalyse

Auch bei der Beseitigung von Kompilier- und Laufzeitfehlern unterstützen uns die KI-Coding-Assistenten. Wenn man bei der Entwicklung festhängt, weil man eine komplexe Syntax nicht vollständig verstanden hat, aber genau weiß, was man umsetzen möchte, kann ein KI-Coding-Assistent oft hilfreich sein. Dies zeigt das folgende Beispiel:

1        BinaryOperator<Integer> add = (x, y) -> x + y;
2        var composed = compose(add, add);
3        System.out.println(composed(2, 3));

Korrekt wäre es, in der letzten Zeile composed.apply(2,3) zu schreiben. Der aktuelle Code hingegen ergibt einen Kompilierfehler. Nutzt man den Inline-Chat mit dem Prompt fix bzw. dem Command /fix, wird der Code auf Anhieb passend korrigiert. Hier erkennt der KI-Assistent also, dass composed in Zeile 2 ebenfalls vom Typ BinaryOperator ist, dass wir in Zeile 3 compose ausführen wollen und weiß, dass BinaryOperator durch apply ausgeführt wird (ähnlich wie eine Function<> mit apply ausgeführt wird). Ein verwandter Use Case wäre übrigens die Konfiguration von Mocks in Tests, bei denen man die genaue Syntax spezieller Mock-Mechanismen oft nicht immer im Kopf hat.

Bekommen wir beim Testen einen Laufzeitfehler, kann – gerade in Kombination mit dem zugehörigen Code – der KI-Coding-Assistent ebenfalls bei der Problemanalyse helfen. Der Kontext kommt dann in Form von Code und Fehlermeldung. Dies ist insbesondere dann hilfreich, wenn man neu in einer (weitverbreiteten) Library oder einem Framework ist, da der KI-Assistent diese wahrscheinlich gut kennt und die Fehlermeldung einordnen kann. Der AI Assistant bietet dafür sogar einen expliziten Button in den Logs an.

Fazit zu KI-Coding-Assistenten

Wir hoffen, wir konnten mit dem Fallbeispiel die wichtigsten Funktionen von KI-Coding-Assistenten aufzeigen. Dazu gehören Auto Completion, Inline-Chat und separater Chat und deren effektive Nutzung durch geschickte Wahl der Aufgabengröße, des Kontexts und eigener und vorgefertigter Prompts. Wir hoffen auch, dass die Use Cases eine gute Anregung für die tägliche Arbeit des Lesers waren. Vielleicht wird bei der nächsten Code-Session erstmalig oder vermehrt einer der folgenden Use Cases mit KI-Unterstützung durchgeführt: Tests schreiben, Reguläre Ausdrücke oder SQL generieren, Funktionalität erweitern, Code reviewen, neue Features durch generierte Beispiele verstehen oder Fehler analysieren.

Autoren
Das könnte Sie auch interessieren

Neuen Kommentar schreiben

Kommentare (0)