Wegen ChatGPT und anderen Chat-Modellen steht die Programmierwelt vor einem nie dagewesenen Umbruch. Im Handumdrehen lassen sich jetzt beliebige Programme generieren. Um die Qualität des Quellcodes zu gewährleisten, sollte man ihn jedoch verstehen oder zumindest mit automatischen Tests prüfen.
Die meisten Programmierumgebungen enthalten heutzutage Werkzeuge wie GitHubs CoPilot oder ähnliche Erweiterungen. Selbstverständlich erleichtern Sie mitunter die tägliche Arbeit. Allerdings stelle ich mir seit einiger Zeit immer häufiger die Frage: Wie wird die professionelle Softwareentwicklung in fünf Jahren aussehen? Ich finde darauf keine definitive Antwort.
Jens-Christoph Brendel hat sich im Artikel “Haste Töne” [1] dem Thema praktisch genähert. Er hat ChatGPT die Aufgabe gestellt, ein Python-Programm zu generieren, das aus den Namen von Musiknoten, beispielsweise C, E und G, den Namen des Akkords ermittelt (hier C-Dur). Dabei hat er absichtlich ein untypisches Thema gewählt, denn für gängige Aufgaben wie Sortier-Algorithmen kennt ChatGPT wahrscheinlich mehrere Hundert Beispiele, von denen es einfach den Code ableiten kann.
Weil ich es für interessant hielt, dieselbe Aufgabe mit Rust auszuprobieren, findet sich die vorliegende Folge Planet Rust ausnahmsweise unter der Titelrubrik. Falls Sie keine musikalische Ausbildung haben, können Sie trotzdem getrost weiterlesen. Der Schwerpunkt des Artikels liegt auf den Tricks und Untiefen beim Generieren von Programmen. Zusätzlich bietet es sich an, in diesem Zusammenhang automatische Tests zu verwenden, die standardmäßig Bestandteil von Rust sind. Damit lässt sich schnell prüfen, ob das erzeugte Programm zumindest die grundlegenden Anforderungen erfüllt.
Fragen an ChatGPT
Wenn Sie sich ein Programm von einem Chat-Modell generieren lassen möchten, bestimmt die Art der Fragestellung maßgeblich die Ergebnisqualität. Einige Hinweise dazu finden Sie im Kasten “Tipps und Tricks beim Generieren von Programmen”. Wenn ich selbst die Ratschläge befolge, könnte eine Anfrage (ein Prompt) an ChatGPT für die Generierung eines Rust-Programms so aussehen: “Schreibe bitte eine Rust-Funktion, die eine Liste von drei oder vier Namen für Musiknoten als Eingabe bekommt. Ermittle, um welchen Akkord es sich handelt und gib dessen Namen als String aus. Beispiel: c4, e4, g4 liefert das Ergebnis C-Dur. Die Funktion hat den Aufbau fn get_chord(chord: Vec<&str>) -> String.” Die Antwort von ChatGPT sehen Sie in Listing 1.
Tipps und Tricks beim Generieren von Programmen
Kommunikation mit Chat-Modellen funktioniert grundsätzlich so wie im richtigen Leben: Sag, was du willst! Eventuell bekommst du es dann. Wenn Sie fünf Tipps beherzigen, bekommen Sie bessere Antworten auf Ihre Anfragen.
Zunächst beschreiben Sie möglichst genau und ausführlich, was das Programm tun soll (mit Eingabe, Funktionalität und Ausgabe). Statt beispielsweise “Schreibe ein Programm, das Zahlen verarbeitet”, präzisieren Sie die Aufgabe: “Schreibe ein Rust-Programm, das eine Liste von ganzen Zahlen als Eingabe bekommt, diese sortiert und die größte Zahl zusammen mit der kleinsten ausgibt.”
Ein kurze Beschreibung der verwendeten Plattform schadet keinesfalls: “Das Programm soll man sowohl auf dem Desktop als auch im Web verwenden können.”
Zusätzlich sollten Sie dem Modell die gewünschten Bibliotheken oder Schnittstellen verraten: “Für die asynchrone Verarbeitung ist immer die Bibliothek tokyo zu verwenden.”
Der vierte Merksatz lautet: Glaube nicht einfach, was ein Chat-Modell generiert. Es ist immer sinnvoll, das generierte Programm wirklich zu verstehen – notfalls lassen Sie es sich Zeile für Zeile vom Modell erklären.
Schließlich lohnt es sich auch, komplexe Aufgaben in mehrere Teile zu splitten. “Schreibe ein Rust-Programm, das in 3D eine komplette Stadt simuliert”, lässt sich erfolgsversprechender in mehreren Schritten formulieren. “Schreibe eine Funktion, die Daten, die mit ‘A’ beginnen, aus einer Datei liest.” Danach folgt “Füge bitte eine Funktion hinzu, die die Daten so und so verarbeitet”. So fahren Sie fort, bis Sie das gewünschte Ziel erreicht haben.
Listing 1
Erstes Programm
use std::collections::HashSet;
fn get_chord(chord: Vec<&str>) -> String {
// Normalisieren der Eingabe: Entfernen der Oktavenummern (z.B. "c4" wird zu "c")
let notes: HashSet<&str> = chord.iter().map(|note| ¬e[..note.len() - 1]).collect();
// Bekannte Akkorde definieren
let major_chords = vec![
("C Dur", vec!["c", "e", "g"]),
("D Dur", vec!["d", "fis", "a"]),
("E Dur", vec!["e", "gis", "h"]),
("F Dur", vec!["f", "a", "c"]),
("G Dur", vec!["g", "h", "d"]),
("A Dur", vec!["a", "cis", "e"]),
("H Dur", vec!["h", "dis", "fis"]),
];
let minor_chords = vec![
("C Moll", vec!["c", "es", "g"]),
("D Moll", vec!["d", "f", "a"]),
("E Moll", vec!["e", "g", "h"]),
("F Moll", vec!["f", "as", "c"]),
("G Moll", vec!["g", "b", "d"]),
("A Moll", vec!["a", "c", "e"]),
("H Moll", vec!["h", "d", "fis"]),
];
// Prüfen, ob die Noten zu einem bekannten Dur-Akkord passen
for (name, chord_notes) in major_chords {
if notes == chord_notes.into_iter().collect::<HashSet<_>>() {
return name.to_string();
}
}
// Prüfen, ob die Noten zu einem bekannten Moll-Akkord passen
for (name, chord_notes) in minor_chords {
if notes == chord_notes.into_iter().collect::<HashSet<_>>() {
return name.to_string();
}
}
// Wenn kein passender Akkord gefunden wurde
"Unbekannter Akkord".to_string()
}
Auf den ersten Blick sieht das Programm ganz gut aus – ich konnte es ohne Fehler kompilieren. Es hat den C-Dur Akkord erkannt, der aus den Noten C4, E4 und G4 besteht. Der Ton C kommt allerdings öfter auf einer Piano-Tastatur vor, daher steht hinter C eine Zahl. Für einen Test mit E4, G4 und jetzt C5 lieferte die Funktion »get_chord« (zweite Zeile) ebenfalls das richtige Ergebnis C-Dur.
Daraufhin habe ich mir die generierte Funktion genauer angesehen. Dabei fällt auf, dass die Funktion nichts allgemeingültig berechnet, sondern nur vorgegebene Töne vergleicht. Die Funktion »get_chord« hat eine Liste mit drei Tönen und den Namen des entsprechenden Akkords. Dass die Abstände zwischen den Tönen den Dur-Akkord ausmachen, weiß die Funktion nicht. Von den schwarzen Tönen auf dem Piano (cis, es und so weiter) gibt es genauso Akkorde (Abbildung 1). Sie tauchen jedoch nicht in der Liste der Funktion auf, weswegen das Modell sie nicht erkennt.

Abbildung 1: Aus drei oder mehr Tönen lassen sich Akkorde bilden. Alle Töne der Tasten, der schwarzen und der weißen, haben zueinander immer den gleichen Abstand.
Das weitere Vorgehen beim Verbessern des Programms wäre, ChatGPT zu informieren, was nicht passt. Anschließend generiert es eine neue Version der Funktion. Sie testen erneut und teilen ChatGPT Abweichungen mit. Das Spiel wiederholen Sie so lang, bis alles passt oder Sie die Lust am Testen verloren haben. Hierbei bietet sich Test-driven Development (TDD) an, eine Softwareentwicklungsmethode, bei der man Tests erstellt und das Programm sie automatisch ausführt.
Test-driven Development
Die grundsätzliche Idee hinter Test-driven Development (TDD) ist relativ einfach: Sie testen nicht direkt selbst, sondern schreiben eine Funktion, die den Test ausführt und prüft, ob das Ergebnis richtig oder falsch ist. Liefert das erstellte Programm nicht das gewünschte Ergebnis, ändern Sie es, bis die Test-Funktion ihr o.k. dazu gibt. Der Vorteil ist, dass das Testen keinen Aufwand verursacht, da das die Funktion übernimmt.
Die zusätzliche Arbeit liegt vielmehr darin, möglichst viele und sinnvolle Tests zu konstruieren, die die unterschiedlichen Eingaben und deren korrekten Ergebnisse abbilden – Stichwort Testabdeckung. Dafür benötigen Sie das passende Fachwissen. Wenn ich keine Ahnung von Musik und Akkorden habe, werde ich an geeigneten Tests scheitern. Genauso kann ich die von ChatGPT generierten Programme nicht beurteilen. Das Fachwissen, zusammen mit sehr guten Programmierkenntnissen, ist essenziell.
Im Zusammenhang mit TDD fällt häufig der Begriff Red-Green-Refactor-Zyklus. Red entspricht: Schreibe einen Test, der ohne die passende Funktionalität im zu erstellenden Programm fehlschlägt. Green bedeutet: Ändere das Programm, bis es den Test besteht. Refactor meint: Verbessere das Programm, ohne die Funktionalität zu ändern.
Da das Chat-Modell aufgrund der Hinweise, dass dies oder jenes nicht passt, stetig neue Versionen generiert, lautet die Aufgabe, sie wieder und wieder zu prüfen – der klassische Red-Green-Refactor-Zyklus. In Rust ist der Befehl »assert!« der zentrale Baustein für automatische Tests.
assert!(get_chord(vec!["c4", "e4", "g4"])=="C Dur","C Dur nicht erkannt!");
Der erste Parameter ist der eigentliche Test. Hat dieser den Wert »true«, ist der Test bestanden – in unserem Fall also, wenn die Funktion »get_chord« den erwarteten Wert zurückliefert. Als zweiten Parameter kann man zusätzlich eine Meldung hinzufügen, die den Fehlerfall genauer beschreibt.
Vom Befehl »assert!« gibt es die beiden Varianten »assert_eq!« (Test auf “equal” – gleich), wie in Listing 2, Zeile 8 zu sehen, und »assert_ne!« (Test auf “not equal” – nicht gleich). Damit vereinfacht sich der Befehl, da Sie sich das Gleichheitszeichen »==« sparen können.
Listing 2
Erste Tests
#[cfg(test)]
mod tests {
//importing
names from outer (for mod tests) scope.
use super::*;
#[test]
fn test_cdur() {
assert_eq!(get_chord(vec!["c4", "e4", "g4"]),"C Dur","C Dur nicht erkannt!");
}
#[test]
fn test_cmoll() {
assert_eq!(get_chord(vec!["c4", "es4", "g4"]),"C Moll","C Moll nicht erkannt!");
}
}
Testen mit Rust
Eine ausschließlich zum Testen eingesetzte Funktion erhält in Rust das Attribut »#[test]« (Listing 2, Zeile 5). Dadurch verwendet Rust sie nur im Testfall und blendet sie ansonsten aus. Listing 2 enthält zwei Funktionen, die prüfen, ob der C-Dur- und der C-Moll-Akkord von »get_chord« erkannt wird.
Damit die Tests möglichst nahe beim eigentlichen Programm abgelegt sind, fügen Sie sie in einem eigenen Modul »mod tests« (zweite Zeile) einfach nach der Funktion »main« in den Quelltext ein. Besteht das Programm aus mehreren Bibliotheken, enthält jede Bibliothek eigene Tests, die sich auf deren Funktionalität beziehen.
Das Modul »tests« besitzt das Attribut »#[cfg(test)]«. Dadurch nutzt Rust dieses Modul nur bei der Konfiguration »test«, also im Testfall. Wenn Sie die Tests ausführen möchten, brauchen Sie lediglich »cargo test« im Terminal eingeben – im Gegensatz zu »cargo run« für die standardmäßige Ausführung. Doch das Modul »tests« ist an sich eine eigene Welt. Damit es einen Bezug zu seiner Umgebung bekommt, ergibt es Sinn, auf das Kommando »use super::*;« zurückzugreifen.
Mehr Tests
Die beiden ersten Tests konnte die von ChatGPT generierte Funktion »get_chord« gut meistern. Jetzt benötige ich noch einige Testfunktionen, die mehr prüfen. Dazu gehören Dreiklänge auf Basis der Töne der schwarzen Tasten (F#, Eb und so weiter), aus vier Tönen bestehende Akkorde, Töne des Akkords in unterschiedlicher Reihenfolge (Dreiklangumkehrung), andere Akkorde, die nicht Dur oder Moll sind, etwa G7 (Septakkord), und eine schwarze Taste mit zwei Namen, beispielsweise Eb und Dis (Enharmonische Verwechslung).
Listing 3 zeigt zum Beispiel die Test-Funktion für den C-Dur-Akkord mit geänderter Reihenfolge der Töne. Da es sich um einfache Tests handelt, bestehen sie nur aus einem »asserts!«-Befehl. Für Test-Funktionen gibt es in Rust keinerlei Einschränkungen, sie können beliebig komplex ausfallen.
Listing 3
C-Dur-Akkord mit geänderter Tonreihenfolge
#[test]
fn test_cdur_umkehrung() {
assert_eq!(get_chord(vec!["e4", "g4", "c5"]),"C Dur",
"[e4, g4, c5] C Dur Umkehrung nicht erkannt!");
}
Drei der Testfunktionen weisen bei der ersten Version von ChatGPT auf einen Fehler hin. Die Fehlermeldung gebe ich an ChatGPT weiter. “Diese Fehler sind aufgetreten:[c4, dis4, g4]+ C Moll nicht erkannt mit Dis! [f4, a4, c5, es5]+ F7 nicht erkannt [des4, f4, as4] Des Moll nicht erkannt!” Das Modell ändert nicht viel und ergänzt für die nicht identifizierten Akkorde in seiner Auflistung ein paar Zeilen (Listing 4).
Listing 4
Ergänzungen für nicht erkannte Akkorde
let minor_chords = vec![
("C Moll", vec!["c", "dis", "g"]),
...
("Des Moll", vec!["des", "f", "as"]),
];
let dominant_seventh_chords = vec![
("F7", vec!["f", "a", "c", "es"]),
];
Jetzt erkennt das Programm zwar den F7-Akkord, aber was ist mit den anderen Septakkorden, wie G7, A7 und so weiter? Genauso geht ChatGPT bei den Moll-Akkorden vor. Es fügt Des-Moll hinzu, darüber hinaus gibt es allerdings noch Es-Moll, Fis-Moll und so weiter. Den C-Moll-Akkord mit Dis identifiziert es zwar, doch dafür unterschlägt es jetzt den C-Moll-Akkord mit Es. Nächste Anfrage an ChatGPT: “Es sind Fehler aufgetreten: [c4, es4, g4] C Moll nicht erkannt!”
Die einzige Änderung am Programm durch ChatGPT ist, dass es das »”dis”« in C-Moll wieder durch »”es”« (Listing 5) ersetzt. Damit erzeugt der Test mit Dis erneut einen Fehler. Jetzt könnte ich zwar nochmals monieren, dass auch dieser Test einen Fehler bringt, aber das klingt nach einer Endlosschleife
Listing 5
Von dis zurück zu es
let minor_chords = vec![
("C Moll", vec!["c", "es", "g"]),
...
];
Testabdeckung
Eigentlich wollte ich an dieser Stelle ein Testprogramm erstellen, das alle möglichen Akkorde erzeugt – nicht nur Dur und Moll-Akkorde, sondern auch Moll-Septim-Akkorde, erweiterte Akkorde, verminderte Akkorde und so weiter. Allerdings bringt mich das kaum weiter, wenn ChatGPT die einfachen Tests nicht auflösen kann.
Eventuell liegt es also eher an meiner Herangehensweise. Bloß zu sagen, das ist falsch oder das andere ist falsch, bringt offensichtlich wenig. Vermutlich sollte ich dem Modell vielmehr mitteilen, was es konkret verbessern sollte. Also konstruktive Kritik geben – wie im richtigen Leben. “Erweitere die Funktion get_chord für alle Septim-Akkorde, die es gibt. Erstelle für alle Halbtöne (Schwarze Tasten auf dem Klavier) ebenfalls eine Akkord-Erkennung. Wie bekannt, können die schwarzen Tasten zwei verschiedene Namen haben (Enharmonische Verwechslung). Berücksichtige das.”
Das Programm von ChatGPT erkennt jetzt die unterschiedlichen Namen der schwarzen Tasten (Listing 6). Die Septim-Akkorde hat es ebenfalls hinzugefügt, die Akkorde für die Halbtöne fehlen jedoch noch immer.
Listing 6
Ergebnis nach konstruktiver Kritik
let note_aliases = vec![
("cis", "des"), ("dis", "es"), ("fis", "ges"), ("gis", "as"), ("ais", "b"),
];
Also starte ich den nächsten Versuch: “Erstelle für alle Halbtöne (Schwarze Tasten auf dem Klavier) ebenfalls eine Akkord-Erkennung.” Die entsprechenden Akkorde sind in der neuen Version (Listing 7) enthalten.
Listing 7
Neue Version
let major_chords = vec![
("C Dur", vec!["c", "e", "g"]),
("Des Dur", vec!["des", "f", "as"]),
("D Dur", vec!["d", "fis", "a"]),
("Es Dur", vec!["es", "g", "b"]),
...
];
Andere LLMs
Derzeit tut sich sehr viel im Bereich der unterschiedlichen Chat-Modelle. Ich persönlich verwende häufig Claude.ai [2], weil ich den Eindruck habe, dass manche Strukturen beim Programmieren besser sind. Mit Fakten lässt sich das allerdings nicht belegen.
Die erste Anfrage an ChatGPT habe ich genauso claude.ai gestellt. Das Ergebnis ähnelte dem von ChatGPT: Claude hat eine Liste von Akkorden erstellt und verglichen, welche es sein könnten. Die Septakkorde hat es erkannt, die Akkorde auf den schwarzen Tasten jedoch ebenfalls nicht. Außerdem fehlten einige Moll- und Dur-Akkorde.
Eine alternative Herangehensweise an Aufgaben legt das chinesische Modell DeepSeek an den Tag – die Reasoning-Funktionalität löst momentan geradezu einen Hype aus. Fragt sich, ob sie bei unserer Aufgabenstellung etwas bringt. Das Ergebnis derselben Anfrage an DeepSeek (Listing 8) hat mich tatsächlich arg überrascht.
Listing 8
DeepSeek-Variante
fn get_chord(chord: Vec<&str>) -> String {
// Parse notes into MIDI semitone values (mod 12)
let mut notes: Vec<i32> = chord.iter()
.filter_map(|s| parse_note(s))
.collect();
if notes.len() < 3 {
return "Ungültiger Akkord".to_string();
}
// Remove duplicates and sort
notes.sort();
notes.dedup();
let notes = notes;
// Check all possible roots
for &root in ¬es {
let mut diffs: Vec<i32> = notes.iter()
.map(|&n| (n - root + 12) % 12)
.collect();
diffs.sort();
diffs.dedup();
// Check against chord patterns
for pattern in CHORD_PATTERNS {
if &diffs == &pattern.intervals {
return format!("{}{}", get_note_name(root), pattern.chord_type);
}
}
}
"Unbekannter Akkord".to_string()
}
// Helper functions and data structures
struct ChordPattern {
intervals: Vec<i32>,
chord_type: &'static str,
}
const CHORD_PATTERNS: &[ChordPattern] = &[
// Triads
ChordPattern { intervals: vec![0, 4, 7], chord_type: " Dur" },
ChordPattern { intervals: vec![0, 3, 7], chord_type: " Moll" },
ChordPattern { intervals: vec![0, 3, 6], chord_type: " vermindert" },
ChordPattern { intervals: vec![0, 4, 8], chord_type: " übermäßig" },
// Seventh chords
ChordPattern { intervals: vec![0, 4, 7, 10], chord_type: "7" },
ChordPattern { intervals: vec![0, 3, 7, 10], chord_type: "m7" },
ChordPattern { intervals: vec![0, 3, 6, 9], chord_type: " vermindert7" },
ChordPattern { intervals: vec![0, 3, 6, 10], chord_type: " halbvermindert" },
ChordPattern { intervals: vec![0, 4, 8, 11], chord_type: " übermäßig7" },
];
fn parse_note(note: &str) -> Option<i32> {
let mut chars = note.chars().peekable();
let c = chars.next()?.to_ascii_uppercase();
let accidental = match chars.peek() {
Some('#') => { chars.next(); 1 },
Some('b') => { chars.next(); -1 },
_ => 0,
};
// Skip octave number
while chars.next().is_some() {}
let base = match c {
'C' => 0,
'D' => 2,
'E' => 4,
'F' => 5,
'G' => 7,
'A' => 9,
'B' => 11,
_ => return None,
};
let value = (base + accidental + 12) % 12;
Some(value)
}
fn get_note_name(value: i32) -> String {
match value {
0 => "C",
1 => "C#",
2 => "D",
3 => "D#",
4 => "E",
5 => "F",
6 => "F#",
7 => "G",
8 => "G#",
9 => "A",
10 => "A#",
11 => "B",
_ => unreachable!(),
}.to_string()
}
Das Modell hat einige Minuten gerechnet und währenddessen über seine Vorgehensweise resümiert. Die gelieferte Funktion ist allgemeingültig und basiert nicht auf statischen Tabellen. DeepSeek definiert die einzelnen Akkordtypen, neben Dur und Moll zudem ausgefallenere, wie verminderte oder halbverminderte. Die Funktion »get_chord« hat sämtliche Tests bestanden. Die Reasoning-Funktionalität unterteilt den Lösungsweg in kleinere Schritte und versucht, sie einzeln anzugehen. Mehr dazu lesen Sie im Kasten “Erklärung zu DeepSeek”.
In der Familie der ChatGPT-Modelle finden sich ebenfalls Mitglieder mit Reasoning-Funktionalität. Sie habe ich zusätzlich mit der Anfrage für das Beispielprogramm konfrontiert. Trommelwirbel – die Ausgabe orientiert sich an der von DeepSeek. Den Unterschied macht eindeutig die Reasoning-Funktionalität.
Erklärung zu DeepSeek
Im ersten Schritt wandelt die Funktion »parse_note« Notennamen in Halbtonwerte (0 bis 11) um und ignoriert Oktavangaben. Daraufhin geht es an die Vorverarbeitung, also das Entfernen von Duplikaten, das Sortieren der Noten und Überprüfen auf Mindestlänge (drei Noten).
Anschließend berechnet das Programm für jede Note als möglichen Grundton die Intervalle, vergleicht mit bekannten Akkordmustern und gibt den ersten passenden Akkord zurück. Im vierten Schritt geht es um die Namensgebung. Ein Grundton wird dabei aus dem Halbtonwert bestimmt, während der Akkordtyp aus dem Intervallmuster abgeleitet wird.
Unterstützte Akkordtypen sind: Dur, Moll, vermindert, übermäßig sowie Dominantseptakkord, Mollseptakkord, verminderter Septakkord, halbverminderter Septakkord und übermäßiger Septakkord.
Fazit und Ausblick
Beim Arbeiten mit Chat-Modellen spielt die richtige Herangehensweise die Hauptrolle. Ich denke, es gibt bereits heute kaum noch jemanden in der Programmierung, der ohne Chat-Modelle arbeitet. Fachwissen und zusätzlich tiefergreifende Programmierkenntnisse wie Programming-Patterns gewinnen immer mehr an Bedeutung. Kleine Teams mit geeigneten Personen können eine beachtlich hohe Produktivität erreichen.
Was Reasoning und ähnliche Funktionen noch bringen werden, bleibt freilich bis dato Spekulation, doch das schon jetzt damit Mögliche wirkt vielversprechend. Mir macht es jedenfalls Spaß, schneller zu sein und mich weniger mit Routineaufgaben herumschlagen zu müssen. Die nimmt Ihnen ein Chatmodell nämlich ab. Rust ist der Weg. (csi)
Infos
- Python mit KI: Jens-Christoph Brendel, “Haste Töne”, LM 05/2025, S. 0, https://www.lm-online.de/51796
- Claude.ai: https://claude.ai/new





