cat posts/agent-im-loop-01-warum-die-schleife.md
Agent im Loop (Teil 1): Warum die Schleife?
Ein KI-Agent ist nur so gut wie seine Feedback-Schleife. Teil 1 richtet das Projekt ein, schreibt den ersten roten Test für eine Würfel-Engine – und klärt gleich, wie man überhaupt Zufall testet.
Einen Coding-Agenten wie Claude Code auf ein Stück Code loszulassen ist einfach. Verlässlichen Code zurückzubekommen ist schwieriger. Das Modell spielt dabei natürlich eine Rolle. Mindestens genauso wichtig ist aber die Rückkopplung: Woran erkennt der Agent, ob seine Änderung tatsächlich funktioniert?
Genau darum geht es in dieser Serie. Wir bauen eine kleine Würfel-Engine test-first auf und lassen Claude Code den Code dazu schreiben. Die Tests bilden dabei die Feedback-Schleife: ändern, ausführen, Fehler lesen, korrigieren, erneut ausführen. Je schneller und eindeutiger dieses Feedback ist, desto weniger Raum bleibt für Code, der plausibel aussieht, aber das falsche Problem löst.
Vorausgesetzt werden Python und erste Test-Erfahrung. Die pytest-Mechanik selbst steht in pytest von Null – hier geht es um den Test-First-Zyklus, nicht um pytest-Grundlagen.
Die These: Der Loop ist alles
Ohne Rückmeldung kann ein Coding-Agent nur beurteilen, ob sein Code plausibel aussieht. Mit ausführbaren Tests bekommt er ein deutlich härteres Kriterium: Entweder das beobachtete Verhalten stimmt oder der Test bleibt rot.
Der klassische TDD-Zyklus heißt red–green–refactor:
- Einen Test schreiben, der zunächst scheitert.
- Gerade genug Code schreiben, damit er grün wird.
- Den Code aufräumen, ohne sein Verhalten zu verändern.
Für einen Coding-Agenten ist dieser Zyklus besonders praktisch. Der Test beschreibt nicht nur in Prosa, was wir wollen, sondern liefert gleichzeitig ein maschinenprüfbares Akzeptanzkriterium. Er ersetzt den Prompt nicht, macht einen Teil davon aber ausführbar.
Das Projekt: eine Würfel-Engine
Durch die Serie wächst eine Engine für Würfelnotation. Wir starten mit 1d6, später
kommen 3d6+2, 4d6kh3 – vier Würfel werfen und die drei höchsten behalten – sowie
explodierende Würfel und Vorteil/Nachteil dazu.
Das Beispiel ist absichtlich überschaubar. Die eigentliche Würfellogik ist klein, die Notation produziert aber schnell interessante Edge Cases. Und der Zufall zwingt uns gleich am Anfang zu einer Frage, die bei Tests regelmäßig auftaucht: Wie prüft man Code, dessen Ergebnis sich bei jedem Aufruf ändern darf?
Setup
Als Projekt- und Paketmanager dient uv (siehe uv: pip und venv in schnell):
uv init --bare wuerfel-engine
cd wuerfel-engine
uv add --dev pytest
--bare ist hier bewusst gewählt. Ab uv 0.12 erzeugt ein normales uv init ein
Package mit src-Layout. Für die wenigen Dateien dieser Serie brauchen wir das nicht;
mit --bare bekommen wir ein minimales Projekt und können dice.py und test_dice.py
direkt ins Projektverzeichnis legen.
uv add --dev pytest trägt pytest in die Entwicklungs-Abhängigkeiten ein.
Claude Code startest Du anschließend im Projektverzeichnis:
claude
Claude Code kann das Repository lesen, Dateien ändern und Shell-Befehle ausführen.
Damit kann der Agent auch uv run pytest selbst starten und das Testergebnis direkt
in seine nächste Änderung einfließen lassen. Der Feedback-Loop läuft also innerhalb
derselben Session.
Der erste rote Test
Test zuerst. Unsere erste Behauptung lautet: Ein Wurf mit 1d6 muss immer zwischen
1 und 6 liegen. Dasselbe soll für andere Würfelgrößen gelten:
# test_dice.py
import pytest
from dice import roll
@pytest.mark.parametrize("sides", [4, 6, 8, 20, 100])
def test_ein_wuerfel_liegt_im_gueltigen_bereich(sides):
for _ in range(1000):
assert 1 <= roll(f"1d{sides}") <= sides
Dann:
uv run pytest
pytest scheitert bereits beim Import:
E ModuleNotFoundError: No module named 'dice'
Perfekt. dice.py existiert noch nicht, also kann der Test gar nicht grün sein. Für
den ersten Red-Schritt reicht das völlig. Wir haben ein erwartetes Verhalten
formuliert und bestätigt, dass der aktuelle Code es noch nicht erfüllt.
Grün – mit dem Agenten
Jetzt bekommt Claude Code ein enges Ziel. Zum Beispiel:
Implementiere nur so viel Code, dass der vorhandene Test grün wird.
Führe anschließend uv run pytest aus.
Der zweite Satz ist wichtig. Wir wollen nicht nur eine Codeänderung, sondern die Rückmeldung aus der Test-Suite.
Eine minimale Implementierung könnte so aussehen:
# dice.py
import random
def roll(notation, rng=None):
if rng is None:
rng = random
sides = int(notation.removeprefix("1d"))
return rng.randint(1, sides)
Danach:
..... [100%]
5 passed
Mehr brauchen wir im Moment nicht. Keine Unterstützung für 3d6, keine Modifikatoren,
kein Parser für die komplette Würfelsprache. Der Test verlangt 1dM, also bauen wir
1dM.
Gerade bei einem Coding-Agenten lohnt sich diese Begrenzung. Modelle ergänzen gern naheliegende Features gleich mit. Das wirkt produktiv, erzeugt aber Code, für den noch kein Test beschreibt, wie er sich verhalten soll.
Wie testet man Zufall?
Unser erster Test hat ein offensichtliches Loch:
def roll(notation, rng=None):
return 1
Auch diese Implementierung würde alle fünf Tests bestehen.
Der Range-Test prüft also eine Eigenschaft des Ergebnisses, aber noch nicht, ob tatsächlich gewürfelt wird. Tausend Durchläufe ändern daran nichts.
Python verwendet mit random.Random einen Pseudozufallszahlengenerator. Der produziert
keinen mystischen, untestbaren Zufall, sondern eine Folge von Werten aus einem internen
State. Starten zwei Generatoren mit demselben Seed, erzeugen sie unter denselben
Bedingungen dieselbe Folge.
Das können wir nutzen:
# test_dice.py (Ergänzung)
import random
def test_gleicher_seed_gibt_gleichen_wurf():
a = roll("1d20", random.Random(42))
b = roll("1d20", random.Random(42))
assert a == b
Damit wird ein zufälliger Ablauf reproduzierbar. Wenn ein Test fehlschlägt, können wir denselben Seed erneut verwenden und denselben Ablauf untersuchen.
Aber auch dieser Test beweist noch nicht, dass roll die Zufallsquelle korrekt
verwendet. Ein hart codiertes return 1 wäre weiterhin grün.
Für einen präzisen Unit-Test ist deshalb eine kontrollierte Zufallsquelle oft noch besser als ein Seed:
class FixedRng:
def randint(self, start, end):
assert (start, end) == (1, 20)
return 13
def test_roll_verwendet_injizierte_zufallsquelle():
assert roll("1d20", FixedRng()) == 13
Jetzt kontrolliert der Test beides: roll muss randint(1, 20) aufrufen und dessen
Ergebnis zurückgeben. Was der echte Zufallszahlengenerator dabei normalerweise
geliefert hätte, spielt keine Rolle.
Der entscheidende Schritt steckt bereits in unserer Implementierung:
def roll(notation, rng=None):
Die Zufallsquelle wird von außen übergeben. Im normalen Betrieb verwenden wir das
random-Modul, im Test können wir stattdessen einen geseedeten Generator oder einen
kleinen Fake einsetzen. Das ist Dependency Injection in ihrer unspektakulärsten Form –
und genau deshalb so praktisch.
Wie sich Zufall, Zeit und I/O auf diese Weise kontrollierbar machen lassen, behandelt der Begleitartikel Determinismus testengeplant ausführlicher.
Stolperfallen
- Grün bedeutet nur: Die vorhandenen Tests sind grün.
return 1zeigt ziemlich eindrucksvoll, dass eine grüne Test-Suite trotzdem eine falsche Implementierung akzeptieren kann. - Viele Zufallswerte ersetzen keinen guten Test. Tausend Würfe testen unsere Bereichsgrenzen tausendmal, aber nicht automatisch die eigentliche Würfellogik.
- Ein Seed macht einen Ablauf reproduzierbar, aber nicht automatisch korrekt. Für exakte Unit-Tests ist eine kontrollierte Zufallsquelle oft die bessere Grenze.
- Der Agent sollte nicht weiter bauen als der Test. Zusätzliche Features ohne Akzeptanzkriterium vergrößern nur die ungetestete Fläche.
Der Stand nach Runde eins
Wir haben noch keine beeindruckende Würfel-Engine. Wir haben etwas Nützlicheres: einen funktionierenden Loop.
Ein Test beschreibt das gewünschte Verhalten, Claude Code implementiert es, pytest liefert die Rückmeldung. Gleichzeitig haben wir die erste Schwäche unserer Tests gefunden und die Zufallsquelle so gebaut, dass sie sich kontrollieren lässt.
In Teil 2 geht es von 1dM zu NdM. Damit reicht ein einzelner Aufruf von randint
nicht mehr, und die Tests müssen präzise beschreiben, wie mehrere Würfel ausgewertet
werden. Genau dort wird aus der kleinen Schleife langsam ein brauchbares
Entwicklungswerkzeug für den Agenten.
Weiterlesen
- pytest von Null – die Test-Grundlagen unter dem Zyklus.
- uv: pip und venv in schnell – das Setup-Werkzeug.
- Python lernen (Teil 11): Werkzeugkasten und Ausblick
– das
random-Modul.
Sources: Claude Code – Dokumentation,
uv – Creating projects,
pytest – Dokumentation,
Python: random.
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.