Confrontare agenti AI fianco a fianco: modalità binaria, a scala e multidimensione
Configura il confronto a coppie tra agenti in Potato con tre modalità: preferenza binaria, scala continua e giudizio multicriterio per dimensione con giustificazione obbligatoria.
Perché il confronto a coppie nella valutazione degli agenti
Chiedere a qualcuno di valutare la trace di un agente di coding su una scala da 1 a 10 produce dati rumorosi, perché ognuno calibra quella scala a modo suo. Il 7 di un annotatore è il 5 di un altro. Il confronto a coppie aggira il problema. Invece di valutare le trace isolatamente, gli annotatori ne guardano due affiancate e dicono quale è migliore. Un giudizio testa a testa è più facile da formulare, più coerente tra persone diverse ed è esattamente quello che serve per la Direct Preference Optimization (DPO) e il Reinforcement Learning from Human Feedback (RLHF).
È lo stesso approccio usato per addestrare i reward model nell'allineamento dei modelli linguistici, e si trasferisce senza attriti agli agenti di coding: raccogli preferenze umane tra coppie di trajectory di agenti, addestraci sopra un reward model, poi usa quel modello per guidare l'addestramento dell'agente o per scegliere il migliore tra N candidati in fase di inferenza.
Potato ha tre modalità di confronto a coppie, ciascuna adatta a un diverso bisogno di valutazione e a un diverso budget di dati.
L'interfaccia mette due trace fianco a fianco:
Annotators compare two agent traces and select which approach was better
Modalità 1: preferenza binaria
È la modalità più semplice e rapida. L'annotatore vede due trace affiancate e fa clic su quella migliore. Un pulsante di pareggio facoltativo copre i casi in cui entrambe sono ugualmente buone o ugualmente scadenti.
Quando usare la modalità binaria
Scegli la modalità binaria quando ti servono molti dati di preferenza in poco tempo. Va bene per addestrare reward model di base, calcolare i tassi di vittoria degli agenti e costruire classifiche Elo. Lo svantaggio è che perdi le sfumature: sai quale trace ha vinto, ma non di quanto né su quali fronti.
Configurazione
# config.yaml
project_name: "Agent Comparison - Binary"
port: 8000
data:
source: "local"
input_path: "./data/paired_traces.jsonl"
data_format: "paired_coding_trace"
coding_agent:
display:
diff_style: "unified"
syntax_highlighting: true
terminal_theme: "dark"
file_tree:
enabled: true
position: "left"
collapsible:
auto_collapse_thinking: true
comparison:
layout: "side_by_side" # "side_by_side" or "tabbed"
label_a: "Agent A"
label_b: "Agent B"
randomize_order: true # Randomize which trace appears on which side
show_agent_identity: false # Hide agent names to avoid bias
sync_scroll: false # Independent scrolling for each trace
annotation_schemes:
- annotation_type: pairwise
name: preference
description: "Which agent produced a better solution?"
items_key:
- value: "a"
text: "Agent A is better"
keyboard_shortcut: "1"
- value: "b"
text: "Agent B is better"
keyboard_shortcut: "2"
- value: "tie"
text: "Tie (equally good or equally bad)"
keyboard_shortcut: "3"
allow_tie: true
- annotation_type: radio
name: confidence
labels:
- value: "high"
text: "Very confident"
- value: "medium"
text: "Somewhat confident"
- value: "low"
text: "Not confident"
output:
path: "./output/"
format: "jsonl"
quality_control:
inter_annotator_agreement: true
overlap_percentage: 20
attention_checks:
annotators:
- username: "judge1"
- username: "judge2"Il flusso di lavoro di annotazione
L'annotatore riceve uno schermo diviso. A sinistra la Trace A viene resa con il CodingTraceDisplay completo: diff, blocchi di terminale, letture di file, ragionamento. A destra la Trace B per lo stesso compito. Ogni lato scorre in modo indipendente.
La descrizione del compito sta sopra entrambe le trace, così l'annotatore sa che cosa stavano cercando di fare i due agenti.
Sotto ci sono tre pulsanti: «Agent A is better», «Agent B is better» e «Tie». Con randomize_order attivo, quale agente sia A e quale B viene mescolato per ogni elemento, così gli annotatori non possono prendere l'abitudine di scegliere sempre il lato sinistro o quello destro.
Per una valutazione più fine, l'interfaccia supporta anche più dimensioni:
Binary preference, continuous scale, and multi-dimension modes are available
Modalità 2: scala continua
La modalità a scala permette all'annotatore di dire quanto una trace è migliore, non solo quale ha vinto. Invece di un singolo clic trascina un cursore che va da «A much better» a sinistra a «B much better» a destra, con «Equal» al centro.
Quando usare la modalità a scala
Usa la modalità a scala quando conta l'intensità della preferenza e non solo la sua direzione. Un cursore vicino all'estremo indica un divario di qualità netto; vicino al centro significa che le due trace erano vicine. DPO e pipeline simili possono pesare gli esempi in base a quell'intensità, dando più peso ai casi netti.
Configurazione
# config.yaml
project_name: "Agent Comparison - Scale"
port: 8000
data:
source: "local"
input_path: "./data/paired_traces.jsonl"
data_format: "paired_coding_trace"
coding_agent:
display:
diff_style: "unified"
syntax_highlighting: true
terminal_theme: "dark"
file_tree:
enabled: true
collapsible:
auto_collapse_thinking: true
comparison:
layout: "side_by_side"
randomize_order: true
show_agent_identity: false
annotation_schemes:
- annotation_type: pairwise
name: preference_scale
description: "Which agent produced a better solution, and by how much?"
scale:
points: 7 # 7-point scale
labels:
1: "A is much better"
2: "A is better"
3: "A is slightly better"
4: "Equal"
5: "B is slightly better"
6: "B is better"
7: "B is much better"
default: 4 # Start at "Equal"
show_numeric_value: true
output:
path: "./output/"
format: "jsonl"
quality_control:
inter_annotator_agreement: true
overlap_percentage: 20
annotators:
- username: "judge1"
- username: "judge2"Usare una scala a 5 punti
Per annotare più in fretta con un po' meno granularità, scendi a una scala a 5 punti:
annotation_schemes:
- annotation_type: pairwise
name: preference_scale_5
description: "Compare the two solutions"
scale:
points: 5
labels:
1: "A is clearly better"
2: "A is somewhat better"
3: "About equal"
4: "B is somewhat better"
5: "B is clearly better"
default: 3Modalità 3: confronto multidimensione
È la modalità più dettagliata. Invece di una singola preferenza complessiva, l'annotatore giudica ogni trace su più dimensioni indipendenti. Ogni dimensione riceve la propria scelta A/B/pareggio e ogni scelta richiede una giustificazione scritta.
Quando usare la modalità multidimensione
Usala quando vuoi sapere non solo quale agente ha vinto, ma perché. Una trace può avere codice corretto ed efficienza pessima; un'altra può essere efficiente ma ignorare un caso limite. I dati per dimensione che ne escono possono addestrare reward model specifici per dimensione o tornare come riscontro dettagliato a chi sviluppa l'agente.
Configurazione
# config.yaml
project_name: "Agent Comparison - Multi-Dimension"
port: 8000
data:
source: "local"
input_path: "./data/paired_traces.jsonl"
data_format: "paired_coding_trace"
coding_agent:
display:
diff_style: "unified"
syntax_highlighting: true
terminal_theme: "dark"
file_tree:
enabled: true
collapsible:
auto_collapse_thinking: true
comparison:
layout: "side_by_side"
randomize_order: true
show_agent_identity: false
annotation_schemes:
- annotation_type: pairwise
name: multi_dim_comparison
description: "Compare the two solutions along each dimension"
- name: "efficiency"
label: "Efficiency"
description: >
How efficient is the agent's process? Does it take unnecessary
steps, read irrelevant files, or make redundant edits?
options: ["A", "B", "Tie"]
require_justification: true
justification_placeholder: "Which agent was more efficient and why?"
weight: 0.2
- name: "code_quality"
label: "Code Quality"
description: >
Is the code well-written? Consider readability, naming,
error handling, documentation, and adherence to existing patterns.
options: ["A", "B", "Tie"]
require_justification: true
justification_placeholder: "Which produces better quality code?"
weight: 0.2
- name: "communication"
label: "Communication"
description: >
How well does the agent explain its reasoning? Are its thinking
steps clear and logical? Does it identify the root cause?
options: ["A", "B", "Tie"]
require_justification: true
justification_placeholder: "Which agent communicates its approach better?"
weight: 0.1
- name: "robustness"
label: "Robustness"
description: >
Does the solution handle edge cases? Does the agent verify its
changes with tests? Is the fix narrow and targeted or fragile?
options: ["A", "B", "Tie"]
require_justification: true
justification_placeholder: "Which solution is more robust?"
weight: 0.1
output:
path: "./output/"
format: "jsonl"
quality_control:
inter_annotator_agreement: true
overlap_percentage: 25 # Higher overlap for this detailed task
minimum_time_per_instance: 120 # 2 minutes minimum for thorough review
annotators:
- username: "judge1"
- username: "judge2"Preparare i dati delle trace appaiate
Tutte e tre le modalità prendono in input trace appaiate. Ogni riga del file JSONL contiene due trace che hanno affrontato lo stesso compito.
Formato dei dati
{
"id": "pair_001",
"task_description": "Fix the IndexError in process_batch() when the input list is empty",
"repo": "myorg/myproject",
"trace_a": {
"agent": "claude_code",
"model": "claude-sonnet-4-20250514",
"structured_turns": [
{
"step_idx": 0,
"type": "file_read",
"path": "src/batch.py",
"content": "def process_batch(items):\n result = items[0]\n ...",
"start_line": 10,
"end_line": 25
},
{
"step_idx": 1,
"type": "file_edit",
"path": "src/batch.py",
"diff": "--- a/src/batch.py\n+++ b/src/batch.py\n@@ -10,3 +10,5 @@\n def process_batch(items):\n+ if not items:\n+ return []\n result = items[0]\n"
},
{
"step_idx": 2,
"type": "bash_command",
"command": "python -m pytest tests/test_batch.py -v",
"output": "PASSED",
"exit_code": 0
}
]
},
"trace_b": {
"agent": "swe_agent",
"model": "gpt-4o",
"structured_turns": [
{
"step_idx": 0,
"type": "bash_command",
"command": "find . -name '*.py' | xargs grep 'process_batch'",
"output": "src/batch.py:def process_batch(items):\ntests/test_batch.py: process_batch([])",
"exit_code": 0
},
{
"step_idx": 1,
"type": "file_read",
"path": "src/batch.py",
"content": "def process_batch(items):\n result = items[0]\n ...",
"start_line": 1,
"end_line": 50
},
{
"step_idx": 2,
"type": "file_edit",
"path": "src/batch.py",
"diff": "--- a/src/batch.py\n+++ b/src/batch.py\n@@ -10,3 +10,6 @@\n def process_batch(items):\n+ if items is None or len(items) == 0:\n+ logger.warning('Empty input to process_batch')\n+ return []\n result = items[0]\n"
},
{
"step_idx": 3,
"type": "bash_command",
"command": "python -m pytest tests/ -v",
"output": "PASSED (12 tests)",
"exit_code": 0
}
]
}
}Costruire le coppie a partire da trace singole
Se hai trace singole che hanno affrontato gli stessi compiti, l'utilità di appaiamento le assembla per te:
# Generate all possible pairs for each task
potato pair-traces \
--input ./data/individual_traces.jsonl \
--output ./data/paired_traces.jsonl \
--pair_by "task_id" \
--strategy "all_pairs"
# Or sample a fixed number of pairs per task
potato pair-traces \
--input ./data/individual_traces.jsonl \
--output ./data/paired_traces.jsonl \
--pair_by "task_id" \
--strategy "sample" \
--pairs_per_task 3Esportare i dati di confronto
Coppie di preferenza per DPO/RLHF
Il formato di esportazione principale per i confronti a coppie sono le coppie di preferenza per l'addestramento DPO o RLHF:
potato export \
--format dpo_preferences \
--project ./output/ \
--output ./training_data/preferences.jsonlPer la modalità binaria l'output è semplice:
{
"prompt": "Fix the IndexError in process_batch() when the input list is empty",
"chosen": {"agent": "claude_code", "trace_id": "trace_a_001", "steps": [...]},
"rejected": {"agent": "swe_agent", "trace_id": "trace_b_001", "steps": [...]},
"annotator": "judge1",
"confidence": "high"
}La modalità a scala aggiunge l'intensità della preferenza:
{
"prompt": "Fix the IndexError in process_batch()",
"chosen": {"agent": "claude_code", "trace_id": "trace_a_001"},
"rejected": {"agent": "swe_agent", "trace_id": "trace_b_001"},
"preference_strength": 0.83,
"scale_value": 2,
"justification": "Agent A found and fixed the bug in fewer steps with cleaner code"
}La modalità multidimensione porta con sé le preferenze per dimensione:
{
"prompt": "Fix the IndexError in process_batch()",
"chosen": {"agent": "claude_code", "trace_id": "trace_a_001"},
"rejected": {"agent": "swe_agent", "trace_id": "trace_b_001"},
"overall_preference": "A",
"dimensions": {
"correctness": {"preference": "Tie", "justification": "Both correctly fix the bug"},
"efficiency": {"preference": "A", "justification": "A solves it in 3 steps vs 4"},
"code_quality": {"preference": "B", "justification": "B adds logging and handles None"},
"communication": {"preference": "A", "justification": "A's reasoning is more focused"},
"robustness": {"preference": "B", "justification": "B runs full test suite, not just one file"}
},
"weighted_score_a": 0.55,
"weighted_score_b": 0.45
}Analisi: tassi di vittoria, punteggi Elo e dettaglio per dimensione
Calcolare i tassi di vittoria
import json
from collections import defaultdict
with open("training_data/preferences.jsonl") as f:
prefs = [json.loads(line) for line in f]
wins = defaultdict(lambda: {"wins": 0, "losses": 0, "ties": 0})
for pref in prefs:
agent_chosen = pref["chosen"]["agent"]
agent_rejected = pref["rejected"]["agent"]
if agent_chosen == agent_rejected:
continue # Skip self-comparisons
if pref.get("overall_preference") == "Tie":
wins[agent_chosen]["ties"] += 1
wins[agent_rejected]["ties"] += 1
else:
wins[agent_chosen]["wins"] += 1
wins[agent_rejected]["losses"] += 1
print("Agent Win Rates:")
print("-" * 55)
for agent, record in sorted(wins.items()):
total = record["wins"] + record["losses"] + record["ties"]
win_rate = (record["wins"] + 0.5 * record["ties"]) / total * 100
print(f" {agent:<20} {win_rate:5.1f}% "
f"(W:{record['wins']} L:{record['losses']} T:{record['ties']})")Calcolare i punteggi Elo
import json
import math
from collections import defaultdict
def compute_elo(preferences, k=32, initial_rating=1500):
"""Compute Elo ratings from pairwise preferences."""
ratings = defaultdict(lambda: initial_rating)
for pref in preferences:
agent_a = pref["chosen"]["agent"]
agent_b = pref["rejected"]["agent"]
ra = ratings[agent_a]
rb = ratings[agent_b]
# Expected scores
ea = 1.0 / (1.0 + math.pow(10, (rb - ra) / 400))
eb = 1.0 / (1.0 + math.pow(10, (ra - rb) / 400))
overall = pref.get("overall_preference", "A")
if overall == "Tie":
sa, sb = 0.5, 0.5
else:
# "chosen" is the winner
sa, sb = 1.0, 0.0
ratings[agent_a] = ra + k * (sa - ea)
ratings[agent_b] = rb + k * (sb - eb)
return dict(ratings)
with open("training_data/preferences.jsonl") as f:
prefs = [json.loads(line) for line in f]
ratings = compute_elo(prefs)
print("Elo Ratings:")
print("-" * 35)
for agent, rating in sorted(ratings.items(), key=lambda x: -x[1]):
print(f" {agent:<20} {rating:.0f}")Dettaglio per dimensione
Per i confronti multidimensione, guarda su quali dimensioni ciascun agente se la cava meglio:
import json
from collections import defaultdict
with open("training_data/preferences.jsonl") as f:
prefs = [json.loads(line) for line in f]
# Only process multi-dimension annotations
multi_dim = [p for p in prefs if "dimensions" in p]
dim_wins = defaultdict(lambda: defaultdict(lambda: {"A": 0, "B": 0, "Tie": 0}))
for pref in multi_dim:
agent_a = pref["chosen"]["agent"]
agent_b = pref["rejected"]["agent"]
pair_key = f"{agent_a} vs {agent_b}"
for dim_name, dim_data in pref["dimensions"].items():
dim_wins[dim_name][pair_key][dim_data["preference"]] += 1
print("Per-Dimension Win Rates:")
print("=" * 60)
for dim_name, matchups in sorted(dim_wins.items()):
print(f"\n {dim_name.upper()}")
print(f" {'-' * 50}")
for pair, counts in matchups.items():
total = counts["A"] + counts["B"] + counts["Tie"]
a_rate = (counts["A"] + 0.5 * counts["Tie"]) / total * 100
print(f" {pair}: A={a_rate:.0f}% B={100-a_rate:.0f}% "
f"(A:{counts['A']} B:{counts['B']} Tie:{counts['Tie']})")Che cosa funziona nella pratica
Scegliere la modalità
La modalità binaria è la scelta giusta quando vuoi migliaia di preferenze in fretta, un reward model generico o una classifica. Metti in conto all'incirca 1-2 minuti per confronto.
La modalità a scala si ripaga quando l'intensità della preferenza entra nella tua pipeline di addestramento. La DPO con pesatura sul margine tiene conto della differenza tra una preferenza forte (cursore all'estremo) e una debole (cursore vicino al centro). Metti in conto 2-3 minuti per confronto.
La modalità multidimensione vale il tempo in più quando devi sapere dove gli agenti sono forti e dove deboli, quando addestri reward model specifici per dimensione o quando devi consegnare un report dettagliato a chi sviluppa gli agenti. Metti in conto 4-6 minuti per confronto.
Quanti confronti servono
Per tassi di vittoria affidabili raccogli almeno 100 confronti per ogni coppia di agenti. Per punteggi Elo su cinque o più agenti, tra i 200 e i 300 confronti totali portano le classifiche a stabilizzarsi. Per i reward model DPO punta a 1.000 o più coppie di preferenza che coprano sia compiti facili sia difficili.
Randomizzare l'ordine
Imposta sempre randomize_order: true. Il bias di posizione, cioè la tendenza a preferire la trace che compare a sinistra o nella prima scheda, è ben documentato negli studi di valutazione umana. Abbina la randomizzazione al controllo attention_checks.type: "duplicate_reversed" per individuare chi si limita a cliccare sempre dallo stesso lato.
Gestire i pareggi
In modalità binaria consenti i pareggi ma tieni d'occhio la loro frequenza. Se supera il 30%, probabilmente gli agenti sono troppo vicini per una scelta binaria e conviene passare alla modalità a scala o multidimensione. In modalità a scala il pareggio è semplicemente il punto centrale. In modalità multidimensione i pareggi sulle singole dimensioni sono attesi e ti dicono qualcosa.
Nascondere l'identità dell'agente
Tieni show_agent_identity: false a meno che tu non abbia un motivo concreto per mostrarla. Se gli annotatori sanno quale agente ha prodotto una trace, tendono a favorire quello che già si aspettano essere più forte.
Combinare le modalità
Per una valutazione approfondita, esegui prima la modalità binaria su un ampio insieme di coppie per ottenere le classifiche generali, poi la modalità multidimensione su un sottoinsieme più piccolo e stratificato per il dettaglio diagnostico. I confronti binari alimentano l'addestramento del reward model; quelli multidimensione ti dicono dove concentrare i miglioramenti dell'agente.
Per il riferimento di configurazione dietro queste modalità, vedi la documentazione sorgente. Per una panoramica più ampia sulla valutazione degli agenti dall'inizio alla fine, il punto di partenza è la guida alla valutazione degli agenti.