مقارنة وكلاء الذكاء الاصطناعي جنباً إلى جنب: الوضع الثنائي والمقياس والأبعاد المتعددة
إعداد المقارنة الزوجية للوكلاء في Potato بثلاثة أوضاع: التفضيل الثنائي، والمقياس المتصل، والحكم متعدد المعايير لكل بُعد مع تبرير إلزامي.
لماذا المقارنة الزوجية في تقييم الوكلاء
إذا طلبت من أحدهم أن يقيّم تتبع وكيل برمجة على مقياس من 1 إلى 10، فستحصل على بيانات مشوّشة، لأن كل شخص يعاير ذلك المقياس على نحو مختلف. فما هو 7 عند معلّق يكون 5 عند آخر. والمقارنة الزوجية تتجاوز هذا. فبدل تقييم التتبعات كلٌّ على حدة، ينظر المعلّقون إلى تتبعين جنباً إلى جنب ويقولون أيهما أفضل. هذا النوع من الحكم المباشر أسهل على المعلّق، وأكثر اتساقاً بين الأشخاص، وهو بالضبط ما تحتاجه لتحسين التفضيل المباشر (DPO) والتعلم المعزَّز من التغذية الراجعة البشرية (RLHF).
وهو الأسلوب نفسه المستخدم في تدريب نماذج المكافأة لمواءمة نماذج اللغة، وينتقل إلى وكلاء البرمجة بسلاسة: اجمع تفضيلات بشرية بين أزواج من مسارات الوكلاء، ودرّب عليها نموذج مكافأة، ثم استخدم ذلك النموذج لتوجيه تدريب الوكيل أو لاختيار أفضل مرشح من بين N عند الاستدلال.
في Potato ثلاثة أوضاع للمقارنة الزوجية، يناسب كلٌّ منها حاجة تقييم مختلفة وميزانية بيانات مختلفة.
تضع الواجهة التتبعين جنباً إلى جنب:
Annotators compare two agent traces and select which approach was better
الوضع 1: التفضيل الثنائي
هذا أبسط الأوضاع وأسرعها. يرى المعلّق تتبعين جنباً إلى جنب فينقر على الأفضل. وثمة زر تعادل اختياري يغطي الحالات التي يتساوى فيها التتبعان جودةً أو رداءةً.
متى تستخدم الوضع الثنائي
استخدم الوضع الثنائي حين تحتاج إلى كمّ كبير من بيانات التفضيل بسرعة. وهو مناسب لتدريب نماذج مكافأة أساسية، وحساب معدلات فوز الوكلاء، وبناء لوحات صدارة بنظام 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: trueسير عمل التعليق التوضيحي
يحصل المعلّق على شاشة مقسومة. في أحد الجانبين يُعرَض التتبع A بكامل عرض تتبع البرمجة: diffs وكتل الطرفية وقراءات الملفات والتفكير. وفي الجانب الآخر التتبع B للمهمة نفسها. ولكل جانب تمرير مستقل.
ويقع وصف المهمة فوق التتبعين معاً حتى يعرف المعلّق ما الذي كان الوكيلان يحاولان فعله.
وتحتهما ثلاثة أزرار: «Agent A is better» و«Agent B is better» و«Tie». ومع تفعيل randomize_order، يُخلط أي وكيل هو A وأيهما B في كل عنصر، فلا يقع المعلّقون في عادة اختيار جانب بعينه.
ولتقييم أدق، تدعم الواجهة أيضاً أبعاداً متعددة:
Binary preference, continuous scale, and multi-dimension modes are available
الوضع 2: المقياس المتصل
يتيح وضع المقياس للمعلّق أن يقول كم يتفوق أحد التتبعين، لا أيهما فاز فقط. فبدل نقرة واحدة، يسحب شريط تمرير يمتد من «A أفضل بكثير» في طرف إلى «B أفضل بكثير» في الطرف الآخر، مع «متساويان» في الوسط.
متى تستخدم وضع المقياس
استخدم وضع المقياس حين تهمك قوة التفضيل لا اتجاهه فحسب. فوجود المؤشر قرب أحد الطرفين يعني فجوة جودة واضحة، ووجوده قرب المنتصف يعني أن الاثنين متقاربان. ويمكن لخطوط أنابيب 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استخدام مقياس من 5 نقاط
لتعليق توضيحي أسرع وتفصيل أقل قليلاً، انزل إلى مقياس من 5 نقاط:
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?"الوضع 3: المقارنة متعددة الأبعاد
هذا أكثر الأوضاع تفصيلاً. فبدل تفضيل كلي واحد، يحكم المعلّق على كل تتبع في عدة أبعاد مستقلة. ولكل بُعد حكمه الخاص بين A وB والتعادل، ولكل حكم تبرير مكتوب إلزامي.
متى تستخدم الوضع متعدد الأبعاد
استخدمه حين تريد أن تعرف لماذا فاز وكيل، لا أيهما فاز فقط. فقد يكون أحد التتبعين صحيح الكود سيئ الكفاءة، وقد يكون الآخر فعّالاً لكنه يغفل حالة حدّية. والبيانات لكل بُعد الخارجة من هذا يمكن أن تدرّب نماذج مكافأة خاصة بكل بُعد، أو أن تُعيد ملاحظات تفصيلية إلى من يبنون الوكيل.
التهيئة
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تجهيز بيانات التتبعات المزدوجة
تأخذ الأوضاع الثلاثة كلها تتبعات مزدوجة كمُدخَل. ويحمل كل سطر في ملف JSONL تتبعين حاولا المهمة نفسها.
صيغة البيانات
{
"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
}
]
}
}بناء الأزواج من تتبعات مفردة
إذا كانت لديك تتبعات مفردة تناولت جميعها المهام نفسها، فستتولى أداة الاقتران تجميعها:
# 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تصدير بيانات المقارنة
أزواج التفضيل لـ DPO/RLHF
صيغة التصدير الأساسية للمقارنات الزوجية هي أزواج تفضيل لتدريب DPO أو RLHF:
potato export \
--format dpo_preferences \
--project ./output/ \
--output ./training_data/preferences.jsonlفي الوضع الثنائي يكون المخرَج بسيطاً:
{
"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"
}ويضيف وضع المقياس قوة التفضيل:
{
"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"
}ويحمل الوضع متعدد الأبعاد التفضيلات لكل بُعد:
{
"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
}التحليل: معدلات الفوز وتصنيفات Elo والتفصيل لكل بُعد
حساب معدلات الفوز
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
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}")التفصيل لكل بُعد
في المقارنات متعددة الأبعاد، انظر في أي الأبعاد يبلي كل وكيل بلاءً حسناً:
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']})")ما الذي ينفع عملياً
اختيار الوضع
الوضع الثنائي هو الخيار الصحيح حين تريد آلاف التفضيلات بسرعة، أو نموذج مكافأة عام الغرض، أو ترتيباً على لوحة صدارة. احسب نحو دقيقة إلى دقيقتين لكل مقارنة.
ويثبت وضع المقياس جدواه حين تغذّي قوة التفضيل خط أنابيب التدريب لديك. فـ DPO مع ترجيح الهامش يهتم بالفرق بين تفضيل قوي (المؤشر عند الطرف) وتفضيل ضعيف (المؤشر قرب المنتصف). احسب من دقيقتين إلى ثلاث لكل مقارنة.
أما الوضع متعدد الأبعاد فيستحق الوقت الإضافي حين تحتاج أن تعرف مواطن قوة الوكلاء وضعفها، أو حين تدرّب نماذج مكافأة خاصة بكل بُعد، أو حين تدين لمطوّري الوكيل بتقرير تفصيلي. احسب من 4 إلى 6 دقائق لكل مقارنة.
كم مقارنة تحتاج
للحصول على معدلات فوز موثوقة، اجمع 100 مقارنة على الأقل لكل زوج وكلاء. ولتصنيفات Elo عبر خمسة وكلاء أو أكثر، تستقر الترتيبات عند 200 إلى 300 مقارنة إجمالاً. ولنماذج مكافأة DPO، استهدف 1000 زوج تفضيل أو أكثر تغطي المهام السهلة والصعبة على السواء.
عشوائية الترتيب
اضبط randomize_order: true دائماً. فتحيّز الموضع، أي الميل إلى تفضيل التتبع الذي يظهر في جانب بعينه أو في التبويب الأول، موثَّق جيداً في دراسات التقييم البشري. واقرن العشوائية بفحص attention_checks.type: "duplicate_reversed" لكشف من يواصل النقر على الجانب نفسه.
التعامل مع التعادل
في الوضع الثنائي، اسمح بالتعادل لكن راقب نسبته. فإن تجاوزت 30%، فالأرجح أن الوكيلين أقرب من أن يفصل بينهما حكم ثنائي، وعليك الانتقال إلى وضع المقياس أو الوضع متعدد الأبعاد. وفي وضع المقياس، التعادل هو نقطة المنتصف ليس إلا. وفي الوضع متعدد الأبعاد، التعادل في أبعاد مفردة متوقَّع ويخبرك بشيء.
إخفاء هوية الوكيل
أبقِ show_agent_identity: false ما لم يكن لديك سبب حقيقي لإظهارها. فإذا عرف المعلّقون أي وكيل أنتج تتبعاً، مالوا إلى تفضيل الوكيل الذي يتوقعون سلفاً أنه الأقوى.
الجمع بين الأوضاع
للحصول على تقييم دقيق، شغّل الوضع الثنائي أولاً على مجموعة كبيرة من الأزواج للحصول على الترتيب العام، ثم شغّل الوضع متعدد الأبعاد على مجموعة فرعية أصغر ومُطبَّقة للحصول على التفاصيل التشخيصية. المقارنات الثنائية تغذّي تدريب نموذج المكافأة؛ والمقارنات متعددة الأبعاد تخبرك أين تركّز تحسينات الوكيل.
للاطلاع على مرجع التهيئة وراء هذه الأوضاع، انظر الوثائق المصدرية. ولشرح أوسع لتقييم الوكلاء من البداية إلى النهاية، ابدأ من دليل تقييم الوكلاء.