Home Self Learning Agents Artikel 2
Teil 2 Generative AI

Reinforcement Learning für Code-Agenten

Bild: KI-generiert mit Beuys


Reinforcement Learning für Code-Agenten

Das formale Framework hinter selbstlernenden Agenten: Wie Reinforcement Learning auf Code-Generierung abgebildet wird – von Markov-Entscheidungsprozessen über Q-Learning bis zur Epsilon-Greedy-Strategie.


1. Vom Feedback-Loop zum formalen Framework

Der vorherige Artikel beschreibt drei Säulen selbstlernender Agenten: Feedback-Loops, persistentes Gedächtnis und Selbstkritik. Der Feedback-Loop – Agent generiert, Umgebung bewertet, Agent passt an – folgt einem Muster, das in der KI-Forschung seit Jahrzehnten formalisiert ist: Reinforcement Learning (RL).

"The idea of RL is that an agent can learn from the environment by interacting with it and receiving rewards for performing actions." — Raschka et al., Machine Learning with PyTorch and Scikit-Learn, S. 1025

RL liefert die mathematische Struktur, um Feedback-Loops präzise zu beschreiben: Welche Zustände existieren? Welche Aktionen stehen zur Verfügung? Wie wird Erfolg gemessen? Wann lohnt es sich, Neues auszuprobieren, und wann die bewährte Strategie zu verfolgen?

2. Markov-Entscheidungsprozess: Das Modell

Ein Markov-Entscheidungsprozess (MDP) formalisiert die Interaktion zwischen Agent und Umgebung durch vier Komponenten:

Komponente Symbol Beschreibung
Zustände S Menge aller möglichen Situationen
Aktionen A Menge aller verfügbaren Handlungen
Übergangsfunktion T(s, a, s') Wahrscheinlichkeit, von s nach s' zu gelangen
Reward-Funktion R(s, a) Belohnung für Aktion a in Zustand s

Mapping auf Code-Generierung

Die Übersetzung auf einen Code-Agenten ergibt ein konkretes MDP:

RL-Konzept Code-Agent
State Aufgabentyp + Komplexität + bisherige Versuche + Test-Pass-Rate
Action Strategie-Wahl: generieren, Fehler beheben, refaktorieren, Tests ergänzen
Transition Aufgabe → Code-Generierung → Test-Ausführung → neuer Zustand
Reward Gewichtetes Signal aus Testergebnissen, Code-Qualität, Effizienz
@dataclass(frozen=True)
class CodeState:
    task_type: str        # "bugfix", "feature", "refactor"
    complexity: int       # 1–5
    attempts: int         # Bisherige Versuche
    test_pass_rate: float # 0.0–1.0

@dataclass(frozen=True)
class CodeAction:
    strategy: str         # "generate", "fix_errors", "refactor", "add_tests"
    context_level: int    # 1–3: minimal, normal, maximal

Die Markov-Eigenschaft besagt: Der nächste Zustand hängt nur vom aktuellen Zustand und der gewählten Aktion ab – nicht von der gesamten Historie. Für Code-Agenten eine vereinfachende Annahme, die durch das persistente Gedächtnis (Artikel 01) kompensiert wird.

3. Reward Shaping: Differenziertes Feedback

❌ Binäres Feedback: Pass oder Fail

Ein naives Reward-Signal – 1.0 für bestandene Tests, 0.0 für Fehlschlag – liefert zu wenig Information. Der Agent erfährt nicht, wie nah er an einer Lösung war.

✅ Gewichtetes Reward-Signal

Reward Shaping zerlegt die Bewertung in multiple Metriken mit unterschiedlicher Gewichtung:

def compute_reward(
    test_pass_rate: float,
    quality_score: float,
    efficiency_score: float,
    weights: dict[str, float] | None = None,
) -> CodeReward:
    w = weights or {"test": 0.6, "quality": 0.25, "efficiency": 0.15}
    total = (
        w["test"] * test_pass_rate
        + w["quality"] * quality_score
        + w["efficiency"] * efficiency_score
    )
    return CodeReward(
        test_score=test_pass_rate,
        quality_score=quality_score,
        efficiency_score=efficiency_score,
        total=round(total, 4),
    )

Die Gewichtung spiegelt eine klare Priorität: Tests dominieren (60 %), Code-Qualität folgt (25 %), Effizienz ergänzt (15 %). Diese Gewichte sind konfigurierbar – ein Team kann die Balance an eigene Standards anpassen.

Fortschritts-Bonus

Zusätzlich zum absoluten Score belohnt ein Shaping-Term den Fortschritt gegenüber dem vorherigen Zustand:

def shaped_reward(
    current_test_rate: float,
    previous_test_rate: float,
    base_reward: float,
    shaping_factor: float = 0.5,
) -> float:
    progress = current_test_rate - previous_test_rate
    return round(base_reward + shaping_factor * progress, 4)

Ein Agent, der die Test-Pass-Rate von 0.3 auf 0.8 steigert, erhält einen höheren Reward als einer, der konstant bei 0.8 bleibt – auch wenn der absolute Score identisch ist. Das beschleunigt das Lernen in frühen Phasen.

4. Q-Learning: Werte für State-Action-Paare

Die Q-Funktion

Die zentrale Frage im RL lautet: Welche Aktion ist in einem gegebenen Zustand optimal? Die Q-Funktion Q(s, a) beantwortet diese Frage – sie schätzt den erwarteten kumulierten Reward für Aktion a in Zustand s.

"The action value function determines the long-term rewards the agent will receive for taking action a. This function is usually expressed as Q(S, a), and hence is also called the Q-function." — Ashish Ranjan Jha, Mastering PyTorch, S. 393

Die Bellman-Gleichung

Q-Werte werden nicht direkt berechnet, sondern iterativ über die Bellman-Gleichung aktualisiert:

Q(s, a) ← Q(s, a) + α · [r + γ · max_a' Q(s', a') − Q(s, a)]
Parameter Bedeutung Typischer Wert
α (Alpha) Lernrate: Wie stark neue Erfahrungen gewichtet werden 0.1
γ (Gamma) Diskontierungsfaktor: Wie wichtig zukünftige Rewards sind 0.9
r Erhaltener Reward variabel
max Q(s', a') Bester geschätzter Wert im Folgezustand berechnet

"The Bellman equation is one of the central elements of many RL algorithms. The Bellman equation simplifies the computation of the value function, such that rather than summing over multiple time steps, it uses a recursion." — Raschka et al., Machine Learning with PyTorch and Scikit-Learn, S. 1025

Implementierung: Q-Table

Für diskrete State-Action-Räume speichert eine Q-Table die Werte als Dictionary:

@dataclass
class QTable:
    values: dict[tuple[str, str], float] = field(default_factory=dict)
    default_value: float = 0.0

    def update(
        self, state_key, action_key, reward,
        next_state_key, actions, alpha=0.1, gamma=0.9,
    ) -> float:
        current_q = self.get(state_key, action_key)
        max_next_q = max(
            (self.get(next_state_key, a) for a in actions),
            default=self.default_value,
        )
        new_q = current_q + alpha * (reward + gamma * max_next_q - current_q)
        self.values[(state_key, action_key)] = round(new_q, 6)
        return new_q

    def best_action(self, state_key, actions) -> str:
        return max(actions, key=lambda a: self.get(state_key, a))

Nach genügend Episoden konvergieren die Q-Werte: Der Agent „weiß", welche Strategie in welcher Situation den höchsten erwarteten Reward liefert.

5. Exploration vs. Exploitation

Das Dilemma

Ein Agent, der immer die Aktion mit dem höchsten bekannten Q-Wert wählt (Exploitation), verpasst möglicherweise bessere Strategien. Ein Agent, der nur zufällig handelt (Exploration), nutzt sein Wissen nicht.

Epsilon-Greedy-Strategie

Die Epsilon-Greedy-Strategie löst das Dilemma mit einem einfachen Mechanismus:

def epsilon_greedy(
    q_table: QTable,
    state_key: str,
    actions: list[str],
    epsilon: float,
    rng: random.Random | None = None,
) -> tuple[str, bool]:
    r = rng.random() if rng else random.random()
    if r < epsilon:
        chosen = rng.choice(actions) if rng else random.choice(actions)
        return chosen, True
    return q_table.best_action(state_key, actions), False

"Under this mechanism, at each episode, an epsilon value is pre-decided, which is a scalar value between 0 and 1. [...] If the generated number is less than the pre-defined epsilon value, the agent takes a random action (exploration), otherwise it takes the action with the highest Q-value (exploitation)." — Ashish Ranjan Jha, Mastering PyTorch, S. 393

Epsilon-Decay

Zu Beginn exploriert der Agent stark (hohes ε), mit zunehmender Erfahrung verlässt er sich stärker auf gelerntes Wissen (niedriges ε). Exponentieller Decay modelliert diesen Übergang:

def decay_epsilon(
    epsilon: float,
    min_epsilon: float = 0.01,
    decay_rate: float = 0.005,
    episode: int = 0,
) -> float:
    decayed = min_epsilon + (epsilon - min_epsilon) * math.exp(-decay_rate * episode)
    return round(decayed, 6)
Episode ε (bei ε₀=1.0, min=0.01) Verhalten
0 1.00 Reine Exploration
100 0.61 Überwiegend Exploration
500 0.09 Überwiegend Exploitation
1000 0.02 Fast reine Exploitation

Für Code-Agenten bedeutet das: In den ersten Aufgaben werden verschiedene Strategien (generieren, refaktorieren, Fehler beheben) ausprobiert. Mit wachsender Erfahrung kristallisiert sich heraus, welche Strategie für welchen Aufgabentyp optimal funktioniert.

6. Der Code-Agent-Environment-Loop

Alle Komponenten zusammen ergeben den vollständigen Agent-Environment-Loop:

    ┌─────────────┐
      CodeState    ◄───────────────────────────┐
    └──────┬──────┘                                                                                    ┌─────────────┐                                    ε-Greedy     ── Aktion wählen                  └──────┬──────┘                                                                                    ┌─────────────┐                                    Environment  ── Code ausführen, Tests          └──────┬──────┘                                                                                    ┌─────────────┐                                    Reward       ── Shaping + Fortschritt          └──────┬──────┘                                                                                    ┌─────────────┐                                    Q-Update     ── Bellman-Gleichung               └──────┬──────┘                                                                                    ┌─────────────┐                                    Next State   ─────────────────────────────►┘
    └─────────────┘

Die run_episode-Funktion orchestriert einen kompletten Durchlauf:

def run_episode(
    initial_state, actions, q_table, environment,
    state_key_fn, epsilon=0.1, alpha=0.1, gamma=0.9,
    max_steps=10, rng=None,
) -> tuple[list[Transition], float]:
    transitions = []
    cumulative_reward = 0.0
    state = initial_state

    for step in range(max_steps):
        state_key = state_key_fn(state)
        action_key, _ = epsilon_greedy(q_table, state_key, actions, epsilon, rng)
        env_result = environment(state, action_key)

        reward_signal = compute_reward(
            env_result.test_pass_rate,
            env_result.quality_score,
            env_result.efficiency_score,
        )
        reward = shaped_reward(
            env_result.test_pass_rate,
            state.test_pass_rate,
            reward_signal.total,
        )

        next_state = CodeState(
            task_type=state.task_type,
            complexity=state.complexity,
            attempts=state.attempts + 1,
            test_pass_rate=env_result.test_pass_rate,
        )

        q_table.update(state_key, action_key, reward,
                       state_key_fn(next_state), actions, alpha, gamma)
        transitions.append(Transition(state, CodeAction(action_key, 1), reward, next_state))
        cumulative_reward += reward

        if env_result.is_terminal:
            break
        state = next_state

    return transitions, round(cumulative_reward, 4)

Jede Episode liefert eine Liste von Transitions – die vollständige Aufzeichnung, welcher Zustand zu welcher Aktion führte und welches Feedback die Umgebung gab. Diese Transitions bilden die Grundlage für das Experience Replay (Artikel 04).

7. Von RL zu RLHF: Die Brücke zu LLMs

Reinforcement Learning from Human Feedback (RLHF) überträgt RL-Konzepte direkt auf die Optimierung von Sprachmodellen:

"RLHF combines reinforcement learning with human feedback to fine-tune language models. It aims to align the model's outputs with human preferences, improving the quality and safety of generated text." — Vaid & Lund, LLM Design Patterns, S. 452

RL-Konzept RLHF-Anwendung Code-Agent-Analogie
Policy Sprachmodell (Token-Generierung) Code-Generierungsstrategie
Reward Model Trainiert auf menschliche Präferenzen Test-Framework + Linting + Metriken
Environment Textgenerierungs-Kontext Codebase + Test-Suite
Action Space Alle möglichen Tokens Strategien + Kontext-Level

Der entscheidende Unterschied: Bei klassischem RLHF werden die Modellgewichte angepasst. Bei Code-Agenten bleibt das LLM unverändert – die „Policy" wird durch Prompt-Anpassung, Strategie-Selektion und Kontextanreicherung optimiert.

8. Fazit

Reinforcement Learning liefert das formale Gerüst für selbstlernende Code-Agenten:

Das LLM selbst wird nicht trainiert. Die „Lernfähigkeit" entsteht durch die RL-Architektur um das Modell herum: Q-Tabelle als Wissensspeicher, Epsilon-Decay als Reifeprozess, Reward Shaping als differenziertes Feedback-Signal.

Der nächste Artikel der Serie behandelt Automatisierte Tests als Reward-Signal – wie Test-Frameworks die „Umgebung" für Code-Agenten bilden und wie Reward Shaping in der Praxis gestaltet wird.


Quellen & Ressourcen

→ Vollständiger Code: src/

📄 Als PDF