並排比較 AI 智慧體:二元、量表與多維度模式
在 Potato 中配置成對智慧體比較的三種模式:二元偏好、偏好量表,以及需要填寫理由的逐維度判斷。
為什麼用成對比較來評估智慧體
讓標註者用 1 到 10 分給一條程式設計智慧體軌跡打分,得到的資料噪聲很大,因為每個人對這個量表的校準都不一樣。一位標註者心裡的 7 分,在另一位那裡是 5 分。成對比較繞開了這個問題。標註者不再單獨給一條軌跡打分,而是並排看兩條,說出哪一條更好。這種正面對比的判斷更容易做出,在不同人之間也更一致,而且正是直接偏好最佳化(DPO)和基於人類反饋的強化學習(RLHF)所需要的輸入。
Potato 有三種成對模式,通過標註方案上的 mode 設定。它們的區別在於要求標註者做什麼,以及每次比較的成本有多高。
標註者比較兩條智慧體軌跡並選出更好的方案
標註方案期待的資料格式
三種模式讀取兩個候選項的方式是一樣的。items_key 指向資料項上的一個欄位,該欄位儲存一個包含兩個或更多條目的列表:
{"id": "pair_001", "task": "Fix the IndexError in process_batch() when the input list is empty", "responses": ["Agent A trace text...", "Agent B trace text..."]}如果你的資料是分別命名兩邊而不是把它們列在一起,Potato 會回退到資料項上的 left 和 right 這一對欄位,而且只有在 items_key 沒有指向那裡的任何欄位時才會讀取它們:
{"id": "pair_002", "task": "Fix the IndexError", "left": "Agent A trace...", "right": "Agent B trace..."}如果兩種方式都拿不到兩個候選項,標籤會渲染出來但沒有東西可比;當 items_key 指向的不是一個列表時,瀏覽器控制台會報告這一情況。
模式一:二元偏好
標註者看到兩個候選項,點選更好的那一個。一個可選的打平按鈕用於兩者一樣好或一樣差的情況。
當你需要快速拿到大量偏好資料,用於獎勵模型訓練、勝率統計或 Elo 排行榜時,就用二元模式。代價是細節:你知道哪一邊贏了,但不知道贏了多少,也不知道憑什麼贏。
annotation_task_name: "Agent Comparison - Binary"
task_dir: "."
port: 8000
data_files:
- "data/paired_traces.jsonl"
item_properties:
id_key: "id"
text_key: "task"
instance_display:
fields:
- key: "task"
type: text
label: "Task"
annotation_schemes:
- annotation_type: pairwise
name: preference
description: "Which agent produced a better solution?"
mode: binary
items_key: "responses"
show_labels: true
labels:
- "Agent A"
- "Agent B"
allow_tie: true
tie_label: "Tie (equally good or equally bad)"
randomize_order: true
sequential_key_binding: true
label_requirement:
required: true
- annotation_type: radio
name: confidence
description: "How confident are you in this judgment?"
labels:
- "Very confident"
- "Somewhat confident"
- "Not confident"
output_annotation_dir: "annotation_output/"
export_annotation_format: jsonl
user_config:
allow_all_users: truerandomize_order: true 會按標註者、按資料項打亂哪個候選項出現在哪一側,這樣沒有人會養成總是選左邊的習慣。Potato 會記錄每位標註者實際看到的順序,你在分析時正是靠它還原這次打亂。
要讓標註者看不出哪條軌跡出自哪個智慧體,就別把智慧體的名字寫進你放在 responses 裡的文本。沒有哪個設定會替你把它藏起來。
模式二:偏好量表
量表模式用一個滑塊取代兩個按鈕,讓標註者說出一個候選項好多少。如果偏好強度會進入你的訓練流程,就用這種模式,因為帶邊際加權的 DPO 會更看重那些判斷明確的樣本。
量表由它的端點定義,而不是由檔位數量定義。負值偏向左側項,正值偏向右側項,零表示兩者相當:
annotation_schemes:
- annotation_type: pairwise
name: preference_scale
description: "Which agent produced a better solution, and by how much?"
mode: scale
items_key: "responses"
labels:
- "Agent A"
- "Agent B"
scale:
min: -3
max: 3
step: 1
default: 0
labels:
min: "A is much better"
max: "B is much better"
center: "Equal"
label_requirement:
required: true量表範圍越窄,標註越快。設定 min: -2 和 max: 2 會得到五個檔位而不是七個,通常已經足夠把明確的偏好和勉強的偏好區分開。
模式三:多維度比較
標註者不再給出一個總體偏好,而是在若干個互相獨立的維度上判斷這一對軌跡,每個維度有自己的一行 A/B/打平。當你不只要知道哪個智慧體贏了、還要知道贏在哪裡時,就用這種模式,因為一條軌跡可能程式碼正確但效率很差,另一條效率很高卻漏掉了一個邊界情況。
一個維度接受 name、一個作為幫助文本顯示的可選 description,以及它自己的打平設定:
annotation_schemes:
- annotation_type: pairwise
name: multi_dim_comparison
description: "Compare the two solutions along each dimension"
mode: multi_dimension
items_key: "responses"
labels:
- "Agent A"
- "Agent B"
dimensions:
- name: correctness
description: "Which solution actually fixes the stated problem?"
allow_tie: true
- name: efficiency
description: "Which takes fewer unnecessary steps, file reads, or redundant edits?"
allow_tie: true
- name: code_quality
description: "Which is more readable, better named, and closer to existing patterns?"
allow_tie: true
- name: communication
description: "Which explains its reasoning more clearly and identifies the root cause?"
allow_tie: true
- name: robustness
description: "Which handles edge cases and verifies its changes?"
allow_tie: true
justification:
required: true
reason_categories:
- "More correct"
- "More efficient"
- "Better written"
- "Better explained"
min_rationale_chars: 20
rationale_placeholder: "Which agent was better here, and why?"維度不帶權重。加權是關於你在意什麼的決定,放在分析階段做,意味著你可以給一項已經做完的研究重新加權,而不必重新收集任何資料。
justification 是標註方案上的一個配置塊,而不是逐維度的設定,三種模式下都能用。reason_categories 給標註者一個快速的結構化理由,min_rationale_chars 給自由文本設了下限,這樣"A 更好"就不能算作一個解釋。
輸出裡有什麼
每種模式都把自己的取值存在標註方案的名稱之下。
二元模式記錄所選的一項:
{"preference": {"selection": "A"}}取值是 "A" 或 "B";當方案設定了 allow_tie: true 時還會有 "tie"。存下來的字串始終是字面量 tie,tie_label 改的只是按鈕文字,不是這個值。
量表模式把滑塊位置記錄成字串,負值代表左側項:
{"preference_scale": {"scale_value": "-2"}}多維度模式為每個維度記錄一個取值,外加編碼成 JSON 字串的理由:
{
"multi_dim_comparison": {
"correctness": "tie",
"efficiency": "A",
"code_quality": "B",
"communication": "A",
"robustness": "B",
"justification": "{\"reasons\": [\"More efficient\"], \"rationale\": \"A fixed it in three steps against four...\"}"
}
}那個 justification 欄位是一個包含 JSON 的字串,不是巢狀物件。讀取 reasons 或 rationale 之前要再解析一次。
匯出
Potato 支援 31 種標註匯出格式,用於 DPO 的偏好對格式不在其中。先把標註匯出來,偏好對自己構造:
python -m potato.export -c config.yaml -f jsonl -o ./export/把匯出的二元偏好轉成 DPO 訓練對只要一個簡短的指令碼,而自己動手的好處是可以自己決定怎麼處理打平和低置信度的判斷,不必沿用別人的選擇:
import json
from pathlib import Path
SCHEME = "preference"
pairs = []
for f in Path("export/").rglob("*.jsonl"):
with open(f) as fh:
for line in fh:
rec = json.loads(line)
labels = rec.get("labels", rec)
sel = (labels.get(SCHEME) or {}).get("selection")
if sel not in ("A", "B"):
continue # drop ties; they carry no preference direction
item = items_by_id[rec["instance_id"]] # your own data file
a, b = item["responses"][0], item["responses"][1]
chosen, rejected = (a, b) if sel == "A" else (b, a)
pairs.append({
"prompt": item["task"],
"chosen": chosen,
"rejected": rejected,
"annotator": rec.get("user_id"),
})
with open("training_data/preferences.jsonl", "w") as out:
for p in pairs:
out.write(json.dumps(p) + "\n")
print(f"Wrote {len(pairs)} preference pairs")用 instance_id 關聯回你自己的資料檔案,並且在判定哪一邊是哪一個之前,用 Potato 記錄下來的順序還原 randomize_order 的打亂。
分析
勝率
import json
from collections import defaultdict
from pathlib import Path
SCHEME = "preference"
records = []
for f in Path("export/").rglob("*.jsonl"):
with open(f) as fh:
records += [json.loads(line) for line in fh]
tally = defaultdict(lambda: {"wins": 0, "losses": 0, "ties": 0})
for rec in records:
sel = (rec.get("labels", rec).get(SCHEME) or {}).get("selection")
if not sel:
continue
item = items_by_id[rec["instance_id"]] # your own data file
# `responses` deliberately carries no agent names, so a win rate per agent
# needs fields you add yourself when building the data file.
agent_a, agent_b = item["agent_a"], item["agent_b"]
if sel == "tie":
tally[agent_a]["ties"] += 1
tally[agent_b]["ties"] += 1
else:
winner, loser = (agent_a, agent_b) if sel == "A" else (agent_b, agent_a)
tally[winner]["wins"] += 1
tally[loser]["losses"] += 1
for agent, r in sorted(tally.items()):
total = r["wins"] + r["losses"] + r["ties"]
rate = (r["wins"] + 0.5 * r["ties"]) / total * 100
print(f" {agent:<20} {rate:5.1f}% (W:{r['wins']} L:{r['losses']} T:{r['ties']})")Elo 評分
Elo 與順序有關,所以執行之前先把比較的次序打亂;如果排名很接近,就多打亂幾次取平均:
import math
from collections import defaultdict
def compute_elo(results, k=32, initial=1500):
ratings = defaultdict(lambda: initial)
for winner, loser, tied in results:
rw, rl = ratings[winner], ratings[loser]
ew = 1.0 / (1.0 + 10 ** ((rl - rw) / 400))
el = 1.0 / (1.0 + 10 ** ((rw - rl) / 400))
sw, sl = (0.5, 0.5) if tied else (1.0, 0.0)
ratings[winner] = rw + k * (sw - ew)
ratings[loser] = rl + k * (sl - el)
return dict(ratings)逐維度拆解
from collections import defaultdict
SCHEME = "multi_dim_comparison"
DIMS = ["correctness", "efficiency", "code_quality", "communication", "robustness"]
counts = defaultdict(lambda: {"A": 0, "B": 0, "tie": 0})
for rec in records:
vals = rec.get("labels", rec).get(SCHEME) or {}
for d in DIMS:
v = vals.get(d)
if v in ("A", "B", "tie"):
counts[d][v] += 1
for d in DIMS:
c = counts[d]
total = sum(c.values()) or 1
a_rate = (c["A"] + 0.5 * c["tie"]) / total * 100
print(f" {d:<16} A={a_rate:.0f}% B={100 - a_rate:.0f}% (ties {c['tie']})")分佈最接近 50/50、打平次數又很高的那個維度,通常是描述需要改寫的維度,而不是兩個智慧體真的旗鼓相當的維度。
實際做起來什麼有用
選哪種模式
二元模式適合成千上萬次比較和一個通用的獎勵模型。當偏好強度要進入訓練時,量表模式值得多花的那點時間。當你要交出的是一份診斷而不是一個排名時,多維度模式值得用;因為每個維度都要寫理由,它每次比較要多花好幾倍時間。
要收集多少次比較
要讓兩個智慧體之間的勝率穩定下來,這一對至少要收集 100 次比較。五個或更多智慧體之間的 Elo,總共幾百次就能穩定。獎勵模型訓練一般需要數以千計的偏好對,而且要同時覆蓋簡單和困難的任務,不能都擠在一類上。
抓出不認真的標註
randomize_order 去掉了固定選某一側的習慣。要抓出一路點過去的人,還要加上真正的注意力檢查和響應時間下限:
attention_checks:
enabled: true
items_file: "data/attention_checks.json"
frequency: 20
min_response_time: 10.0
failure_handling:
warn_threshold: 2
block_threshold: 5這裡真正有用的是 min_response_time。沒有人能在十秒內讀完兩條智慧體軌跡。
打平
二元模式下允許打平,但要盯住打平率。超過 30% 左右,就說明兩個智慧體太接近,做不了二元判斷,量表模式或多維度模式能告訴你更多。
組合使用不同模式
在一個大樣本池上跑二元模式拿到排名,再在一個分層抽樣的子集上跑多維度模式做診斷。二元比較餵給獎勵模型訓練,逐維度的結果告訴你該修什麼。