Wie Test-Frameworks zur „Umgebung" für lernende Code-Agenten werden. Vom binären Pass/Fail zum differenzierten Multi-Signal-Reward – und warum die Feedback-Pipeline Agent → Code → Test → Score → Agent den Kern jedes selbstlernenden Systems bildet.
1. Tests als Umgebung
Im vorherigen Artikel haben wir das RL-Framework für Code-Agenten aufgebaut: State, Action, Reward, Policy. Doch eine zentrale Frage blieb offen: Woher kommt das Reward-Signal in der Praxis?
Die Antwort ist überraschend naheliegend: Automatisierte Tests.
„Tests are the first line of feedback and defense against coding mistakes, refactoring mistakes, and so on." — Fabrizio Romano, Learn Python Programming, 4. Aufl., S. 472
In der Welt der Code-Agenten übernehmen Test-Suites die Rolle, die in klassischem RL die physische Umgebung spielt. Der Agent generiert Code (Aktion), die Tests evaluieren das Ergebnis (Umgebung), und das Testergebnis wird zum Reward-Signal:
Agent → Code generieren → Tests ausführen → Ergebnis → Reward berechnen → Agent
Das ist kein abstraktes Konzept – es ist exakt die Schleife, die TDD (Test-Driven Development) seit Jahrzehnten praktiziert:
„TDD is a software development methodology that is based on the continuous repetition of a very short development cycle." — Fabrizio Romano, Learn Python Programming, 4. Aufl., S. 472
Der Unterschied: Bei TDD sitzt ein Mensch in der Schleife. Bei selbstlernenden Agenten wird die Schleife automatisiert – und das Testergebnis wird zum formalen Reward-Signal.
2. Das Problem mit binärem Feedback
Der einfachste Ansatz: Alle Tests bestehen → Reward 1.0. Mindestens ein Test schlägt fehl → Reward 0.0.
def binary_reward(suite: SuiteResult) -> float:
return 1.0 if suite.all_passed else 0.0
Das funktioniert – ist aber ein spärliches (sparse) Reward-Signal. Warum das problematisch ist:
| Szenario | Pass-Rate | Binärer Reward |
|---|---|---|
| 0 von 10 Tests bestehen | 0% | 0.0 |
| 9 von 10 Tests bestehen | 90% | 0.0 |
| 10 von 10 Tests bestehen | 100% | 1.0 |
Ein Agent mit 9 von 10 bestandenen Tests erhält dasselbe Feedback wie einer mit 0 von 10. Aus RL-Perspektive ist das fatal: Der Agent kann keinen Gradienten in Richtung Verbesserung erkennen.
„A key innovation in RLHF is its approach to handling the high cost of human feedback. Rather than requiring constant human oversight, RLHF allows for asynchronous and sparse feedback." — Labonne, M. et al. (2025), LLM Engineer's Handbook, S. 331
Was für menschliches Feedback akzeptabel ist – Spärlichkeit – wird bei automatisierten Tests zum unnötigen Engpass. Wir haben detaillierte Informationen. Wir müssen sie nur nutzen.
3. Strukturierte Testergebnisse
Der erste Schritt zum besseren Reward: Testergebnisse nicht als Boolean, sondern als strukturierte Daten behandeln.
class TestStatus(Enum):
PASSED = "passed"
FAILED = "failed"
ERROR = "error"
SKIPPED = "skipped"
class TestSeverity(Enum):
CRITICAL = 3
MAJOR = 2
MINOR = 1
@dataclass(frozen=True)
class TestResult:
name: str
status: TestStatus
severity: TestSeverity = TestSeverity.MAJOR
message: str = ""
duration_ms: float = 0.0
Jeder einzelne Test trägt jetzt drei Dimensionen an Information: - Status: Bestanden, fehlgeschlagen, Fehler oder übersprungen - Schweregrad: Wie kritisch ist dieser Test? (Ein fehlgeschlagener Login-Test wiegt schwerer als ein fehlender Tooltip) - Kontext: Fehlermeldung und Ausführungsdauer
Die Aggregation zu einer Suite liefert eine reichhaltige Datenstruktur:
@dataclass(frozen=True)
class SuiteResult:
tests: tuple[TestResult, ...]
suite_name: str = "default"
@property
def pass_rate(self) -> float:
active = self.total - self.skipped
return self.passed / active if active > 0 else 0.0
Beachte: Übersprungene Tests werden aus der Pass-Rate ausgeschlossen. Ein Skip ist keine Aussage über Code-Qualität – er ist eine Aussage über den Testumfang.
4. Vom Einzelsignal zum Reward-Vektor
Mit strukturierten Testergebnissen können wir vier verschiedene Reward-Signale extrahieren:
4.1 Pass-Rate: Proportional statt binär
def pass_rate_reward(suite: SuiteResult) -> float:
return suite.pass_rate
Schon besser: 9/10 → 0.9, 5/10 → 0.5. Der Agent sieht jetzt einen Gradienten.
4.2 Severity-Weighted: Kritische Tests zählen mehr
def severity_weighted_reward(suite: SuiteResult) -> float:
active = [t for t in suite.tests if t.status != TestStatus.SKIPPED]
if not active:
return 0.0
total_weight = sum(t.severity.value for t in active)
passed_weight = sum(
t.severity.value for t in active
if t.status == TestStatus.PASSED
)
return passed_weight / total_weight
Wenn ein CRITICAL-Test (Gewicht 3) fehlschlägt und ein MINOR-Test (Gewicht 1) besteht, ist der gewichtete Score niedriger als bei einfacher Pass-Rate. Das reflektiert die Realität: Ein fehlender Tooltip ist weniger schlimm als ein kaputter Login.
4.3 Progression: Fortschritt belohnen
def progression_reward(
current: SuiteResult,
previous: SuiteResult | None,
) -> float:
if previous is None:
return current.pass_rate
return current.pass_rate - previous.pass_rate
Das Delta zwischen aktueller und vorheriger Iteration. Ein Agent, der von 3/10 auf 7/10 verbessert, erhält ein positives Signal (+0.4), auch wenn noch nicht alle Tests bestehen. Ein Agent, der von 8/10 auf 5/10 verschlechtert, erhält ein negatives Signal (−0.3).
4.4 Speed: Schnellere Tests als Bonus
def speed_score(suite: SuiteResult, target_ms: float = 1000.0) -> float:
if not suite.tests:
return 0.0
total_ms = sum(t.duration_ms for t in suite.tests)
return math.exp(-total_ms / target_ms)
Exponentieller Abfall relativ zu einem Zielwert. Code, der alle Tests schnell besteht, ist besser als Code, der alle Tests langsam besteht. Das verhindert Brute-Force-Lösungen und belohnt effiziente Implementierungen.
5. Multi-Signal-Reward-Shaping
Jedes einzelne Signal hat blinde Flecken. Die Pass-Rate ignoriert Schweregrade. Die Progression ignoriert den absoluten Stand. Der Speed-Score ignoriert Korrektheit. Erst die Kombination ergibt ein vollständiges Bild.
@dataclass(frozen=True)
class RewardSignal:
pass_rate: float
severity_score: float
progression: float
coverage_score: float
speed_score: float
total: float
Die Gewichtung ist konfigurierbar:
@dataclass(frozen=True)
class RewardWeights:
pass_rate: float = 0.35
severity: float = 0.25
progression: float = 0.20
coverage: float = 0.10
speed: float = 0.10
Die Standard-Gewichtung spiegelt Prioritäten wider: - Pass-Rate (35%): Die wichtigste Metrik – funktioniert der Code? - Schweregrad (25%): Kritische Fehler wiegen schwerer - Progression (20%): Verbesserung wird belohnt, auch bei unvollständiger Lösung - Coverage (10%): Bonus für breite Testabdeckung - Speed (10%): Bonus für effiziente Lösungen
„Reward hacking: Models may exploit loopholes in the reward function." — Vaid, S. & Lund, B. (2025), LLM Design Patterns, S. 452
Die Gewichtung ist bewusst nicht auf eine einzige Metrik fokussiert. Reward Hacking – das Ausnutzen von Schwachstellen in der Reward-Funktion – wird durch Multi-Signal-Scoring erschwert. Ein Agent, der nur die Pass-Rate optimiert und dabei katastrophalen Code schreibt, wird durch den Severity- und Speed-Score bestraft.
6. Die Feedback-Pipeline
Das Reward-Signal allein reicht nicht. Der Agent braucht strukturiertes Feedback, das ihm sagt, was schiefgelaufen ist und wo er ansetzen soll.
@dataclass(frozen=True)
class FeedbackEntry:
iteration: int
submission: CodeSubmission
suite_result: SuiteResult
reward: RewardSignal
failing_tests: tuple[tuple[str, str], ...]
Jede Iteration produziert einen FeedbackEntry mit:
1. Dem eingereichten Code – was hat der Agent versucht?
2. Dem vollständigen Testergebnis – welche Tests bestanden, welche nicht?
3. Dem Multi-Signal-Reward – wie gut war der Versuch insgesamt?
4. Den fehlgeschlagenen Tests mit Meldungen – konkrete Hinweise zur Korrektur
Die Funktion build_feedback orchestriert die Berechnung:
def build_feedback(
submission: CodeSubmission,
suite: SuiteResult,
previous_suite: SuiteResult | None = None,
coverage: float = 0.0,
target_ms: float = 1000.0,
weights: RewardWeights | None = None,
) -> FeedbackEntry:
reward = compute_reward_signal(
suite, previous_suite, coverage, target_ms, weights
)
failing = extract_failing_tests(suite)
return FeedbackEntry(
iteration=submission.iteration,
submission=submission,
suite_result=suite,
reward=reward,
failing_tests=failing,
)
7. Der Agent-Test-Loop
Alle Komponenten zusammen ergeben den Agent-Test-Loop – die zentrale Architektur selbstlernender Code-Agenten:
@dataclass
class AgentTestLoop:
test_runner: Callable[[str], SuiteResult]
agent_fn: Callable[[list[FeedbackEntry], str | None], str]
max_iterations: int = 10
reward_weights: RewardWeights = field(default_factory=RewardWeights)
def run(self, task: str | None = None) -> list[FeedbackEntry]:
history: list[FeedbackEntry] = []
previous_suite: SuiteResult | None = None
for i in range(1, self.max_iterations + 1):
code = self.agent_fn(history, task)
submission = CodeSubmission(
code=code, iteration=i,
strategy=_infer_strategy(history),
)
suite = self.test_runner(code)
entry = build_feedback(
submission, suite,
previous_suite=previous_suite,
weights=self.reward_weights,
)
history.append(entry)
previous_suite = suite
if suite.all_passed:
break
return history
Die Architektur ist bewusst minimal und kompositorisch:
test_runner: Beliebige Funktion, die Code entgegennimmt undSuiteResultliefert. Das kann pytest, unittest, Playwright oder ein Custom-Runner sein.agent_fn: Beliebige Funktion, die die bisherige Historie und eine Aufgabe entgegennimmt und neuen Code liefert. Das kann ein LLM-API-Call, eine Template-Engine oder ein regelbasiertes System sein.max_iterations: Sicherheitsgrenze gegen Endlosschleifen.
Der Loop stoppt bei zwei Bedingungen: 1. Alle Tests bestehen – der Agent hat die Aufgabe gelöst 2. Max-Iterationen erreicht – der Agent konvergiert nicht
Strategie-Inferenz
Ein subtiles Detail: Die Strategie wird automatisch aus der Historie abgeleitet:
def _infer_strategy(history: list[FeedbackEntry]) -> str:
if not history:
return "initial"
last = history[-1]
if last.suite_result.errors > 0:
return "fix_errors"
if last.suite_result.failed > 0:
return "fix_failures"
return "refactor"
- Keine Historie → „initial" (erster Versuch)
- Laufzeitfehler → „fix_errors" (Code crasht)
- Testfehler → „fix_failures" (Code läuft, aber falsche Ergebnisse)
- Alles besteht → „refactor" (Optimierung)
Diese Strategie-Inferenz liefert dem Agenten einen Hint, welche Art von Änderung als nächstes sinnvoll ist – ohne die Entscheidungsfreiheit einzuschränken.
8. Lernkurven und Konvergenz
Die Feedback-Historie ist nicht nur Input für den Agenten – sie ist auch ein Diagnosewerkzeug für die Qualität des Reward-Shapings.
@dataclass(frozen=True)
class LearningPoint:
iteration: int
pass_rate: float
total_reward: float
cumulative_reward: float
tests_fixed: int
Die Lernkurve zeigt: - Pass-Rate über Iterationen: Steigt sie monoton? → Gutes Reward-Signal - Kumulierter Reward: Wächst er gleichmäßig? → Stabiler Lernprozess - Tests-fixed pro Iteration: Werden in jeder Runde neue Tests gefixt? → Fortschritt statt Oszillation
def convergence_rate(history: list[FeedbackEntry]) -> float | None:
for entry in history:
if entry.suite_result.all_passed:
return float(entry.iteration)
return None
Die Konvergenzrate – in welcher Iteration alle Tests bestehen – ist die ultimative Metrik. Niedrigere Werte = besserer Agent oder besseres Reward-Shaping.
Ein gleitender Durchschnitt des Rewards glättet Ausreißer und zeigt den Gesamttrend:
def reward_trend(history: list[FeedbackEntry], window: int = 3) -> list[float]:
rewards = [e.reward.total for e in history]
smoothed = []
for i in range(len(rewards)):
start = max(0, i - window + 1)
chunk = rewards[start:i + 1]
smoothed.append(sum(chunk) / len(chunk))
return smoothed
9. Vergleich: Reward-Strategien in der Praxis
| Strategie | Stärke | Schwäche | Einsatz |
|---|---|---|---|
| Binary | Einfach, klar | Sparse, kein Gradient | Finale Validierung |
| Pass-Rate | Gradient vorhanden | Ignoriert Schweregrade | Schnelles Prototyping |
| Severity-Weighted | Priorisiert kritische Tests | Benötigt Severity-Annotation | Produktions-Code |
| Progression | Belohnt Verbesserung | Instabil bei Oszillation | Lern-Phase |
| Multi-Signal | Robustes Gesamtbild | Komplexere Konfiguration | Produktiv-Einsatz |
Die Empfehlung: Starte mit Pass-Rate, wechsle zu Multi-Signal, sobald der Agent grundlegend funktioniert. Binary-Reward nur als finale Abnahme.
10. Fazit
Automatisierte Tests sind das natürliche Reward-Signal für Code-Agenten:
- Strukturierte Testergebnisse transformieren binäres Pass/Fail in einen informativen Datentyp
- Severity-Gewichtung sorgt dafür, dass kritische Fehler schwerer wiegen
- Progression belohnt jeden Fortschritt, auch wenn die Lösung noch unvollständig ist
- Multi-Signal-Shaping kombiniert fünf Dimensionen zu einem robusten Gesamt-Reward
- Die Feedback-Pipeline liefert dem Agenten nicht nur einen Score, sondern konkretes Feedback zur Korrektur
- Lernkurven machen den Fortschritt sichtbar und diagnostizierbar
Die Kernarchitektur – AgentTestLoop mit austauschbarem Test-Runner und Agent-Funktion – ist bewusst minimal gehalten. Sie erzwingt keine bestimmte Test-Technologie und kein bestimmtes LLM. Die Intelligenz sitzt nicht im Loop, sondern im Reward-Shaping und im Feedback-Format.
Der nächste Artikel der Serie behandelt Memory & Experience Replay für Agenten – wie Agenten erfolgreiche Strategien speichern, bei ähnlichen Aufgaben abrufen und über Aufgaben hinweg lernen.
Quellen & Ressourcen
- Romano, F. (2024): Learn Python Programming. 4. Aufl. Packt. S. 472 ff. – TDD als Feedback-Loop, Test-Methodologie, pytest-Architektur.
- Vaid, S. & Lund, B. (2025): LLM Design Patterns. Packt. S. 452 ff. – Reward Hacking, RLHF-Herausforderungen, Constrained Optimization.
- Labonne, M. et al. (2025): LLM Engineer's Handbook. Packt. S. 331 ff. – RLHF, Sparse Feedback, Reward-Modelle, Policy-Optimierung.
- Ramirez, J. (2024): Generative AI with LangChain. 2. Aufl. Packt. S. 440 ff. – Evaluierung von LLM-Output, Format-Validierung, Scoring-Metriken.
- Labonne, M. et al. (2025): LLM Engineer's Handbook. Packt. S. 580 ff. – Linting als Code-Qualitätsmetrik, statische Analyse.
- Sutton, R. S. & Barto, A. G. (2018): Reinforcement Learning: An Introduction. 2. Aufl. MIT Press. – Reward Shaping, Exploration vs. Exploitation.
→ Vollständiger Code: src/