Skip to content
Guides11 min read

Cómo recopilar datos de recompensa de proceso para entrenar mejores agentes de coding

Guía paso a paso para recopilar señales de recompensa por paso y entrenar PRM con Potato. Cubre el modo de primer error, la anotación por paso y la exportación a pipelines de entrenamiento.

Potato Team

Qué son los modelos de recompensa de proceso

Two ways to label process rewards: first-error mode marks one breaking point, per-step mode rates every stepDos formas de etiquetar recompensas de proceso

Los modelos de recompensa de resultado (ORM) solo miran el final de la trayectoria de un agente de coding: ¿compiló el código?, ¿pasaron las pruebas?, ¿se resolvió el problema? Los modelos de recompensa de proceso (PRM) puntúan en cambio cada paso intermedio. Con una señal de recompensa en cada paso, los métodos de entrenamiento pueden señalar dónde se equivocó el agente, lo que suele hacer el aprendizaje más eficiente en muestras y ayuda a la generalización.

Hay trabajo reciente en esta dirección. AgentPRM redefine las recompensas de proceso para tareas de agente: puntúa cada acción por el avance que logra hacia el objetivo y no por si es correcta, y reporta más de 8× mejor eficiencia de cómputo que las líneas base con las que se compara. ToolRM encontró que los modelos de recompensa entrenados con salidas en lenguaje natural juzgan mal las llamadas a herramientas, y construyó modelos de recompensa específicos para herramientas junto con FC-RewardBench para evaluarlos. En contraste, DeepSWE entrena un agente de código solo con una recompensa de resultado dispersa, si pasan las pruebas, y alcanza 42,2 % de Pass@1 y 59 % con escalado en tiempo de inferencia en SWE-bench Verified. Ese es el montaje basado solo en resultados que la supervisión de proceso intenta mejorar.

Lo que todos ellos necesitan es buena anotación humana a nivel de paso, y ahí suele estar el cuello de botella. Los esquemas de recompensa de proceso de Potato están hechos para que recopilar esos datos sea más rápido. Para el esquema subyacente, consulta la documentación de evaluación de trayectorias, y para los detalles del formato de entrada de trazas, la documentación de trazas de agente.

Dos modos de anotación

Potato tiene dos modos de anotación PRM que intercambian velocidad por granularidad. Elige el que encaje con tu presupuesto de datos y tus objetivos.

Modo de primer error

En el modo de primer error, el anotador lee la trayectoria de arriba abajo y hace clic en el primer paso en el que el agente comete un fallo. Potato marca entonces como correctos todos los pasos anteriores y como incorrectos el paso señalado y todos los siguientes.

Es rápido porque el anotador solo tiene que encontrar un punto de decisión. Funciona bien cuando los errores se encadenan, es decir, cuando un agente se descarrila y rara vez se recupera, que es el caso habitual en la práctica.

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.

El flujo de anotación de primer error es así:

  1. El anotador abre una traza y ve todos los pasos renderizados con el componente CodingTraceDisplay.
  2. Lee los pasos en orden y examina los diffs, las salidas de terminal y el razonamiento.
  3. Cuando encuentra el primer paso incorrecto, hace clic en el marcador de error que hay junto a él.
  4. Los pasos 0 a N-1 se ponen en verde (correctos) y los pasos de N en adelante en rojo (incorrectos).
  5. El anotador revisa el etiquetado automático y pulsa «Submit» para confirmar.

Si toda la traza es correcta (el agente resolvió la tarea sin fallos), el anotador pulsa «All Correct». Si el primer paso ya está mal, hace clic en el paso 0 o usa «All Incorrect».

Así se ve la interfaz de anotación PRM en funcionamiento:

Process reward annotation showing per-step evaluationEn el modo de primer error, haz clic en el primer paso incorrecto y todos los pasos siguientes se marcan automáticamente

Modo por paso

En el modo por paso, cada paso recibe su propia etiqueta. Esto produce datos más ricos, porque recoge los casos en los que un agente se recupera parcialmente de un error, toma un desvío inofensivo pero innecesario o da un paso que por sí solo está bien pero es erróneo en su contexto.

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

Montar un proyecto de anotación PRM

Paso 1: prepara los datos de las trazas

Los datos de entrada deben ser un archivo JSONL en el que cada línea sea un objeto JSON con la trayectoria del agente. Los campos clave son id, issue_description y 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 vienes de un formato de agente ya existente, usa la herramienta de conversión de trazas:

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 renderiza las trazas de agentes de coding con resaltado de diffs:

Coding agent trace with diff renderingLos diffs de código, la salida de terminal y las lecturas de archivos se renderizan con resaltado de sintaxis

Paso 2: crea la configuración

Esta es una configuración de proyecto completa para anotación PRM con el modo de primer error:

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"

Paso 3: arranca el servidor de anotación

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 &

Ve a http://localhost:8000, inicia sesión con una de las cuentas de anotador configuradas y empieza a revisar trazas.

Paso 4: sigue el progreso

Mientras la anotación está en marcha, vigila el avance y el acuerdo:

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

Exportar a formatos de entrenamiento

Cuando termine la anotación, exporta los datos con el formato que espere tu pipeline de entrenamiento.

Formato PRM para entrenar modelos de recompensa

El formato de exportación PRM produce un objeto JSON por traza con las etiquetas a nivel de paso:

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

La salida tiene esta pinta:

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

Pares de preferencia para DPO/RLHF

Cuando tienes varias trazas del mismo problema (por ejemplo, de distintos agentes o de distintas ejecuciones), Potato puede generar pares de preferencia a partir de las etiquetas PRM:

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

La exportación de pares de preferencia compara trazas que intentaron la misma tarea y elige la mejor según las etiquetas de nivel de paso:

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
}

Resultados compatibles con SWE-bench

Exporta en formato SWE-bench para hacer benchmarking:

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

Ejemplos de análisis

Una vez recopiladas las anotaciones, usa estos fragmentos de Python para analizar los datos e identificar patrones.

Exactitud por tipo de paso

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

Encontrar los puntos de fallo habituales

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

Calcular el acuerdo entre anotadores sobre las etiquetas 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}")

Consejos para recopilar datos PRM con eficacia

Usa el modo de primer error si buscas velocidad. Si entrenas un PRM para guiar búsqueda (MCTS, muestreo best-of-N), el modo de primer error da señal suficiente a una velocidad de anotación 2 o 3 veces mayor que la del modo por paso. De todas formas, casi todos los agentes fallan en cascada: un error arrastra una cadena de pasos malos.

Usa el modo por paso cuando necesites el detalle. Si te importa la recuperación parcial, los desvíos inofensivos o estás construyendo un modelo de recompensa por paso con más de dos etiquetas, el modo por paso compensa el tiempo extra.

Combina PRM con comparación por pares. Etiqueta las trazas por separado con PRM y luego pasa una comparación por pares sobre las trazas que abordaron el mismo problema. Con una sola pasada de anotación obtienes recompensas por paso y pares de preferencia.

Empieza con anotadores con experiencia. Anotar PRM implica leer código, diffs y salida de terminal. Empieza con un grupo pequeño de desarrolladores con oficio, mide el acuerdo, calibra con ejemplos y luego amplía.

Fija un tiempo mínimo por instancia. Las trazas se complican. Un suelo de 30 segundos evita que los anotadores pasen de largo sin leer de verdad los cambios. Ajústalo a la longitud media de tus trazas.

Prepara ejemplos de calibración. Antes de la anotación de producción, que todo el mundo etiquete las mismas 10 o 20 trazas y luego hablad de dónde discrepasteis. La consistencia mejora bastante.