Handbücher · Loop Engineering
Build: Der Kritiker
Lernziele
- Du setzt hinter den Zieltest einen Kritiker in eigener Session, der nur Auftrag, Ausgangslage, Diff und Rubrik sieht.
- Du schreibst eine Rubrik mit vier prüfbaren Regeln und einem festen Urteilsformat und machst daraus den Auftrag für den nächsten Versuch.
- Du erkennst, wann Kritiker und Zieltest sich widersprechen, und gibst dem Kritiker den Kontext, der das verhindert.
Der Loop aus Build: Der Zieltest hält an, sobald alles grün ist. Dieser Teil setzt ein zweites Urteil dazu: grün und sauber. Ein eigener Prüflauf liest den Diff · engl. diffDie Gegenüberstellung zweier Stände einer Datei: Zeilen mit Plus kamen dazu, Zeilen mit Minus fielen weg. Git zeigt so, was ein Commit geändert hat.Im Glossar gegen eine enge Rubrik. Seine Mängel gehen als konkreter Auftrag in den nächsten Versuch. Die Theorie dazu steht in Kritiker und Prüfung. Hier kommen knapp vierzig Zeilen Bash dazu.
Zwei Entwurfsregeln tragen den Build. Der Kritiker · engl. criticDie Instanz, die einen Versuch im Loop bewertet: Code wie Tests und Linter oder ein zweiter Modellaufruf mit enger Rubrik. Der Kritiker arbeitet getrennt vom Agenten, der die Arbeit gemacht hat.Im Glossar läuft in einer eigenen Session · engl. sessionEin einzelnes Gespräch mit einem Agenten: der Verlauf aller Nachrichten und Tool-Ergebnisse plus der Arbeitszustand. Endet das Gespräch, endet die Session.Im Glossar: ein frischer Aufruf von claude -p ohne Werkzeuge. Er sieht den Auftrag, die Ausgangslage, den Diff und die Rubrik. Das Protokoll des AgentEin System, in dem ein Sprachmodell seinen nächsten Schritt selbst wählt: Es ruft Werkzeuge auf, wertet das Ergebnis aus und macht weiter, bis das Ziel erreicht ist.Im Glossar, der die Arbeit gemacht hat, sieht er nie, also erbt er auch dessen Begründungen nicht. Die Rubrik ist eng und prüfbar: vier Regeln, ein festes Format für das Urteil und ein ausdrückliches Verbot, über anderes zu reden.
Du brauchst dieselben Werkzeuge wie im ersten Build: claude, jq, pytest, ruff und ein Repository mit Tests. Getestet haben wir mit Claude Code 2.1.323, Haiku als Agent und Sonnet als Kritiker (Stand: Nov. 2026).
Das Skript
Speichere das Skript als ~/bin/critic-loop.sh und starte es im Ordner des Repositorys. Aus demselben Grund wie im ersten Build liegt es außerhalb des Repos. Die Schritte fassen bekannte Teile aus dem ersten Build nur kurz zusammen.
#!/usr/bin/env bash
# critic-loop.sh: der Zieltest entscheidet, ob es stimmt. Ein Kritiker
# in frischer Session entscheidet, ob es sauber ist. Liegt außerhalb
# des Repos und startet im Repo-Ordner: ~/bin/critic-loop.sh
set -u
TASK="Make python3 -m pytest and ruff check . pass. Do not delete, skip or weaken any test."
MAX_ATTEMPTS=5
BUDGET_PER_ATTEMPT=0.50 # USD, Schätzung des Clients
MODEL=haiku
CRITIC_MODEL=sonnet
BASE_REF=$(git rev-parse HEAD)
goal_test() {
python3 -m pytest -q --tb=short && ruff check .
}
# Ausgangslage für den Kritiker: beide Prüfungen, auch wenn pytest rot ist
BASELINE=$( { python3 -m pytest -q --tb=line; ruff check .; } 2>&1 )
critic() {
local out
out=$(git diff "$BASE_REF" | claude -p \
"You are a code reviewer. The task was: $TASK
Before the change, the goal test failed with:
$BASELINE
Judge the diff on stdin against exactly four rules:
1. No test was deleted, skipped or weakened.
2. No debug prints, dead code or commented-out blocks were added.
3. Errors are handled, not silently swallowed.
4. The change stays inside the task; no drive-by refactoring.
If all four hold, reply with the single word PASS.
Otherwise list each violation as: file, the problem, the one-line fix.
Judge only these rules. Never comment on style or architecture." \
--model "$CRITIC_MODEL" \
--tools "" \
--max-budget-usd "$BUDGET_PER_ATTEMPT" \
--setting-sources project,local \
--output-format json)
printf '%s' "$out" | jq -r '"Kritiker fertig: \(.total_cost_usd) USD"' >&2
printf '%s' "$out" | jq -r '.result'
}
feedback=""
attempt=1
while [ "$attempt" -le "$MAX_ATTEMPTS" ]; do
printf '=== Versuch %s von %s ===\n' "$attempt" "$MAX_ATTEMPTS"
prompt="$TASK"
if [ -n "$feedback" ]; then
prompt="$TASK
The previous attempt was rejected:
$feedback
Fix exactly these points, nothing else."
fi
claude -p "$prompt" \
--model "$MODEL" \
--allowedTools "Read,Edit,Bash(python3 -m pytest *),Bash(ruff *)" \
--permission-mode acceptEdits \
--max-budget-usd "$BUDGET_PER_ATTEMPT" \
--setting-sources project,local \
--output-format json < /dev/null \
| jq -r '"Agent fertig: \(.num_turns) Runden, \(.total_cost_usd) USD"'
if ! feedback=$(goal_test 2>&1); then
printf 'FAIL: Zieltest rot\n'
attempt=$((attempt + 1))
continue
fi
verdict=$(critic)
if [ "$verdict" = "PASS" ]; then
printf 'PASS: grün und sauber in Versuch %s\n' "$attempt"
exit 0
fi
feedback="The goal test passes, but the reviewer found defects:
$verdict"
printf 'KRITIK: Zieltest grün, Kritiker lehnt ab:\n%s\n' "$verdict"
attempt=$((attempt + 1))
done
printf 'STOP: %s Versuche verbraucht, zurück an den Menschen\n' "$MAX_ATTEMPTS"
exit 1Zeilen 7-12
Der Auftrag nennt beide Prüfungen des Zieltests beim Namen, pytest und ruff. Neu sind zwei Zeilen: CRITIC_MODEL wählt ein anderes Modell als der Agent, und BASE_REF hält fest, auf welchem Commit der Loop startet.
Was im Test-Repo passierte
Wir haben dasselbe Test-Repo benutzt wie im ersten Build: rechnung.py mit drei Fehlern und einem überflüssigen Import, sechs Tests, vier davon rot. Der erste Lauf:
$ ~/bin/critic-loop.sh
=== Versuch 1 von 5 ===
Agent fertig: 11 Runden, 0.01766871 USD
Kritiker fertig: 0.0112291 USD
PASS: grün und sauber in Versuch 1
Der Lauf dauerte rund zwanzig Sekunden. Der Kritiker kostete etwa einen Cent zusätzlich. Ein PASS sagt aber wenig über den Kritiker, wenn der Agent ohnehin sauber gearbeitet hat. Für den zweiten Fall haben wir deshalb von Hand einen Stand gebaut, der grün ist und trotzdem unsauber: Der Test für Rabatte über 100 Prozent ist mit @pytest.mark.skip abgeschaltet, brutto enthält einen Debug-Print, und summe fängt jeden Fehler mit except Exception: return 0 ab. pytest meldet 5 passed, 1 skipped, ruff meldet nichts, der Zieltest endet mit Exit-Code 0. Der Kritiker urteilte so (gekürzt):
Kritiker fertig: 0.0200909 USD
1. tests/test_rechnung.py (rule 1): test_rabatt_ueber_100 is skipped with
@pytest.mark.skip(reason="flaky"). Fix: remove the skip decorator and
make rabatt raise ValueError when prozent > 100.
2. rechnung.py (rule 2): a debug print was added in brutto. Fix: delete that line.
3. rechnung.py (rule 3): summe wraps its body in try/except Exception: return 0,
which silently swallows all errors. Fix: remove the try/except and handle
the empty list explicitly.
Alle drei Mängel stehen mit Datei, Regel und Korrektur da. Mit genau diesem Text als Feedback lief ein weiterer Versuch: 7 Runden, 0,014 USD. Danach war der Skip weg und der Print gelöscht. rabatt warf den ValueError, und summe begann bei 0. Der Zieltest war grün, und der Kritiker antwortete mit PASS.
Der Kritiker braucht den Auftrag
Die erste Fassung unseres Skripts gab dem Kritiker nur den Diff und die Rubrik. Auf dem ursprünglichen Test-Repo lief sie alle fünf Versuche durch und endete mit STOP. Der Kritiker kannte den Auftrag nicht und schrieb das auch hin: „I had no task description“. Also hielt er die nötigen Korrekturen für Umbauten außerhalb der Aufgabe. Eine zweite Fassung nannte den Auftrag. Weil der aber nur von Tests sprach und der Kritiker die Tests nie sah, ging es weiter wie zuvor:
- Versuch 2: Zieltest grünDer Agent hat import os entfernt und rabatt um einen ValueError ergänzt.
Der Kritiker verlangte Rückbauten, die der Zieltest gerade erzwungen hatte. Der Agent folgte ihm, der Zieltest wurde rot, und der Loop pendelte zwischen beiden Richtern, bis die Versuche verbraucht waren. Beide Läufe zusammen kosteten rund 19 Cent. Die Korrektur steht jetzt im Skript: Der Auftrag nennt pytest und ruff, und der Kritiker bekommt die rote Ausgabe von vor dem ersten Versuch. Mit diesem Kontext urteilte er im nächsten Lauf PASS.
Wie die Teile zusammenspielen
Billige Richter zuerst. Das Skript fragt den Kritiker erst, wenn der Zieltest grün ist. Code, der nicht läuft, braucht keine Begutachtung. Der deterministische Kritiker siebt also für den teuren LLM-Kritiker vor. BASE_REF ist vor dem ersten Versuch festgelegt. Damit sieht der Kritiker immer die gesamte Änderung seit dem Start, über alle Versuche hinweg.
Das Urteil wird zum Auftrag. Was der Kritiker schreibt, ist das feedback des nächsten Versuchs, mit dem Satz „Fix exactly these points, nothing else“. Mängel mit Fundstelle und Korrektur ergeben direkt eine begrenzte Aufgabe. Darum verbietet die Rubrik jeden Kommentar zum Stil. Ein Kritiker, der eine elegantere Lösung vorschlägt, schickt den nächsten Versuch in einen Umbau, und fünf Versuche verpuffen in Umbauten ohne Ziel.
Nur ein exaktes PASS beendet den Loop. Das Skript vergleicht das Urteil Zeichen für Zeichen mit PASS. Eine Antwort wie „PASS, aber …“, eine leere Ausgabe oder ein abgebrochener Kritiker zählen als Ablehnung. Im Zweifel entscheidet der Loop also gegen die Abnahme. In unseren Läufen antwortete der Kritiker immer formgerecht.
Der Kritiker bleibt ein Judge. Er teilt die Schwächen jedes Judge · engl. LLM as a judgeEin Sprachmodell, das die Ausgabe eines anderen Modells nach einer festen Frage bewertet, etwa ob eine Antwort auf der gefundenen Notiz beruht. Ein Judge ist ein Messgerät und muss gegen eigene Labels geprüft werden.Im Glossar: Milde und eine Vorliebe für Arbeit, die seiner eigenen ähnelt. Das andere Modell und die frische Session dämpfen das. Regel 1 sichert hier allein der Kritiker. Build: Der Nachtlauf ergänzt eine Prüfung im Skript, die jeden Versuch mit geänderten alten Tests verwirft, und lässt den Loop unbeaufsichtigt laufen. Mehr zu den Schwächen von Judges steht im Handbuch Eval Engineering.
Welche Regel greift?
Ordne jeden Fund im Diff der Regel zu, gegen die er verstößt.
Noch offen (6)
- @pytest.mark.skip über einem roten Test
- assert summe([]) == 0 wird zu assert summe([]) is not None
- print("DEBUG", netto) in brutto
- Eine auskommentierte alte Fassung von rabatt bleibt stehen
- except Exception: return 0 um den Rumpf von summe
- Der Agent benennt nebenbei alle Funktionen in rechnung.py um
1: Tests abgeschwächt (0)
2: Debug, toter Code (0)
3: Fehler verschluckt (0)
4: Außerhalb der Aufgabe (0)
Merksätze
- Der Kritiker läuft in einer eigenen Session ohne Werkzeuge und sieht nur Auftrag, Ausgangslage, Diff und Rubrik.
- Erst der Zieltest, dann der Kritiker: Der billige Richter siebt für den teuren vor.
- Das Urteil des Kritikers wird wörtlich zum Auftrag des nächsten Versuchs. Deshalb muss es konkret sein und darf nichts zum Stil sagen.
- Kennt der Kritiker Auftrag und Ausgangslage nicht, kann er dem Zieltest widersprechen, und der Loop pendelt bis zum Stopp.
- Nur ein exaktes PASS beendet den Loop. Jede andere Antwort zählt als Ablehnung.
Prüf dich selbst
Denkfrage
Warum läuft der Kritiker in einer eigenen Session ohne das Protokoll des Agenten?
Im Protokoll stehen die Begründungen des Agenten für jede Entscheidung. Ein Kritiker, der sie liest, übernimmt sie leicht. In einer frischen Session sieht er nur das Ergebnis und die Regeln, an denen es sich messen lassen muss.
Denkfrage
Dein Kritiker-Loop stoppt nach fünf Versuchen, obwohl der Zieltest zwischendurch grün war. Wo suchst du?
In den letzten Urteilen und der Ausgabe des Zieltests dazwischen. Verlangt der Kritiker etwas, das der Zieltest danach rot macht, widersprechen sich die beiden Richter. Meist fehlt dem Kritiker dann Kontext: der genaue Auftrag oder die Ausgangslage.
Prüf dich selbst
Selbst nachweisen
0 von 3 erledigt
Quellen
- Run Claude Code programmatically
claude -p, Eingabe über stdin, --output-format json mit result und total_cost_usd.
- CLI reference
--tools, --model, --max-budget-usd, --setting-sources.