Skip to content
Guides5 min read

より良いコーディングエージェントを訓練するためのプロセス報酬データの集め方

Potatoを使ってPRM訓練用のステップ単位の報酬シグナルを収集する手順を解説します。first-errorモード、ステップごとのアノテーション、訓練パイプラインへのエクスポートまで扱います。

Potato Team

プロセス報酬モデルとは

プロセス報酬をラベル付けする2つの方法:first-errorモードは破綻した1点を、ステップごとモードは全ステップを評価しますプロセス報酬をラベル付けする2つの方法

結果報酬モデル(ORM)が見るのは、コーディングエージェントのトラジェクトリの末尾だけです。コードはコンパイルできたか、テストは通ったか、issueは解決したか。これに対してプロセス報酬モデル(PRM)は、途中の各ステップを採点します。すべてのステップに報酬シグナルがあると、訓練手法はエージェントがどこで誤ったかを特定でき、学習のサンプル効率が上がり、汎化にも効く傾向があります。

近年の研究はこの方向を押し進めています。AgentPRM はエージェントタスク向けにプロセス報酬を定義し直し、各行動を正しさではなく目標にどれだけ近づいたかで採点して、比較対象のベースラインより8倍以上の計算効率を報告しています。ToolRM は、自然言語出力で学習した報酬モデルはツール呼び出しをうまく判定できないことを見いだし、ツール専用の報酬モデルと、それを評価するための FC-RewardBench を作りました。対照的に、DeepSWE はテストが通るかどうかという疎な結果報酬だけでコーディングエージェントを学習させ、SWE-bench Verified で Pass@1 42.2%、テスト時スケーリングありで59%に達しています。プロセス監督が改善しようとしているのは、まさにこの結果のみの設定です。

いずれも必要とするのは質の高いステップ単位の人手アノテーションで、たいていはそこがボトルネックになります。Potatoのプロセス報酬スキーマは、そのデータ収集を速くするために作られています。基盤となるスキーマについてはトラジェクトリ評価のドキュメントを、トレース入力の詳細についてはエージェントトレースのドキュメントを参照してください。

2つのアノテーションモード

Potatoには、速度と粒度を引き換えにする2つのPRMアノテーションモードがあります。データ予算と目的に合うほうを選んでください。

first-errorモード

first-errorモードでは、アノテーターがトラジェクトリを上から下へ読み、エージェントが最初に誤ったステップをクリックします。Potatoはそれより前のステップをすべて正しい、クリックしたステップ以降をすべて誤りとしてマークします。

判断すべき箇所が1つだけなので速く進みます。エラーが連鎖する場合、つまり一度道を外れたエージェントが立て直すことはめったにないという実際によくある状況に向いています。

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.

first-errorのアノテーションの流れは次のようになります。

  1. アノテーターがトレースを開くと、CodingTraceDisplayコンポーネントによって全ステップが描画されます。
  2. diff、ターミナル出力、推論を確認しながら、ステップを順に読んでいきます。
  3. 最初の誤ったステップを見つけたら、その横のエラーマーカーをクリックします。
  4. ステップ0からN-1が緑(正しい)に、ステップNから末尾までが赤(誤り)に変わります。
  5. アノテーターは自動的に付いたラベルを確認し、「Submit」をクリックして確定します。

トレース全体が正しい場合(エージェントがタスクを完璧に解いた場合)、アノテーターは「All Correct」をクリックします。最初のステップから既に誤っている場合は、ステップ0をクリックするか「All Incorrect」を使います。

PRMのアノテーションインターフェースの実際の様子はこちらです。

ステップ単位の評価を行うプロセス報酬アノテーションfirst-errorモードでは、最初の誤ったステップをクリックすると以降のステップが自動でマークされます

ステップごとモード

ステップごとモードでは、すべてのステップに個別のラベルを付けます。エージェントがエラーから部分的に立て直した場合、無害だが不要な回り道をした場合、それ単体では問題ないが文脈上は誤っているステップなども捉えられるので、より情報量の多いデータになります。

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

PRMアノテーションプロジェクトを立ち上げる

ステップ1:トレースデータを用意する

入力データは、1行ごとにエージェントのトラジェクトリを表すJSONオブジェクトが入ったJSONLファイルにします。主要なフィールドはidissue_descriptionstructured_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
    }
  ]
}

既存のエージェント形式から変換する場合は、トレースコンバータのツールを使います。

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はコーディングエージェントのトレースを、diffのハイライト付きで描画します。

diffを描画したコーディングエージェントのトレースコードのdiff、ターミナル出力、ファイルの読み取りがシンタックスハイライト付きで描画されます

ステップ2:設定ファイルを作る

first-errorモードでPRMアノテーションを行う、完成した状態のプロジェクト設定は次のとおりです。

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"

ステップ3:アノテーションサーバーを起動する

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 &

http://localhost:8000を開き、設定したアノテーターアカウントのいずれかでログインして、トレースのレビューを始めます。

ステップ4:進捗を確認する

アノテーションを進めている間は、進捗と一致度を確認します。

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

訓練用形式へのエクスポート

アノテーションが終わったら、訓練パイプラインが期待する形式でデータをエクスポートします。

報酬モデル訓練向けのPRM形式

PRMのエクスポート形式は、トレースごとに1つのJSONオブジェクトを出力し、その中にステップ単位のラベルが入ります。

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

出力は次のようになります。

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

DPO/RLHFの選好ペア

同じissueに対するトレースが複数ある場合(別のエージェントや別の実行から得たものなど)、PotatoはPRMラベルをもとに選好ペアを生成できます。

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

選好ペアのエクスポートは、同じタスクに取り組んだトレース同士を比較し、ステップ単位のラベルに基づいて良いほうを選びます。

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互換の結果

ベンチマーク用にSWE-bench形式でエクスポートします。

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

分析の例

アノテーションを集めたら、次のPythonスニペットでデータを分析し、傾向を見つけられます。

ステップ種別ごとのステップ単位の正解率

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

よくある失敗箇所を見つける

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

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

PRMデータ収集を効率よく進めるコツ

速さが要るならfirst-errorモードを使う。探索(MCTS、best-of-Nサンプリング)を導くPRMを訓練するなら、first-errorモードでも十分なシグナルが得られ、ステップごとモードの2〜3倍の速さでアノテーションできます。どのみち多くのエージェントは連鎖的に失敗します。1つのミスが、その後の悪いステップの連なりにつながるからです。

細かさが必要なときはステップごとモードを使う。部分的な立て直しや無害な回り道が気になる場合、あるいは2つより多いラベルでステップ単位の報酬モデルを作る場合は、ステップごとモードは余分にかかる時間に見合います。

PRMとペアワイズ比較を組み合わせる。まずトレースを個別にPRMでラベル付けし、そのうえで同じissueに取り組んだトレース同士でペアワイズ比較を行います。1回のアノテーションで、ステップ単位の報酬と選好ペアの両方が手に入ります。

経験のあるアノテーターから始める。PRMのアノテーションはコード、diff、ターミナル出力を読む作業です。経験のある開発者の少人数から始めて、一致度を測り、事例で基準をそろえてから規模を広げてください。

インスタンスあたりの最小時間を設定する。トレースは複雑になりがちです。30秒の下限があれば、変更をろくに読まないまま次々に流していくのを防げます。平均的なトレースの長さに合わせて調整してください。

基準合わせの例を用意する。本番のアノテーションに入る前に、全員で同じ10〜20件のトレースをラベル付けし、食い違ったところを話し合ってください。一貫性がかなり変わります。