Skip to content
Guides11 min read

Comment recueillir des données de récompense de processus pour entraîner de meilleurs agents de coding

Guide pas à pas pour recueillir des signaux de récompense par étape en vue de l'entraînement d'un PRM avec Potato. Couvre le mode première erreur, l'annotation par étape et l'exportation vers les pipelines d'entraînement.

Potato Team

Qu'est-ce qu'un modèle de récompense de processus ?

Deux façons d'étiqueter les récompenses de processus : le mode première erreur marque un point de rupture, le mode par étape note chaque étapeDeux façons d'étiqueter les récompenses de processus

Les modèles de récompense de résultat (ORM) ne regardent que la fin de la trajectoire d'un agent de coding : le code a-t-il compilé, les tests sont-ils passés, le problème a-t-il été résolu ? Les modèles de récompense de processus (PRM) notent à la place chaque étape intermédiaire. Avec un signal de récompense à chaque étape, les méthodes d'entraînement peuvent situer précisément où l'agent a dérapé, ce qui rend en général l'apprentissage plus économe en exemples et aide à la généralisation.

Des travaux récents poussent dans cette direction. AgentPRM redéfinit les récompenses de processus pour les tâches d'agent : chaque action est notée selon le progrès qu'elle apporte vers l'objectif plutôt que selon sa justesse, et l'article rapporte une efficacité de calcul plus de 8× meilleure que les références auxquelles il se compare. ToolRM a constaté que les modèles de récompense entraînés sur des sorties en langue naturelle jugent mal les appels d'outils, et a construit des modèles de récompense propres aux outils ainsi que FC-RewardBench pour les évaluer. À l'inverse, DeepSWE entraîne un agent de code avec une seule récompense de résultat parcimonieuse, à savoir si les tests passent, et atteint 42,2 % en Pass@1 et 59 % avec mise à l'échelle au moment du test sur SWE-bench Verified. C'est précisément ce dispositif purement fondé sur le résultat que la supervision de processus cherche à améliorer.

Tout cela réclame une bonne annotation humaine au niveau des étapes, et c'est en général là que ça coince. Les schémas de récompense de processus de Potato sont faits pour accélérer cette collecte. Pour le schéma sous-jacent, voir la documentation sur l'évaluation de trajectoire, et pour le détail des traces en entrée, la documentation sur les traces d'agents.

Deux modes d'annotation

Potato propose deux modes d'annotation PRM, qui échangent de la vitesse contre de la finesse. Choisissez celui qui correspond à votre budget de données et à vos objectifs.

Mode première erreur

En mode première erreur, l'annotateur lit la trajectoire de haut en bas et clique sur la première étape où l'agent se trompe. Potato marque ensuite comme correcte chaque étape antérieure et comme erronée chaque étape à partir de celle qui a été cliquée.

C'est rapide, parce que l'annotateur n'a qu'un seul point de décision à trouver. Cela fonctionne bien quand les erreurs se propagent en cascade, c'est-à-dire quand un agent qui a quitté la bonne voie n'y revient que rarement, ce qui est le cas courant en pratique.

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.

Le déroulé de l'annotation en mode première erreur ressemble à ceci :

  1. L'annotateur ouvre une trace et voit toutes les étapes rendues par le composant CodingTraceDisplay.
  2. Il parcourt les étapes dans l'ordre, en examinant les diffs, les sorties du terminal et le raisonnement.
  3. Quand il trouve la première étape erronée, il clique sur le marqueur d'erreur à côté d'elle.
  4. Les étapes 0 à N-1 passent au vert (correctes), les étapes N jusqu'à la fin passent au rouge (erronées).
  5. L'annotateur vérifie l'étiquetage automatique et clique sur « Submit » pour confirmer.

Si toute la trace est correcte (l'agent a résolu la tâche parfaitement), l'annotateur clique sur « All Correct ». Si la toute première étape est déjà fausse, il clique sur l'étape 0 ou utilise « All Incorrect ».

Voici l'interface d'annotation PRM à l'œuvre :

Annotation de récompense de processus montrant l'évaluation étape par étapeEn mode première erreur, cliquez sur la première étape erronée et toutes les étapes suivantes sont marquées automatiquement

Mode par étape

En mode par étape, chaque étape reçoit sa propre étiquette. On obtient des données plus riches, puisqu'elles rendent compte des cas où l'agent se rattrape partiellement après une erreur, prend un détour inutile mais sans conséquence, ou effectue une étape correcte en soi mais fausse dans le contexte.

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

Mettre en place un projet d'annotation PRM

Étape 1 : préparer vos données de trace

Vos données d'entrée doivent être un fichier JSONL où chaque ligne est un objet JSON contenant la trajectoire de l'agent. Les champs clés sont id, issue_description et 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
    }
  ]
}

Si vous partez d'un format d'agent existant, servez-vous de l'outil de conversion de traces :

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 rend les traces d'agents de coding avec un surlignage correct des diffs :

Trace d'agent de coding avec rendu des diffsLes diffs de code, la sortie du terminal et les lectures de fichiers sont rendus avec coloration syntaxique

Étape 2 : créer votre configuration

Voici une configuration de projet complète pour l'annotation PRM en mode première erreur :

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"

Étape 3 : lancer le serveur d'annotation

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 &

Rendez-vous sur http://localhost:8000, connectez-vous avec l'un des comptes d'annotateur configurés, et commencez à relire les traces.

Étape 4 : suivre l'avancement

Pendant l'annotation, surveillez l'avancement et l'accord :

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

Exporter vers les formats d'entraînement

Une fois l'annotation terminée, exportez les données dans le format qu'attend votre pipeline d'entraînement.

Format PRM pour l'entraînement d'un modèle de récompense

Le format d'exportation PRM produit un objet JSON par trace, avec les étiquettes au niveau des étapes :

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

La sortie ressemble à ceci :

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}
  ]
}

Paires de préférence DPO/RLHF

Quand vous disposez de plusieurs traces pour le même problème (par exemple issues d'agents différents ou d'exécutions différentes), Potato peut produire des paires de préférence à partir des étiquettes PRM :

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

L'exportation de paires de préférence compare les traces qui ont tenté la même tâche et retient la meilleure d'après les étiquettes au niveau des étapes :

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
}

Résultats compatibles SWE-bench

Exportez au format SWE-bench pour le banc d'essai :

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

Exemples d'analyse

Une fois les annotations recueillies, ces bouts de code Python servent à analyser les données et à repérer des motifs.

Exactitude par type d'étape

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']})")

Trouver les points de défaillance récurrents

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}")

Calculer l'accord inter-annotateurs sur les étiquettes PRM

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}")

Conseils pour recueillir des données PRM sans perdre de temps

Utilisez le mode première erreur pour aller vite. Si vous entraînez un PRM destiné à guider une recherche (MCTS, échantillonnage best-of-N), le mode première erreur donne assez de signal, à une vitesse d'annotation 2 à 3 fois supérieure au mode par étape. De toute façon, la plupart des agents échouent en cascade : une erreur en entraîne une série d'autres.

Utilisez le mode par étape quand le détail compte. Si la récupération partielle, les détours anodins ou un modèle de récompense au niveau des étapes avec plus de deux étiquettes vous intéressent, le mode par étape vaut le temps supplémentaire.

Associez le PRM à la comparaison par paires. Étiquetez les traces une par une avec le PRM, puis lancez une comparaison par paires sur celles qui ont tenté le même problème. Une seule passe d'annotation vous donne à la fois des récompenses par étape et des paires de préférence.

Commencez avec des annotateurs expérimentés. L'annotation PRM demande de lire du code, des diffs et des sorties de terminal. Démarrez avec un petit groupe de développeurs chevronnés, mesurez l'accord, calibrez sur des exemples, puis passez à l'échelle.

Fixez un temps minimum par instance. Les traces se compliquent vite. Un plancher de 30 secondes empêche les annotateurs de foncer sans vraiment lire les modifications. Ajustez-le à la longueur moyenne de vos traces.

Prévoyez des exemples de calibrage. Avant la phase de production, faites étiqueter les mêmes 10 à 20 traces par tout le monde et discutez des points de désaccord. La cohérence y gagne beaucoup.