Skip to content
Guides10 min read

So sammelt man Process-Reward-Daten für das Training besserer Coding-Agenten

Schritt-für-Schritt-Anleitung zum Sammeln von Reward-Signalen pro Schritt für das PRM-Training mit Potato. Behandelt den First-Error-Modus, die Annotation pro Schritt und den Export in Trainingspipelines.

Potato Team

Was sind Process Reward Models?

Zwei Wege, Process Rewards zu labeln: Der First-Error-Modus markiert einen Bruchpunkt, der Modus pro Schritt bewertet jeden SchrittZwei Wege, Process Rewards zu labeln

Outcome Reward Models (ORMs) schauen nur auf das Ende der Trajektorie eines Coding-Agenten: Ließ sich der Code kompilieren, liefen die Tests durch, wurde das Issue gelöst? Process Reward Models (PRMs) bewerten stattdessen jeden Zwischenschritt. Mit einem Reward-Signal an jedem Schritt können Trainingsverfahren genau bestimmen, wo ein Agent falsch abgebogen ist, was das Lernen in der Regel sample-effizienter macht und der Generalisierung hilft.

Aktuelle Arbeiten treiben das voran. AgentPRM definiert Process Rewards für Agentenaufgaben neu: Jede Aktion wird danach bewertet, wie viel Fortschritt sie in Richtung Ziel bringt, und nicht danach, ob sie korrekt ist. Berichtet wird eine mehr als 8× bessere Compute-Effizienz gegenüber den verglichenen Baselines. ToolRM stellte fest, dass Reward-Modelle, die auf natürlichsprachlichen Ausgaben trainiert wurden, Tool-Aufrufe schlecht beurteilen, und baute tool-spezifische Reward-Modelle sowie FC-RewardBench zu deren Bewertung. Im Kontrast dazu trainiert DeepSWE einen Coding-Agenten allein mit einem spärlichen Outcome Reward, nämlich ob die Tests durchlaufen, und erreicht 42,2 % Pass@1 sowie 59 % mit Test-Time-Scaling auf SWE-bench Verified. Genau dieses reine Outcome-Setup will Process Supervision verbessern.

Was all das braucht, ist gute menschliche Annotation auf Schrittebene, und genau da liegt meist der Engpass. Die Process-Reward-Schemata von Potato sind darauf ausgelegt, das Sammeln dieser Daten zu beschleunigen. Zum zugrunde liegenden Schema siehe die Dokumentation zur Trajektorien-Evaluation, zu den Details der Trace-Eingabe die Dokumentation zu Agent-Traces.

Zwei Annotationsmodi

Potato bietet zwei PRM-Annotationsmodi, die Geschwindigkeit gegen Detailtiefe tauschen. Man wählt den, der zum Datenbudget und zum Ziel passt.

First-Error-Modus

Im First-Error-Modus liest der Annotator die Trajektorie von oben nach unten und klickt auf den ersten Schritt, in dem der Agent einen Fehler macht. Potato markiert dann jeden früheren Schritt als korrekt und jeden Schritt ab dem angeklickten als inkorrekt.

Das geht schnell, weil der Annotator nur einen einzigen Entscheidungspunkt finden muss. Es funktioniert gut, wenn Fehler kaskadieren, wenn also ein Agent nach einem Fehltritt selten wieder auf Kurs kommt, was in der Praxis der Normalfall ist.

yaml
annotation_schemes:
  - annotation_type: process_reward
    name: prm_first_error
    mode: "first_error"
    description: >
      Review the agent's steps from top to bottom. Click on the
      first step where the agent makes a mistake. All steps before
      your selection will be marked correct; all steps after
      (including the selected step) will be marked incorrect.

Der Ablauf der First-Error-Annotation sieht so aus:

  1. Der Annotator öffnet einen Trace und sieht alle Schritte, dargestellt über die Komponente CodingTraceDisplay.
  2. Er liest die Schritte der Reihe nach durch und prüft dabei Diffs, Terminalausgaben und Reasoning.
  3. Sobald er den ersten inkorrekten Schritt findet, klickt er auf die Fehlermarkierung daneben.
  4. Die Schritte 0 bis N-1 werden grün (korrekt), die Schritte N bis zum Ende werden rot (inkorrekt).
  5. Der Annotator prüft die automatische Labelvergabe und bestätigt mit „Submit".

Ist der gesamte Trace korrekt (der Agent hat die Aufgabe fehlerfrei gelöst), klickt der Annotator auf „All Correct". Ist schon der allererste Schritt falsch, klickt er auf Schritt 0 oder verwendet „All Incorrect".

So sieht die PRM-Annotationsoberfläche im Einsatz aus:

Process-Reward-Annotation mit Bewertung pro SchrittIm First-Error-Modus klickt man den ersten inkorrekten Schritt an, alle folgenden Schritte werden automatisch markiert

Modus pro Schritt

Im Modus pro Schritt bekommt jeder Schritt ein eigenes Label. Das liefert reichhaltigere Daten, weil es Fälle erfasst, in denen sich ein Agent teilweise von einem Fehler erholt, einen harmlosen, aber überflüssigen Umweg nimmt oder einen Schritt macht, der für sich genommen in Ordnung, im Kontext aber falsch ist.

yaml
annotation_schemes:
  - annotation_type: process_reward
    name: prm_per_step
    mode: "per_step"

Ein PRM-Annotationsprojekt aufsetzen

Schritt 1: Die Trace-Daten vorbereiten

Die Eingabedaten sollten eine JSONL-Datei sein, in der jede Zeile ein JSON-Objekt mit der Trajektorie des Agenten ist. Die zentralen Felder sind id, issue_description und structured_turns:

json
{
  "id": "trace_001",
  "issue_description": "Fix the TypeError in parse_config() when config file is empty",
  "repo": "myorg/myproject",
  "base_commit": "a1b2c3d",
  "structured_turns": [
    {
      "step_idx": 0,
      "type": "thinking",
      "content": "I need to understand what parse_config does and where the TypeError occurs. Let me read the file first."
    },
    {
      "step_idx": 1,
      "type": "file_read",
      "path": "src/config.py",
      "content": "def parse_config(path):\n    with open(path) as f:\n        data = json.load(f)\n    return data['settings']",
      "start_line": 1,
      "end_line": 4
    },
    {
      "step_idx": 2,
      "type": "thinking",
      "content": "The bug is on line 4. When the file is empty, json.load returns None, and None['settings'] raises TypeError. I should add a check."
    },
    {
      "step_idx": 3,
      "type": "file_edit",
      "path": "src/config.py",
      "diff": "--- a/src/config.py\n+++ b/src/config.py\n@@ -1,4 +1,6 @@\n def parse_config(path):\n     with open(path) as f:\n         data = json.load(f)\n+    if data is None:\n+        return {}\n     return data['settings']"
    },
    {
      "step_idx": 4,
      "type": "bash_command",
      "command": "python -m pytest tests/test_config.py -v",
      "output": "tests/test_config.py::test_parse_config_empty PASSED\ntests/test_config.py::test_parse_config_valid PASSED\n\n2 passed in 0.12s",
      "exit_code": 0
    }
  ]
}

Wer aus einem bestehenden Agentenformat konvertiert, nutzt das Trace-Converter-Werkzeug:

bash
# Convert Claude Code traces
potato convert-traces \
  --format claude_code \
  --input ./raw_traces/ \
  --output ./data/traces.jsonl
 
# Convert SWE-Agent trajectories
potato convert-traces \
  --format swe_agent \
  --input ./swe_agent_output/ \
  --output ./data/traces.jsonl

Potato stellt Traces von Coding-Agenten mit passender Diff-Hervorhebung dar:

Trace eines Coding-Agenten mit Diff-DarstellungCode-Diffs, Terminalausgabe und Dateilesevorgänge werden mit Syntaxhervorhebung dargestellt

Schritt 2: Die Konfiguration anlegen

Hier eine vollständige Projektkonfiguration für die PRM-Annotation im First-Error-Modus:

yaml
# config.yaml
project_name: "PRM Data Collection - SWE-bench Traces"
port: 8000
 
data:
  source: "local"
  input_path: "./data/traces.jsonl"
  data_format: "coding_trace"
 
coding_agent:
  display:
    diff_style: "unified"
    context_lines: 3
    syntax_highlighting: true
    terminal_theme: "dark"
    file_tree:
      enabled: true
      position: "left"
    collapsible:
      auto_collapse_thinking: true
      auto_collapse_long_output: true
      long_output_threshold: 50
 
annotation_schemes:
  - annotation_type: process_reward
    name: step_reward
    mode: "first_error"
    description: >
      Review the agent's trajectory step by step. Click the first
      step where the agent makes an error. If the entire trajectory
      is correct, click "All Correct."
 
  - annotation_type: radio
    name: outcome
    labels:
      - value: "resolved"
        text: "Fully Resolved"
      - value: "partial"
        text: "Partially Resolved"
      - value: "not_resolved"
        text: "Not Resolved"
 
  - annotation_type: text
    name: error_description
    description: "If incorrect, briefly describe the error"
    placeholder: "e.g., Agent edited the wrong file..."
 
output:
  path: "./output/"
  format: "jsonl"
 
quality_control:
  inter_annotator_agreement: true
  overlap_percentage: 15
  minimum_time_per_instance: 20
 
annotators:
  - username: "reviewer1"
  - username: "reviewer2"
  - username: "reviewer3"

Schritt 3: Den Annotationsserver starten

bash
# Start the annotation server
potato start config.yaml -p 8000
 
# Or run in the background
nohup potato start config.yaml -p 8000 > potato.log 2>&1 &

Dann http://localhost:8000 aufrufen, mit einem der konfigurierten Annotatoren-Konten anmelden und mit der Durchsicht der Traces beginnen.

Schritt 4: Den Fortschritt verfolgen

Während die Annotation läuft, Fortschritt und Übereinstimmung im Blick behalten:

bash
# Check annotation progress
potato status config.yaml
 
# View inter-annotator agreement
potato agreement config.yaml --metric krippendorff_alpha

Export in Trainingsformate

Ist die Annotation abgeschlossen, exportiert man die Daten in dem Format, das die eigene Trainingspipeline erwartet.

PRM-Format für das Training von Reward-Modellen

Der PRM-Export erzeugt ein JSON-Objekt pro Trace mit Labels auf Schrittebene:

bash
potato export \
  --format prm \
  --project ./output/ \
  --output ./training_data/prm_labels.jsonl

Die Ausgabe sieht so aus:

json
{
  "trace_id": "trace_001",
  "issue_description": "Fix the TypeError in parse_config() when config file is empty",
  "total_steps": 5,
  "first_error_step": null,
  "all_correct": true,
  "steps": [
    {"step_idx": 0, "type": "thinking", "label": "correct", "reward": 1.0},
    {"step_idx": 1, "type": "file_read", "label": "correct", "reward": 1.0},
    {"step_idx": 2, "type": "thinking", "label": "correct", "reward": 1.0},
    {"step_idx": 3, "type": "file_edit", "label": "correct", "reward": 1.0},
    {"step_idx": 4, "type": "bash_command", "label": "correct", "reward": 1.0}
  ]
}

Präferenzpaare für DPO/RLHF

Liegen mehrere Traces zum selben Issue vor (etwa von verschiedenen Agenten oder aus verschiedenen Durchläufen), kann Potato daraus Präferenzpaare auf Basis der PRM-Labels erzeugen:

bash
potato export \
  --format preference_pairs \
  --project ./output/ \
  --output ./training_data/preferences.jsonl \
  --pair_by "issue_id"

Der Export der Präferenzpaare vergleicht Traces, die dieselbe Aufgabe angegangen sind, und wählt anhand der Labels auf Schrittebene den besseren aus:

json
{
  "prompt": "Fix the TypeError in parse_config() when config file is empty",
  "chosen_trace_id": "trace_001",
  "rejected_trace_id": "trace_002",
  "chosen_first_error": null,
  "rejected_first_error": 3,
  "chosen_steps": 5,
  "rejected_steps": 7,
  "margin": 0.8
}

SWE-bench-kompatible Ergebnisse

Export im SWE-bench-Format fürs Benchmarking:

bash
potato export \
  --format swe_bench \
  --project ./output/ \
  --output ./training_data/swe_bench_results.json

Auswertungsbeispiele

Sind die Annotationen gesammelt, lassen sich mit diesen Python-Schnipseln die Daten auswerten und Muster erkennen.

Genauigkeit auf Schrittebene nach Schritttyp

python
import json
from collections import defaultdict
 
# Load PRM annotations
with open("training_data/prm_labels.jsonl") as f:
    traces = [json.loads(line) for line in f]
 
# Compute accuracy by step type
type_stats = defaultdict(lambda: {"correct": 0, "total": 0})
 
for trace in traces:
    for step in trace["steps"]:
        step_type = step["type"]
        type_stats[step_type]["total"] += 1
        if step["label"] == "correct":
            type_stats[step_type]["correct"] += 1
 
print("Step-Level Accuracy by Type:")
print("-" * 45)
for step_type, stats in sorted(type_stats.items()):
    acc = stats["correct"] / stats["total"] * 100
    print(f"  {step_type:<20} {acc:5.1f}%  ({stats['correct']}/{stats['total']})")

Häufige Fehlerstellen finden

python
import json
from collections import Counter
 
with open("training_data/prm_labels.jsonl") as f:
    traces = [json.loads(line) for line in f]
 
# Analyze where errors first occur
error_positions = []
error_types_at_first_error = Counter()
 
for trace in traces:
    if trace["first_error_step"] is not None:
        pos = trace["first_error_step"]
        total = trace["total_steps"]
        # Normalize position to 0-1 range
        error_positions.append(pos / total)
        # Track what type of step caused the first error
        error_step = trace["steps"][pos]
        error_types_at_first_error[error_step["type"]] += 1
 
if error_positions:
    avg_pos = sum(error_positions) / len(error_positions)
    print(f"Average first-error position: {avg_pos:.2f} (0=start, 1=end)")
    print(f"Traces with errors: {len(error_positions)}/{len(traces)}")
    print()
    print("Most common step types at first error:")
    for step_type, count in error_types_at_first_error.most_common(5):
        print(f"  {step_type}: {count}")

Übereinstimmung zwischen Annotatoren bei PRM-Labels berechnen

python
import json
import numpy as np
from sklearn.metrics import cohen_kappa_score
 
def load_annotations(annotator_file):
    """Load annotations from a single annotator's output file."""
    with open(annotator_file) as f:
        data = {item["trace_id"]: item for item in
                (json.loads(line) for line in f)}
    return data
 
ann1 = load_annotations("output/reviewer1/annotations.jsonl")
ann2 = load_annotations("output/reviewer2/annotations.jsonl")
 
# Find overlapping traces
overlap_ids = set(ann1.keys()) & set(ann2.keys())
print(f"Overlapping traces: {len(overlap_ids)}")
 
# Compare first-error step labels
labels1 = []
labels2 = []
for trace_id in overlap_ids:
    fe1 = ann1[trace_id].get("first_error_step", -1)
    fe2 = ann2[trace_id].get("first_error_step", -1)
    # Bin into: all_correct, early_error (first half), late_error (second half)
    total = ann1[trace_id]["total_steps"]
    for fe, labels in [(fe1, labels1), (fe2, labels2)]:
        if fe is None or fe == -1:
            labels.append("all_correct")
        elif fe < total / 2:
            labels.append("early_error")
        else:
            labels.append("late_error")
 
kappa = cohen_kappa_score(labels1, labels2)
print(f"Cohen's kappa (binned first-error): {kappa:.3f}")

Tipps für effizientes Sammeln von PRM-Daten

Für Tempo den First-Error-Modus nehmen. Wer ein PRM trainiert, das eine Suche steuern soll (MCTS, Best-of-N-Sampling), bekommt im First-Error-Modus genug Signal bei zwei- bis dreifacher Annotationsgeschwindigkeit gegenüber dem Modus pro Schritt. Die meisten Agenten scheitern ohnehin in einer Kaskade: Ein Fehler zieht eine Kette schlechter Schritte nach sich.

Den Modus pro Schritt nehmen, wenn es auf die Details ankommt. Wenn teilweise Erholung, harmlose Umwege oder ein Reward-Modell auf Schrittebene mit mehr als zwei Labels im Spiel sind, ist der Modus pro Schritt seine zusätzliche Zeit wert.

PRM mit paarweisem Vergleich kombinieren. Traces einzeln per PRM labeln, dann einen paarweisen Vergleich über die Traces laufen lassen, die dasselbe Issue angegangen sind. Ein Annotationsdurchgang liefert damit Rewards auf Schrittebene und Präferenzpaare zugleich.

Mit erfahrenen Annotatoren anfangen. PRM-Annotation heißt Code, Diffs und Terminalausgaben lesen. Also mit einer kleinen Gruppe erfahrener Entwickler starten, die Übereinstimmung messen, an Beispielen kalibrieren und dann hochskalieren.

Eine Mindestzeit pro Instanz setzen. Traces werden schnell unübersichtlich. Eine Untergrenze von 30 Sekunden hält Annotatoren davon ab, durchzurauschen, ohne die Änderungen wirklich gelesen zu haben. Der Wert sollte zur durchschnittlichen Trace-Länge passen.

Kalibrierungsbeispiele bereitstellen. Vor dem Produktivbetrieb alle dieselben 10 bis 20 Traces labeln lassen und durchsprechen, wo es Unstimmigkeiten gab. Für die Konsistenz macht das viel aus.