如何為訓練更好的程式設計智慧體收集過程獎勵資料
在 Potato 中收集用於 PRM 訓練的逐步驟獎勵標註:首個錯誤模式與逐步驟模式,以及把匯出結果整理成訓練格式的指令碼。
什麼是過程獎勵模型
標註過程獎勵的兩種方式
結果獎勵模型只看程式設計智慧體軌跡的結尾:程式碼編譯通過了嗎,測試跑過了嗎,問題解決了嗎?過程獎勵模型則給中間的每一步打分。有了每一步的訊號,訓練就能精確定位智慧體是在哪裡走偏的。
近期的研究一直在這個方向上推進。AgentPRM 為智慧體任務重新定義了過程獎勵,按每個動作朝目標推進了多少來打分,而不是按它是否正確來打分,並報告其計算效率比所對比的基線高出 8 倍以上。ToolRM 發現,在自然語言輸出上訓練出來的獎勵模型判斷工具呼叫的能力很差,於是構建了面向工具的獎勵模型,以及用來評測它們的 FC-RewardBench。作為對照,DeepSWE 只用一個稀疏的結果獎勵(測試是否通過)來訓練程式設計智慧體,在 SWE-bench Verified 上達到 42.2% 的 Pass@1,配合測試時擴充套件達到 59%。這正是過程監督想要改進的那種純結果式設定。
這些做法都需要步驟級的人工標註,而這通常就是瓶頸。資料結構參考見過程獎勵文件。
兩種標註模式
首個錯誤模式
標註者從上到下讀完整條軌跡,點選智慧體第一次出錯的那一步。之前的每一步都記為正確,那一步以及之後的全部記為錯誤。它很快,因為每條軌跡只有一個決定,而且適合常見的那種情況:智慧體一旦跑偏就再也回不來。
annotation_schemes:
- annotation_type: process_reward
name: step_rewards
description: "Click the first step where the agent makes a mistake. Steps after it are flagged automatically."
mode: first_error
steps_key: structured_turns
step_text_key: content逐步驟模式
每一步都有自己的標籤,這能捕捉到首個錯誤模式捕捉不到的東西:部分糾正、無害的繞路,或者單看沒問題、放在上下文裡卻是錯的那種步驟。allow_neutral 會加上 PRM800K 風格的第三個選項。
annotation_schemes:
- annotation_type: process_reward
name: step_rewards
description: "Rate each step correct, neutral, or incorrect"
mode: per_step
steps_key: structured_turns
step_text_key: content
allow_neutral: true
inline_with_trace: trueinline_with_trace 把控制元件掛到顯示區裡的每一步上,這在長軌跡上很重要,因為單獨的面板會讓標註者找不到自己看到哪兒了。
搭建項目
第一步:準備軌跡資料
每一行是一條軌跡。steps_key 指向步驟列表,step_text_key 指向每個步驟裡要顯示的欄位:
{
"id": "trace_001",
"issue_description": "Fix the TypeError in parse_config() when the config file is empty",
"structured_turns": [
{"step_idx": 0, "type": "thinking", "content": "I need to find where the TypeError occurs. Let me read the file."},
{"step_idx": 1, "type": "file_read", "content": "def parse_config(path):\n with open(path) as f:\n data = json.load(f)\n return data['settings']"},
{"step_idx": 2, "type": "thinking", "content": "When the file is empty json.load returns None, so None['settings'] raises. Add a check."},
{"step_idx": 3, "type": "file_edit", "content": "+ if data is None:\n+ return {}"},
{"step_idx": 4, "type": "bash_command", "content": "python -m pytest tests/test_config.py -v -> 2 passed"}
]
}從別的智慧體格式轉換過來是一個命令列步驟:
python -m potato.trace_converter \
--input claude_code_sessions.json \
--input-format claude_code \
--output data/traces.jsonl--list-formats 會列印它支援的格式,其中包括 claude_code、aider、swe_agent_trajectory、swebench、react、langchain,另外還有九種。如果你不確定手上的是哪一種,就用 --auto-detect。
第二步:寫配置
annotation_task_name: "PRM Data Collection"
task_dir: "."
data_files:
- "data/traces.jsonl"
item_properties:
id_key: id
text_key: issue_description
instance_display:
fields:
- key: issue_description
type: text
label: "Issue"
- key: structured_turns
type: coding_trace
label: "Agent trajectory"
display_options:
diff_view: unified
show_file_tree: true
show_reasoning: true
terminal_theme: dark
collapse_long_outputs: true
max_output_lines: 50
annotation_schemes:
- annotation_type: process_reward
name: step_rewards
description: "Click the first step where the agent makes an error"
mode: first_error
steps_key: structured_turns
step_text_key: content
- annotation_type: radio
name: outcome
description: "Did the agent resolve the issue?"
labels:
- name: resolved
text: "Fully resolved"
- name: partial
text: "Partially resolved"
- name: not_resolved
text: "Not resolved"
- annotation_type: text
name: error_description
description: "If the trajectory went wrong, describe the error briefly"
rows: 3
placeholder: "e.g. edited the wrong file"
label_requirement:
required: false
num_annotators_per_item: 2
attention_checks:
enabled: true
items_file: "data/attention_checks.json"
frequency: 20
min_response_time: 20.0
failure_handling:
warn_threshold: 2
block_threshold: 5
user_config:
allow_all_users: false
users:
- reviewer1@example.com
- reviewer2@example.com
- reviewer3@example.com
output_annotation_dir: "annotation_output/"
export_annotation_format: jsonl標籤物件以 name 為鍵。text 欄位是標註者讀到的內容,所以存下來的值是一個穩定的識別符號,而措辭隨時可以改。
min_response_time: 20.0 是這裡真正有用的控制項。沒有人能在二十秒裡讀完一條帶 diff 的多步驟軌跡,所以它能抓到按整條軌跡計時抓不到的搶答。
第三步:啟動伺服器
potato start config.yaml -p 8000匯出用於訓練
coding_eval 匯出器直接寫出訓練格式,整理工作它替你做了:
python -m potato.export -c config.yaml -f coding_eval -o ./export/它會寫出帶步驟級標籤的 prm_training_data.jsonl、由標註構造出的 DPO 風格 chosen/rejected 軌跡 preference_pairs.jsonl,swebench_results.jsonl,以及 code_reviews.jsonl。四個檔案預設都會寫。要縮小範圍,就傳匯出器的 types 選項:--option types=prm,preference。如果某個請求的類型在這項研究裡沒有資料,它會報告出來,而不是寫一個空檔案。
標註以 steps 列表加上模式的形式,存在標註方案的名稱之下。reward 為 1 表示正確,-1 表示錯誤,0 表示中性,null 表示這一步沒有人判斷過:
{
"step_rewards": {
"mode": "first_error",
"steps": [
{"reward": 1, "source": "human", "verified": true},
{"reward": 1, "source": "human", "verified": true},
{"reward": -1, "source": "human", "verified": true},
{"reward": -1, "source": "human", "verified": true}
]
}
}只有當你需要匯出器寫不出的格式時,才動手寫指令碼。最常見的理由是自己處理未被判斷的步驟,因為沒人判斷過的步驟是 null 而不是 0:
import json
from pathlib import Path
SCHEME = "step_rewards"
examples = []
for f in Path("export/").rglob("*.jsonl"):
with open(f) as fh:
for line in fh:
rec = json.loads(line)
payload = (rec.get("labels", rec) or {}).get(SCHEME)
if not payload:
continue
steps = payload.get("steps", [])
# Drop traces with unjudged steps rather than guessing at them.
if any(s.get("reward") is None for s in steps):
continue
item = items_by_id[rec["instance_id"]] # your own data file
examples.append({
"trace_id": rec["instance_id"],
"prompt": item["issue_description"],
"steps": [
{"content": src["content"], "label": s["reward"]}
for src, s in zip(item["structured_turns"], steps)
],
})
with open("prm_labels.jsonl", "w") as out:
for ex in examples:
out.write(json.dumps(ex) + "\n")
print(f"Wrote {len(examples)} traces")偏好對來自同一批標註。當兩條軌跡處理的是同一個問題時,匯出器會把它們配成一對,並把首個錯誤出現得更晚、或者根本沒有錯誤的那一條當作 chosen。
分析
首個錯誤落在哪裡
import json
from pathlib import Path
SCHEME = "step_rewards"
traces = []
for f in Path("export/").rglob("*.jsonl"):
with open(f) as fh:
for line in fh:
payload = (json.loads(line).get("labels", {}) or {}).get(SCHEME)
if payload:
traces.append(payload)
positions = []
all_correct = 0
for t in traces:
steps = t.get("steps", [])
first = next((i for i, s in enumerate(steps) if s.get("reward") == -1), None)
if first is None:
all_correct += 1
elif len(steps) > 1:
positions.append(first / (len(steps) - 1))
print(f"Traces annotated: {len(traces)}")
print(f"No errors: {all_correct} ({all_correct / len(traces):.1%})")
if positions:
print(f"Average first-error position: {sum(positions) / len(positions):.2f} (0=start, 1=end)")關於首個錯誤的一致性
兩位標註者很少會選中同一步,用精確一致來衡量會低估他們實際的一致程度。先把位置分箱再比較,資訊量更大:
def bin_position(steps):
first = next((i for i, s in enumerate(steps) if s.get("reward") == -1), None)
if first is None:
return "all_correct"
return "early_error" if first < len(steps) / 2 else "late_error"在標註了同一批資料項的標註者之間比較這些分箱,用的就是 num_annotators_per_item 給你的重疊量。
一些建議
追求速度就用首個錯誤模式。 如果是訓練一個 PRM 來引導搜尋,它用幾分之一的標註成本就能給出足夠的訊號,而且大多數智慧體的失敗本來就是連鎖式的。
需要細節時用逐步驟模式。 部分糾正和無害的繞路只有在逐步驟模式下才存在對應的標籤。
從有經驗的標註者開始。 這項工作要讀程式碼、讀 diff、讀終端輸出。先從一小組人開始,測一致性,用共享的樣例做校準,然後再擴大規模。
設一個響應時間下限。 軌跡會越來越長,下限能讓標註者不至於一路快進略過 diff。
做一輪校準。 讓所有人標註同樣的十到二十條軌跡,把分歧討論清楚,再開始正式標註。