Skip to content
Guides14 min read

Comparer des agents IA côte à côte : modes binaire, échelle et multidimension

Mettez en place la comparaison d'agents par paires dans Potato avec trois modes : préférence binaire, échelle continue et jugement multicritère par dimension avec justification obligatoire.

Potato Team

Pourquoi la comparaison par paires pour évaluer des agents

Demander à quelqu'un de noter la trace d'un agent de coding sur une échelle de 1 à 10 produit des données bruitées, parce que chacun calibre cette échelle à sa façon. Le 7 d'un annotateur est le 5 d'un autre. La comparaison par paires contourne le problème. Au lieu de noter les traces isolément, les annotateurs en regardent deux côte à côte et disent laquelle est la meilleure. Ce jugement en tête-à-tête est plus facile à rendre, plus stable d'une personne à l'autre, et c'est exactement ce dont vous avez besoin pour le Direct Preference Optimization (DPO) et le Reinforcement Learning from Human Feedback (RLHF).

C'est l'approche employée pour entraîner les modèles de récompense servant à l'alignement des modèles de langage, et elle se transpose sans peine aux agents de coding : recueillir des préférences humaines entre paires de trajectoires d'agents, entraîner un modèle de récompense dessus, puis se servir de ce modèle pour guider l'entraînement de l'agent ou choisir le meilleur de N candidats au moment de l'inférence.

Potato propose trois modes de comparaison par paires, chacun adapté à un besoin d'évaluation et à un budget de données différents.

L'interface place deux traces côte à côte :

Interface de comparaison d'agents côte à côteLes annotateurs comparent deux traces d'agent et choisissent la meilleure démarche

Mode 1 : préférence binaire

C'est le mode le plus simple et le plus rapide. L'annotateur voit deux traces côte à côte et clique sur la meilleure. Un bouton d'égalité optionnel couvre les cas où les deux se valent, en bien comme en mal.

Quand utiliser le mode binaire

Choisissez le mode binaire quand il vous faut beaucoup de données de préférence rapidement. Il convient pour entraîner des modèles de récompense simples, calculer des taux de victoire entre agents et construire des classements Elo. En contrepartie, vous perdez la nuance : vous savez quelle trace l'a emporté, mais pas de combien ni sur quels aspects.

Configuration

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"

Le déroulé de l'annotation

L'annotateur obtient un écran scindé en deux. À gauche, la trace A s'affiche avec le CodingTraceDisplay complet : diffs, blocs de terminal, lectures de fichiers, raisonnement. À droite, la trace B pour la même tâche. Chaque côté défile indépendamment.

La description de la tâche figure au-dessus des deux traces, pour que l'annotateur sache ce que les deux agents cherchaient à faire.

En dessous, trois boutons : « Agent A is better », « Agent B is better » et « Tie ». Avec randomize_order activé, l'attribution des rôles A et B est tirée au sort à chaque élément, ce qui empêche les annotateurs de prendre l'habitude de cliquer toujours à gauche ou toujours à droite.

Pour une évaluation plus fine, l'interface gère aussi plusieurs dimensions :

Interface de sélection de préférence par pairesLes modes préférence binaire, échelle continue et multidimension sont disponibles

Mode 2 : échelle continue

Le mode échelle permet à l'annotateur d'indiquer de combien une trace est meilleure, et pas seulement laquelle l'emporte. Au lieu d'un simple clic, il déplace un curseur qui va de « A much better » à gauche à « B much better » à droite, avec « Equal » au centre.

Quand utiliser le mode échelle

Utilisez le mode échelle quand l'intensité de la préférence compte, pas seulement son sens. Un curseur proche d'une extrémité signale un écart de qualité net ; proche du centre, les deux traces se valaient presque. Le DPO et les chaînes de traitement voisines savent pondérer les exemples par cette intensité, en s'appuyant davantage sur les cas tranchés.

Configuration

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"

Utiliser une échelle à 5 points

Pour annoter plus vite, au prix d'un peu de granularité, descendez à une échelle à 5 points :

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

Mode 3 : comparaison multidimension

C'est le mode le plus détaillé. Plutôt qu'une préférence globale, l'annotateur juge les traces sur plusieurs dimensions indépendantes. Chaque dimension reçoit son propre verdict A/B/égalité, et chaque verdict doit être justifié par écrit.

Quand utiliser le mode multidimension

Utilisez-le quand vous voulez savoir non seulement quel agent l'a emporté, mais pourquoi. Une trace peut avoir du code correct et une efficacité déplorable ; une autre être efficace mais passer à côté d'un cas limite. Les données par dimension qui en sortent servent à entraîner des modèles de récompense spécialisés par dimension, ou à remonter un compte rendu détaillé aux personnes qui développent l'agent.

Configuration

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"

Préparer les données de traces appariées

Les trois modes prennent des traces appariées en entrée. Chaque ligne du fichier JSONL contient deux traces qui ont tenté la même tâche.

Format des données

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
      }
    ]
  }
}

Constituer des paires à partir de traces individuelles

Si vous disposez de traces individuelles portant toutes sur les mêmes tâches, l'utilitaire d'appariement les assemble :

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

Exporter les données de comparaison

Paires de préférence DPO/RLHF

Le principal format d'export des comparaisons par paires est celui des paires de préférence pour l'entraînement DPO ou RLHF :

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

En mode binaire, la sortie est simple :

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"
}

Le mode échelle ajoute l'intensité de la préférence :

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"
}

Le mode multidimension transporte les préférences par dimension :

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
}

Analyse : taux de victoire, scores Elo et détail par dimension

Calculer les taux de victoire

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']})")

Calculer les scores Elo

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}")

Détail par dimension

Pour les comparaisons multidimension, regardez sur quelles dimensions chaque agent se distingue :

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']})")

Ce qui marche en pratique

Choisir un mode

Le mode binaire s'impose quand vous voulez des milliers de préférences rapidement, un modèle de récompense généraliste ou un classement. Comptez environ 1 à 2 minutes par comparaison.

Le mode échelle se justifie quand l'intensité de la préférence alimente votre chaîne d'entraînement. Le DPO avec pondération par marge tient compte de la différence entre une préférence forte (curseur à l'extrémité) et une préférence faible (curseur près du centre). Comptez 2 à 3 minutes par comparaison.

Le mode multidimension vaut le temps supplémentaire quand vous devez savoir où les agents sont forts et où ils sont faibles, quand vous entraînez des modèles de récompense par dimension, ou quand vous devez un rapport détaillé aux développeurs de l'agent. Comptez 4 à 6 minutes par comparaison.

Combien de comparaisons il vous faut

Pour des taux de victoire fiables, recueillez au moins 100 comparaisons par paire d'agents. Pour des scores Elo sur cinq agents ou plus, 200 à 300 comparaisons au total suffisent à stabiliser le classement. Pour des modèles de récompense DPO, visez 1 000 paires de préférence ou plus, couvrant aussi bien des tâches faciles que difficiles.

Randomiser l'ordre

Mettez toujours randomize_order: true. Le biais de position, c'est-à-dire la tendance à préférer la trace qui apparaît à gauche ou dans le premier onglet, est bien documenté dans les études d'évaluation humaine. Associez cette randomisation au contrôle attention_checks.type: "duplicate_reversed" pour repérer ceux qui se contentent de cliquer toujours du même côté.

Gérer les égalités

En mode binaire, autorisez les égalités mais surveillez leur taux. S'il dépasse 30 %, les agents sont sans doute trop proches pour un verdict binaire et vous devriez passer au mode échelle ou multidimension. En mode échelle, l'égalité n'est que le point central. En mode multidimension, les égalités sur des dimensions isolées sont attendues et porteuses d'information.

Masquer l'identité des agents

Gardez show_agent_identity: false sauf raison sérieuse d'afficher l'information. Si les annotateurs savent quel agent a produit une trace, ils ont tendance à favoriser celui qu'ils s'attendent déjà à trouver meilleur.

Combiner les modes

Pour une évaluation complète, passez d'abord en mode binaire sur un grand ensemble de paires afin d'obtenir un classement d'ensemble, puis en mode multidimension sur un sous-ensemble stratifié plus petit pour le détail diagnostique. Les comparaisons binaires alimentent l'entraînement du modèle de récompense ; les comparaisons multidimension vous disent où concentrer les améliorations de l'agent.

Pour la référence de configuration de ces modes, voir la documentation source. Pour un parcours plus large de l'évaluation d'agents de bout en bout, commencez par le guide d'évaluation des agents.