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.
Qué son los modelos de recompensa de proceso
Dos 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.
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í:
- El anotador abre una traza y ve todos los pasos renderizados con el componente CodingTraceDisplay.
- Lee los pasos en orden y examina los diffs, las salidas de terminal y el razonamiento.
- Cuando encuentra el primer paso incorrecto, hace clic en el marcador de error que hay junto a él.
- Los pasos 0 a N-1 se ponen en verde (correctos) y los pasos de N en adelante en rojo (incorrectos).
- 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:
En 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.
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:
{
"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:
# 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.jsonlPotato renderiza las trazas de agentes de coding con resaltado de diffs:
Los 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:
# 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
# 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:
# Check annotation progress
potato status config.yaml
# View inter-annotator agreement
potato agreement config.yaml --metric krippendorff_alphaExportar 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:
potato export \
--format prm \
--project ./output/ \
--output ./training_data/prm_labels.jsonlLa salida tiene esta pinta:
{
"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:
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:
{
"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:
potato export \
--format swe_bench \
--project ./output/ \
--output ./training_data/swe_bench_results.jsonEjemplos 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
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
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
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.