KI-Agenten nebeneinander vergleichen: die Modi Binär, Skala und Mehrdimensional
Paarweisen Agentenvergleich in Potato einrichten, mit drei Modi: binäre Präferenz, kontinuierliche Skala und mehrdimensionales Urteil je Kriterium mit verpflichtender Begründung.
Warum paarweiser Vergleich bei der Agentenevaluierung
Wer jemanden bittet, den Trace eines Coding-Agenten auf einer Skala von 1 bis 10 zu bewerten, bekommt verrauschte Daten, weil jeder diese Skala anders kalibriert. Die 7 des einen Annotators ist die 5 des anderen. Der paarweise Vergleich umgeht das. Statt Traces einzeln zu bewerten, sehen sich Annotatoren zwei davon nebeneinander an und sagen, welcher besser ist. Dieses direkte Gegenüberstellen fällt leichter, ist über Personen hinweg konsistenter und ist zufällig genau das, was man für Direct Preference Optimization (DPO) und Reinforcement Learning from Human Feedback (RLHF) braucht.
Es ist dasselbe Vorgehen, mit dem Reward-Modelle für das Alignment von Sprachmodellen trainiert werden, und es lässt sich sauber auf Coding-Agenten übertragen: menschliche Präferenzen zwischen Paaren von Agenten-Trajektorien sammeln, darauf ein Reward-Modell trainieren und dieses Modell dann zur Steuerung des Agententrainings oder zur Auswahl des besten von N Kandidaten zur Inferenzzeit nutzen.
Potato hat drei Modi für den paarweisen Vergleich, jeder passend für einen anderen Evaluierungsbedarf und ein anderes Datenbudget.
Die Oberfläche stellt zwei Traces nebeneinander:
Annotatoren vergleichen zwei Agenten-Traces und wählen aus, welches Vorgehen besser war
Modus 1: binäre Präferenz
Das ist der einfachste und schnellste Modus. Der Annotator sieht zwei Traces nebeneinander und klickt den besseren an. Eine optionale Unentschieden-Schaltfläche deckt die Fälle ab, in denen beide gleich gut oder gleich schlecht sind.
Wann der binäre Modus passt
Der binäre Modus ist die Wahl, wenn schnell viele Präferenzdaten gebraucht werden. Er eignet sich für das Training einfacher Reward-Modelle, für die Bestimmung von Siegquoten und für Elo-Ranglisten. Der Preis dafür sind die Nuancen: Man erfährt, welcher Trace gewonnen hat, aber nicht, wie deutlich und in welcher Hinsicht.
Konfiguration
# 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"Der Annotationsablauf
Der Annotator bekommt einen geteilten Bildschirm. Links wird Trace A mit dem vollen CodingTraceDisplay gerendert: Diffs, Terminal-Blöcke, Datei-Lesevorgänge, Überlegungen. Rechts steht Trace B zur selben Aufgabe. Jede Seite scrollt für sich.
Die Aufgabenbeschreibung steht über beiden Traces, damit der Annotator weiß, was die beiden Agenten erreichen sollten.
Darunter liegen drei Schaltflächen: „Agent A is better", „Agent B is better" und „Tie". Mit aktiviertem randomize_order wird pro Element gemischt, welcher Agent A und welcher B ist, sodass Annotatoren sich keine Links- oder Rechts-Gewohnheit angewöhnen können.
Für eine feiner aufgelöste Evaluierung unterstützt die Oberfläche auch mehrere Dimensionen:
Verfügbar sind die Modi binäre Präferenz, kontinuierliche Skala und mehrdimensional
Modus 2: kontinuierliche Skala
Im Skalenmodus kann der Annotator angeben, wie viel besser ein Trace ist, und nicht nur, welcher gewonnen hat. Statt eines einzelnen Klicks zieht er einen Schieberegler, der links bei „A much better" beginnt, rechts bei „B much better" endet und in der Mitte „Equal" hat.
Wann der Skalenmodus passt
Nehmen Sie den Skalenmodus, wenn die Stärke einer Präferenz zählt und nicht nur ihre Richtung. Ein Regler nahe am Rand bedeutet einen klaren Qualitätsunterschied, nahe der Mitte bedeutet, dass die beiden dicht beieinander lagen. DPO und ähnliche Pipelines können Beispiele nach dieser Stärke gewichten und sich stärker auf die eindeutigen Fälle stützen.
Konfiguration
# 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"Eine 5-Punkte-Skala verwenden
Für schnellere Annotation bei etwas gröberer Auflösung reicht eine 5-Punkte-Skala:
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: 3Modus 3: mehrdimensionaler Vergleich
Das ist der detaillierteste Modus. Statt einer einzigen Gesamtpräferenz beurteilt der Annotator jeden Trace auf mehreren unabhängigen Dimensionen. Jede Dimension bekommt ihr eigenes A/B/Unentschieden, und jede Entscheidung braucht eine schriftliche Begründung.
Wann der mehrdimensionale Modus passt
Nehmen Sie ihn, wenn Sie nicht nur wissen wollen, welcher Agent gewonnen hat, sondern warum. Ein Trace kann korrekten Code und miserable Effizienz haben, ein anderer effizient sein und dabei einen Randfall übersehen. Aus den Daten je Dimension lassen sich dimensionsspezifische Reward-Modelle trainieren, oder sie gehen als detaillierte Rückmeldung an die Leute, die den Agenten bauen.
Konfiguration
# 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"Gepaarte Trace-Daten vorbereiten
Alle drei Modi erwarten gepaarte Traces als Eingabe. Jede Zeile der JSONL-Datei enthält zwei Traces, die dieselbe Aufgabe bearbeitet haben.
Datenformat
{
"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
}
]
}
}Paare aus einzelnen Traces bilden
Wenn Sie einzelne Traces haben, die alle dieselben Aufgaben bearbeitet haben, stellt das Paarungswerkzeug die Paare zusammen:
# 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 3Vergleichsdaten exportieren
Präferenzpaare für DPO/RLHF
Das wichtigste Exportformat für paarweise Vergleiche sind Präferenzpaare für das Training mit DPO oder RLHF:
potato export \
--format dpo_preferences \
--project ./output/ \
--output ./training_data/preferences.jsonlIm binären Modus ist die Ausgabe schlicht:
{
"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"
}Der Skalenmodus ergänzt die Präferenzstärke:
{
"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"
}Der mehrdimensionale Modus führt die Präferenzen je Dimension mit:
{
"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
}Auswertung: Siegquoten, Elo-Wertungen und Aufschlüsselung nach Dimensionen
Siegquoten berechnen
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']})")Elo-Wertungen berechnen
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}")Aufschlüsselung nach Dimensionen
Sehen Sie bei mehrdimensionalen Vergleichen nach, auf welchen Dimensionen jeder Agent gut abschneidet:
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']})")Was sich in der Praxis bewährt
Den passenden Modus wählen
Der binäre Modus ist richtig, wenn Sie schnell Tausende von Präferenzen brauchen, ein allgemeines Reward-Modell trainieren wollen oder eine Rangliste erstellen. Rechnen Sie mit etwa 1 bis 2 Minuten pro Vergleich.
Der Skalenmodus lohnt sich, wenn die Präferenzstärke in Ihre Trainingspipeline einfließt. DPO mit Margin-Gewichtung interessiert sich für den Unterschied zwischen einer starken Präferenz (Regler am Rand) und einer schwachen (Regler nahe der Mitte). Rechnen Sie mit 2 bis 3 Minuten pro Vergleich.
Der mehrdimensionale Modus ist den Mehraufwand wert, wenn Sie wissen müssen, wo Agenten stark und wo sie schwach sind, wenn Sie dimensionsspezifische Reward-Modelle trainieren oder wenn Sie den Agentenentwicklern einen detaillierten Bericht schuldig sind. Rechnen Sie mit 4 bis 6 Minuten pro Vergleich.
Wie viele Vergleiche nötig sind
Für belastbare Siegquoten sollten Sie mindestens 100 Vergleiche pro Agentenpaar sammeln. Für Elo-Wertungen über fünf oder mehr Agenten pendeln sich die Ränge nach 200 bis 300 Vergleichen insgesamt ein. Für DPO-Reward-Modelle sollten es 1.000 oder mehr Präferenzpaare sein, die leichte wie schwere Aufgaben abdecken.
Die Reihenfolge randomisieren
Setzen Sie immer randomize_order: true. Positionsbias, also die Neigung, den links oder im ersten Tab gezeigten Trace zu bevorzugen, ist in Studien zur menschlichen Evaluierung gut belegt. Kombinieren Sie die Randomisierung mit der Prüfung attention_checks.type: "duplicate_reversed", um Leute zu erwischen, die einfach immer dieselbe Seite anklicken.
Mit Unentschieden umgehen
Erlauben Sie im binären Modus Unentschieden, behalten Sie aber deren Anteil im Blick. Steigt er über 30 %, liegen die Agenten für eine binäre Entscheidung vermutlich zu dicht beieinander, und Sie sollten auf den Skalen- oder den mehrdimensionalen Modus wechseln. Im Skalenmodus ist das Unentschieden schlicht der Mittelpunkt. Im mehrdimensionalen Modus sind Unentschieden auf einzelnen Dimensionen zu erwarten und selbst eine Aussage.
Die Identität des Agenten verbergen
Lassen Sie show_agent_identity: false, sofern Sie keinen guten Grund für das Gegenteil haben. Wenn Annotatoren wissen, von welchem Agenten ein Trace stammt, bevorzugen sie meist den, den sie ohnehin für stärker halten.
Modi kombinieren
Für eine gründliche Evaluierung laufen Sie zuerst den binären Modus über eine große Menge Paare, um die Gesamtränge zu bekommen, und danach den mehrdimensionalen Modus über eine kleinere, geschichtete Teilmenge für die diagnostischen Details. Die binären Vergleiche speisen das Training des Reward-Modells, die mehrdimensionalen sagen Ihnen, wo Sie den Agenten verbessern sollten.
Die Konfigurationsreferenz zu diesen Modi steht in der Quelldokumentation. Für einen breiteren Durchgang durch die Evaluierung von Agenten von Anfang bis Ende ist der Leitfaden zur Agentenevaluierung der richtige Einstieg.