Modul 07 · Lektion 3

Ein verlässlicher Arbeitsablauf fürs Programmieren mit KI

Erst Abnahmekriterien und Tests schreiben, dann die KI arbeiten lassen, und anhand der Testergebnisse statt der Selbstauskunft der KI beurteilen, ob sie fertig ist. Den Ablauf an einer echten kleinen Aufgabe durchspielen, dann: wie man KI-geschriebenen Code prüft und wann man keine KI einsetzen sollte.

  • Etwa 40 Minuten
  • Niveau: Fortgeschritten
  • Getestet: 2026-09-14 deepseek-flash, pytest 9.1

Code und Programmausgaben stehen genau so da, wie sie gelaufen sind – Kommentare und Ausgaben sind daher auf Chinesisch.

Beim Programmieren mit KI ist das häufigste Scheitern nicht, dass sie etwas nicht schreiben kann, sondern dass sie es geschrieben hat, „fertig“ sagt, du ihr glaubst und das Problem erst nach dem Livegang auffällt.

Die Ursache liegt meist an den beiden Enden: Am Anfang wurde nicht klar gesagt, wie „fertig“ aussieht, und am Ende wurde nicht objektiv geprüft, ob es wirklich fertig ist. Der Teil dazwischen, in dem die KI Code schreibt, macht dagegen am wenigsten Probleme.

Diese Lektion zeigt einen einfachen Arbeitsablauf, der beide Enden absichert. Er ist an kein Werkzeug gebunden und funktioniert mit Vervollständigung, Chat und Agent gleichermaßen.

Erst die „Kriterien für fertig“ schreiben

Bevor die KI loslegt, beantworte eine Frage: Woran erkenne ich nach der Änderung, dass sie richtig ist?

Die beste Antwort ist eine Reihe von Tests. Tests machen „fertig“ zu etwas, das man ausführen kann und das ein eindeutiges Ergebnis liefert: Alles besteht, dann ist es fertig; besteht einer nicht, ist es nicht fertig. Man muss weder darauf hören, was die KI sagt, noch nach Gefühl urteilen.

Zur Vorführung eine kleine Aufgabe. Der httpx-Frage-Antwort-Assistent kann beim API-Aufruf 429 bekommen (gedrosselt), und der Antwort-Header Retry-After sagt, wann man es erneut versuchen darf. Er hat zwei Schreibweisen: eine Sekundenzahl (120) oder ein HTTP-Datum (Wed, 21 Oct 2026 07:28:00 GMT). Wir brauchen eine Funktion, die daraus „noch so viele Sekunden warten“ macht.

Erst die Tests, mit allen Fällen, die mir einfallen:

from datetime import datetime, timezone

from retry_after import parse_retry_after

NOW = datetime(2026, 10, 21, 7, 0, 0, tzinfo=timezone.utc)


def test_seconds():
    assert parse_retry_after("120", NOW) == 120.0


def test_seconds_with_spaces():
    assert parse_retry_after("  30 ", NOW) == 30.0


def test_http_date_in_future():
    assert parse_retry_after("Wed, 21 Oct 2026 07:28:00 GMT", NOW) == 28 * 60.0


def test_http_date_in_past_means_no_wait():
    assert parse_retry_after("Wed, 21 Oct 2026 06:00:00 GMT", NOW) == 0.0


def test_negative_seconds_is_invalid():
    assert parse_retry_after("-5", NOW) is None


def test_decimal_seconds_is_invalid():
    # 标准规定秒数是非负整数,"1.5" 不合法
    assert parse_retry_after("1.5", NOW) is None


def test_garbage_is_invalid():
    assert parse_retry_after("soon", NOW) is None

(Alle 10 Tests stehen in code/07-ai-coding/test_retry_after.py.)

Das Schreiben der Tests hilft schon dabei, die Anforderung zu klären. Kommst du zu „was, wenn das Datum schon vorbei ist?“, musst du entscheiden, ob 0 oder eine negative Zahl zurückkommt; bei „ist 1.5 Sekunden gültig?“ musst du im Standard nachsehen. Klärst du diese Fragen nicht vorher, entscheidet die KI sie beliebig für dich.

Zusätzlich zu den Tests schreibt man eine kurze Aufgabenbeschreibung (TASK.md) mit Anforderungen, die Tests nicht ausdrücken können: nur die Standardbibliothek verwenden, die Testdatei nicht ändern, keine Sonderfälle für konkrete Werte aus den Tests schreiben. Der letzte Punkt ist wichtig: Ohne ihn schreibt eine „schlaue“ KI vielleicht Code wie „ist die Eingabe 120, gib 120.0 zurück“, der gezielt die Tests austrickst.

Die KI arbeiten lassen, mit Tests prüfen

code/07-ai-coding/ai_coding_loop.py macht aus diesem Ablauf ein kleines Programm: Aufgabenbeschreibung und Tests gehen an das Modell, es schreibt retry_after.py, die Tests laufen; besteht es nicht, geht die Testausgabe zur Überarbeitung zurück, höchstens 3 Runden:

for round_ in range(1, 4):
    reply = client.chat.completions.create(model=MODEL, messages=messages).choices[0].message.content
    TARGET.write_text(extract_code(reply))
    code, output = run_tests()
    summary = output.strip().splitlines()[-1] if output.strip() else ""
    print(f"第 {round_} 轮:退出码 {code},{summary}")
    if code == 0:
        print("测试全部通过。生成的代码:\n")
        print(TARGET.read_text())
        break
    messages += [{"role": "assistant", "content": reply},
                 {"role": "user", "content": f"测试没有通过,输出如下。修改代码,再给出完整的 retry_after.py:\n\n{output}"}]
else:
    print("3 轮都没有通过,停下来交给人看。最后一次的测试输出:\n" + output)

Ob es „fertig“ ist, entscheidet der Exit-Code von pytest: 0 heißt alles bestanden, ungleich 0 heißt Fehlschläge. Nicht das „ich bin fertig“ des Modells.

Das ist im Kleinen, was Programmieragenten wie Claude Code oder Codex tun: Code schreiben, Tests laufen lassen, Ergebnis ansehen, wieder ändern. Der Unterschied ist, dass sie selbst entscheiden, wann und welche Tests sie laufen lassen. Gib ihnen also eine Regeldatei mit den Testbefehlen (vorige Lektion), dann können sie selbst prüfen.

Die echten Ergebnisse

Ich habe es oft ausgeführt, in zwei Varianten.

Sieht das Modell die vollständige Aufgabenbeschreibung und die Tests, bestanden alle 4 Läufe gleich in der ersten Runde.

Bekommt das Modell nur einen Satz als Anforderung und sieht die Tests nicht (mit dem Parameter --vague; die Tests behalte ich für die Abnahme), bestanden 10 von 11 Läufen in der ersten Runde; 1-mal bestand in der ersten Runde 1 Test nicht, und nachdem es die Testausgabe bekommen hatte, bestand es in der zweiten Runde:

第 1 轮:退出码 1,1 failed, 9 passed in 0.01s
第 2 轮:退出码 0,10 passed in 0.00s

(Damals gab mein Skript die Namen fehlgeschlagener Tests noch nicht aus, deshalb weiß ich nicht, welcher es war. Das aktuelle Skript gibt Zeilen aus, die mit FAILED beginnen.)

Das ist der Code aus dem letzten Lauf, kein Zeichen geändert:

from datetime import timezone
from email.utils import parsedate_to_datetime


def parse_retry_after(value, now):
    if not isinstance(value, str):
        return None

    value = value.strip()
    if not value:
        return None

    if value.isascii() and value.isdigit():
        try:
            return float(int(value))
        except (ValueError, OverflowError):
            return None

    try:
        retry_time = parsedate_to_datetime(value)
    except (TypeError, ValueError, OverflowError):
        return None

    if retry_time is None:
        return None

    if retry_time.tzinfo is None:
        retry_time = retry_time.replace(tzinfo=timezone.utc)

    return max(0.0, (retry_time - now).total_seconds())

Ein Detail ist bemerkenswert: value.isascii() and value.isdigit(). Nur isdigit() reicht nicht; es liefert auch für arabische Ziffern, hochgestellte Ziffern und andere Nicht-ASCII-Zeichen True. Das zusätzliche isascii() des Modells schließt eine Lücke, die meine Tests nicht abdecken. Umgekehrt: Hätte es das nicht geschrieben, hätten meine 10 Tests es auch nicht bemerkt.

Ehrlich gesagt ist diese Aufgabe für heutige Modelle nicht schwer: Das Format von Retry-After ist im HTTP-Standard klar beschrieben, das Modell kennt es gut, und die Standardbibliothek hat mit parsedate_to_datetime bereits ein Werkzeug zum Parsen von HTTP-Daten.

Aber genau das ist der Sinn des Ablaufs: Du weißt vorher nicht, ob es diesmal falsch liegt. In 1 von 11 Fällen, bei nur einem Satz als Anforderung, übersah es ein Detail. Ohne Tests hättest du genau diesen fehlerhaften Code bekommen, und das Modell hätte dir „fertig“ gemeldet. Mit Tests wurde dieses Scheitern automatisch entdeckt und automatisch behoben, und du musstest nicht einmal nachsehen, wo der Fehler lag.

Kleine Schritte

Das Beispiel oben ist eine einzige Funktion. Echte Aufgaben sind oft viel größer: „Gib RepoBot eine Nutzeranmeldung.“ Bekommt die KI so eine Aufgabe, ist das häufigste Ergebnis: Sie ändert auf einmal ein gutes Dutzend Dateien, Hunderte Zeilen, die du nicht mehr überblickst, und du kannst nur alles annehmen oder alles verwerfen.

Besser zerlegt man große Aufgaben in kleine Schritte, die jeweils drei Bedingungen erfüllen:

  • Nur eine Sache. „Eine Nutzertabelle anlegen“ ist ein Schritt, „die Login-Schnittstelle schreiben“ ein anderer, „im Frontend ein Login-Feld ergänzen“ wieder einer.
  • Die Änderung ist so klein, dass du sie in wenigen Minuten durchsehen kannst. Ist ein Diff so groß, dass du ihn nicht ansehen willst, war der Schritt zu groß.
  • Eigene Abnahme. Am besten Tests, mindestens ein Ergebnis, das du von Hand prüfen kannst.

Nach jedem Schritt ein git-Commit. Geht etwas schief, kann man zum letzten guten Zustand zurück, statt ratlos vor einem Haufen vermischter Änderungen zu stehen.

Bevor es losgeht, kann man die KI auch erst nur einen Plan machen lassen, ohne zu handeln (der Plan- oder Nur-Lese-Modus aus Lektion 1 und der vorigen Lektion). Der Plan soll klar sagen: welche Dateien geändert werden sollen, was jeder Schritt tut, wie geprüft wird. Du liest den Plan, und wenn die Richtung stimmt, darf sie ändern. Ist die Richtung falsch, merkst du es in der Planungsphase und hast nur ein paar Minuten verloren.

Die Änderungen der KI prüfen

Bestandene Tests heißen nicht, dass alles in Ordnung ist. Tests prüfen nur die Fälle, an die du gedacht hast. Vor dem Mergen die Änderungen der KI durchsehen, besonders diese Stellen:

  • Hat sie Tests geändert? Um Tests zu bestehen, ändert die KI womöglich die Tests selbst oder löscht fehlschlagende. Darauf muss man am meisten achten.
  • Gibt es Sonderbehandlungen für Tests? Stehen im Code genau dieselben konkreten Werte wie in den Testfällen, sollte man stutzig werden.
  • Hat sie nebenbei anderes geändert? Du lässt sie einen Bug beheben, und sie „optimiert“ nebenbei drei nicht zugehörige Funktionen. Diese Änderungen sind von keinem Test abgedeckt und liegen außerhalb deiner Erwartung.
  • Grenzfälle und Fehlerbehandlung. Leere Werte, überlange Eingaben, Netzausfälle, Nebenläufigkeit. parse_retry_after oben fängt mit try/except den Fall ab, dass das Datum nicht geparst werden kann; genau das bestätigt man bei der Durchsicht.
  • Sicherheitsprobleme. Zusammengesetztes SQL, ausgeführte Befehle, verarbeitete Uploads von Nutzern, ausgegebene oder protokollierte Schlüssel. Die Probleme aus Modul 05, Lektion 8 und Modul 06, Lektion 5 tauchen in KI-Code genauso auf.
  • Neue Abhängigkeiten? KI installiert gern „nebenbei“ ein Paket, um ein Problem zu lösen. Jede neue Abhängigkeit ist eine dauerhafte Wartungslast und ein mögliches Sicherheitsrisiko.
  • Verstehst du es? Verstehst du ein Stück Code nicht, merge es nicht. Geht es später kaputt, musst du es reparieren können.

Wann man keine KI einsetzen sollte

  • Du weißt selbst nicht genau, was du willst. Die KI trifft gern Entscheidungen für dich, aber die sind nicht unbedingt richtig. Erst klären, Abnahmekriterien aufschreiben, dann loslegen.
  • Du kannst das Ergebnis nicht prüfen. In Gebieten, die du nicht verstehst, oder bei Code ohne Tests, den du auch nicht von Hand prüfen kannst, weißt du nicht, ob er stimmt, so überzeugend die KI ihn auch schreibt.
  • Die Änderung ist teuer und unumkehrbar. Datenbankmigrationen, Löschen von Daten, Konfiguration der Produktivumgebung. Solches kann die KI für dich schreiben, aber du musst es selbst prüfen und erst in einer Testumgebung bestätigen.
  • Du willst es lernen. Übungsaufgaben von der KI schreiben zu lassen ist, wie die erste Lektion dieses Kurses sagt, als würdest du jemanden an deiner Stelle ins Fitnessstudio schicken.

Der ganze Ablauf zusammengefasst

1. 想清楚:做完是什么样子?写成测试或者可检查的验收标准
2. 拆小:一步只做一件事
3. 计划:让 AI 先说它打算怎么做,你确认方向
4. 动手:让 AI 改,改动控制在你能看完的范围
5. 验证:跑测试,看退出码,不看 AI 的自述
6. 审查:看它改了什么,重点看测试、边界、安全、依赖
7. 提交:git commit,然后开始下一步
8. 复盘:它犯过的错,写进项目的规则文件(上一课)

Dieser Ablauf wirkt umständlicher als „die KI einfach schreiben lassen“. Aber die meiste Zeit kostet nie das Schreiben von Code, sondern Probleme finden, eingrenzen und beheben. Dieser Ablauf holt das Finden von Problemen nach vorn und begrenzt den Schaden eines Problems auf einen kleinen Schritt.

Übungen

  1. Führ ai_coding_loop.py und ai_coding_loop.py --vague jeweils einige Male aus und notiere, nach wie vielen Runden es jeweils besteht.
  2. Füg test_retry_after.py einen Test hinzu: Was soll parse_retry_after("Wed, 21 Oct 2026 07:28:00 +0800", NOW) zurückgeben? Sieh zuerst nach, was der HTTP-Standard über das Datumsformat verlangt, leg deine Antwort fest und prüfe dann, ob der Code der KI sie erfüllt.
  3. Wähl in deinem eigenen Projekt eine kleine Funktion und geh den Ablauf dieser Lektion ganz durch: erst Tests schreiben, dann die KI umsetzen lassen, Tests laufen lassen, prüfen, committen. Notiere, welche Probleme du bei der Durchsicht gefunden hast.

Selbsttest

1. Warum schreibt man Tests, bevor man die KI arbeiten lässt?

Tests machen „fertig“ zu einem ausführbaren Kriterium mit eindeutigem Ergebnis. Ob die Aufgabe erledigt ist, entscheidet dann das Testergebnis, nicht das „fertig“ der KI. Das Schreiben der Tests zwingt dich außerdem, die unklaren Stellen der Anforderung zu klären (etwa was bei einem abgelaufenen Datum passiert oder ob Dezimalzahlen gültig sind), statt diese Entscheidungen beliebig der KI zu überlassen.

2. Alle Tests bestehen. Muss man den Code der KI trotzdem prüfen? Worauf achtet man dabei besonders?

Ja. Tests prüfen nur die Fälle, an die du gedacht hast. Bei der Durchsicht besonders darauf achten: ob sie Tests geändert oder gelöscht hat, ob es Sonderbehandlungen für Testfälle gibt, ob nebenbei Unzugehöriges geändert wurde, Grenzfälle und Fehlerbehandlung, Sicherheitsprobleme, neue Abhängigkeiten und ob du den Code verstehst.

3. Warum zerlegt man große Aufgaben in kleine Schritte und committet nach jedem Schritt?

Gibt man der KI eine große Aufgabe auf einmal, sind die Änderungen zu umfangreich für eine gründliche Prüfung; man kann nur alles annehmen oder alles verwerfen, und Probleme sind schwer einzugrenzen. In kleinen Schritten ist jede Änderung klein genug zum Durchsehen und hat ihre eigene Abnahme; mit einem Commit pro Schritt kann man bei Problemen zum letzten guten Zustand zurück.