Home Self Learning Agents Artikel 3
Teil 3 Generative AI

Automatisierte Tests als Reward-Signal

Bild: KI-generiert mit Beuys


Automatisierte Tests als Reward-Signal

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:

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"

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:

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

→ Vollständiger Code: src/

📄 Als PDF