Skip to content

過程獎勵標註

用首錯模式和逐步模式為過程獎勵模型收集每一步的獎勵標籤,可選中性標籤,並支援由人工核驗的 LLM 預標註。

v2.4.0 新增

過程獎勵模型評的是模型推理過程中的每一步,而不只是最終答案,所以訓練它需要的是每步一個標籤,而不是每條軌跡一個標籤。process_reward 方案負責收集這些標籤。

兩種模式在速度和細緻程度之間做取捨。首錯模式只要一次點選:第一個出錯的步驟,它之後的一切都視為受牽連。逐步模式要求對每一步都做判斷,代價更大,但能捕捉首錯模式抓不到的情況,比如智慧體從自己的錯誤裡恢復過來。

兩種模式都能配合編碼軌跡、智慧體軌跡和思維鏈這幾種顯示方式使用。

首錯模式

標註者順著軌跡往下讀,點中第一個不正確的步驟。它之前的步驟記為正確,被點中的這一步以及之後的全部記為不正確。這樣產生的標籤序列正是二元 PRM 訓練所需要的:錯誤之前是 1,從錯誤開始是 -1。

yaml
annotation_schemes:
  - annotation_type: process_reward
    name: step_rewards
    description: "Click the first step where the agent made a mistake"
    mode: first_error
    steps_key: structured_turns
    step_text_key: content

steps_key 指定存放步驟列表的欄位,step_text_key 指定每個步驟裡用來顯示的欄位。

標註流程

  1. 步驟起初都未標記
  2. 標註者點選第一個不正確的步驟
  3. 它之前的一切變為綠色
  4. 該步驟被標記為首個錯誤
  5. 它之後的一切被標記為受牽連

顏色把這種連帶關係顯示了出來,而這正是這個模式的用意:一次判斷就標完整條軌跡。

逐步模式

每一步都單獨判斷。加上 allow_neutral 可以得到 PRM800K 那種三分類標籤,其中一步可以既不算對也不算錯:

yaml
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: true

allow_neutral 只在逐步模式下起作用。首錯模式的連帶關係裡沒有中性步驟的位置,所以這個選項在那裡會被忽略。

inline_with_trace: true 把評分控制元件掛到顯示中的每一步上,而不是收在單獨的面板裡。軌跡很長時你會想要這個,否則標註者容易找不著自己看到哪了。

給標籤改名

三個按鈕預設顯示 Correct、Neutral 和 Wrong。在這些詞不合適的領域裡,reward_labels 可以給它們改名:

yaml
annotation_schemes:
  - annotation_type: process_reward
    name: step_rewards
    description: "Rate each reasoning step"
    mode: per_step
    steps_key: cot_steps
    allow_neutral: true
    reward_labels:
      correct: "Valid"
      neutral: "Neutral"
      incorrect: "Flawed"

切分思維鏈

推理軌跡通常是一整條長字串,而不是步驟列表。cot_segmentation 會在標註開始前把它切開:

yaml
cot_segmentation:
  source_key: reasoning      # the item field holding the long CoT string
  strategy: auto             # blank_line | numbered | markers | sentence | llm | auto
  target_key: cot_steps      # where the step list is written
  min_step_chars: 30         # merge anything shorter into the previous step
  max_steps: 200
 
instance_display:
  fields:
    - key: cot_steps
      type: cot_trace
      label: "Reasoning"
 
annotation_schemes:
  - annotation_type: process_reward
    name: step_rewards
    description: "Rate each reasoning step"
    mode: per_step
    steps_key: cot_steps
    allow_neutral: true
    inline_with_trace: true

min_step_chars 值得設一下。不設的話,按句子和按空行切分會產生一行的碎片,標註者孤立地看根本沒法判斷,拿回來的標籤就是噪聲。

LLM 預標註

可以讓模型先給出每一步的標籤,標註者再逐個確認或改掉,在長軌跡上這比從零開始打標籤要快:

yaml
annotation_schemes:
  - annotation_type: process_reward
    name: step_rewards
    description: "Verify or correct each suggested reward"
    mode: per_step
    steps_key: cot_steps
    allow_neutral: true
    inline_with_trace: true
    ai_prelabel: true
    require_verification: true
 
ai_support:
  enabled: true
  endpoint_type: openai
  ai_config:
    model: gpt-4o-mini
    max_tokens: 768
    temperature: 0.1
    include:
      all: true

ai_prelabel 加上那個請求建議的按鈕。require_verification 會在每條建議都被確認或修改之前拒絕接受這份標註,正是它保證了輸出是人給的標籤,而不是模型給的。

max_tokens 要留足餘量。一條長思維鏈加上每一步的判定是一份很大的回覆,而被截斷的回覆呈現出來就是什麼建議都沒有。

輸出裡有什麼

標註以方案名為鍵存放,內容是一個 steps 列表,外加收集時所用的 mode:

json
{
  "step_rewards": {
    "mode": "per_step",
    "steps": [
      {"reward": 1, "source": "human", "verified": true},
      {"reward": -1, "source": "human", "verified": true},
      {"reward": 0, "source": "ai", "verified": true, "ai_reward": 0},
      {"reward": null, "source": null, "verified": false}
    ]
  }
}

reward 為 1 表示正確,-1 表示不正確,0 表示中性。被跳過的步驟是 null 而不是 0,所以沒人判斷過的步驟和被判為既不對也不錯的步驟仍然分得清。這個區別在標籤變成訓練資料時很重要,因為把跳過當成中性等於憑空造出訊號。

source 記錄這個值來自人還是來自模型,verified 記錄是否有人確認過。開著 ai_prelabel 時,即使標註者改掉了建議,ai_reward 也會保留模型最初的那個,正是它讓你不必再跑一遍就能報告模型對的頻率。

匯出

Potato 沒有內建的 PRM 或 DPO 匯出格式。把標註匯出來,自己整理成需要的形狀:

bash
python -m potato.export -c config.yaml -f jsonl -o ./export/

把首錯標註轉成步驟級的訓練標籤是段很短的程式碼,而自己來寫,才能由你決定跳過的步驟怎麼處理:

python
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"],
                "steps": [
                    {"content": src, "label": s["reward"]}
                    for src, s in zip(item["structured_turns"], steps)
                ],
            })
 
with open("prm_training_data.jsonl", "w") as out:
    for ex in examples:
        out.write(json.dumps(ex) + "\n")
 
print(f"Wrote {len(examples)} traces")

分析

python
import json
from collections import Counter
from pathlib import Path
 
SCHEME = "step_rewards"
 
traces = []
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 payload:
                traces.append(payload)
 
print(f"Traces annotated: {len(traces)}")
 
all_correct = sum(
    1 for t in traces
    if all(s.get("reward") == 1 for s in t.get("steps", []))
)
print(f"Traces with no errors: {all_correct} ({all_correct / len(traces):.1%})")
 
# Where the first error lands
first_error_positions = []
for t in traces:
    for i, s in enumerate(t.get("steps", [])):
        if s.get("reward") == -1:
            first_error_positions.append(i)
            break
 
if first_error_positions:
    avg = sum(first_error_positions) / len(first_error_positions)
    print(f"Average first-error position: step {avg:.1f}")
    print("By position:", Counter(first_error_positions).most_common(10))

開著 ai_prelabel 時,同一批記錄還能告訴你模型的建議有多少經受住了稽核:

python
kept = changed = 0
for t in traces:
    for s in t.get("steps", []):
        if s.get("ai_reward") is None:
            continue
        if s.get("reward") == s.get("ai_reward"):
            kept += 1
        else:
            changed += 1
 
total = kept + changed
if total:
    print(f"AI suggestions kept: {kept}/{total} ({kept / total:.1%})")

在一個新領域裡信任預標註之前,這個接受率就是要盯的數字。接近 100% 的比率通常意味著標註者是在確認而不是在檢查,而不是模型真有那麼準。

研究背景

過程獎勵標註的存在是為了支撐多步系統上的獎勵模型研究,推動整套做法的那個發現是:正確的最終答案可以架在有缺陷的推理之上。步驟級的標籤把兩者分開,而這裡的首錯模式和逐步模式對應的正是這一脈研究所用的標籤格式。

關於這套分類體系的設計以及隨之而來的一致性問題,見逐步錯誤定位,它從錯誤分類的角度講同樣的軌跡。

另請參閱

實現細節見源文件。