Skip to content
Guides13 min read

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.

Potato Team

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:

Oberfläche für den Agentenvergleich nebeneinanderAnnotatoren 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

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

Oberfläche zur Auswahl der paarweisen PräferenzVerfü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

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

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

Modus 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

yaml
# 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

json
{
  "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:

bash
# 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 3

Vergleichsdaten 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:

bash
potato export \
  --format dpo_preferences \
  --project ./output/ \
  --output ./training_data/preferences.jsonl

Im binären Modus ist die Ausgabe schlicht:

json
{
  "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:

json
{
  "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:

json
{
  "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

python
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

python
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:

python
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.