كيف تجمع بيانات مكافأة العملية لتدريب وكلاء برمجة أفضل
دليل عملي خطوة بخطوة لجمع إشارات المكافأة لكل خطوة من أجل تدريب PRM باستخدام Potato. يغطي وضع الخطأ الأول، والتعليق لكل خطوة، والتصدير إلى خطوط التدريب.
ما نماذج مكافأة العملية؟
طريقتان لتوسيم مكافآت العملية
لا تنظر نماذج مكافأة النتيجة (ORMs) إلا إلى نهاية مسار وكيل البرمجة: هل تُرجم الكود؟ هل نجحت الاختبارات؟ هل حُلّت المشكلة؟ أما نماذج مكافأة العملية (PRMs) فتعطي درجة لكل خطوة وسيطة بدل ذلك. ومع إشارة مكافأة عند كل خطوة، تستطيع طرق التدريب تحديد موضع خطأ الوكيل بدقة، وهذا يجعل التعلّم أكفأ في استهلاك العينات ويساعد على التعميم.
دفعت أعمال حديثة هذا الاتجاه. يعيد AgentPRM تعريف مكافآت العملية لمهام الوكلاء، فيقيّم كل إجراء بمقدار التقدّم الذي يحرزه نحو الهدف بدلاً من صوابه، ويذكر كفاءة حوسبية أفضل بأكثر من 8 أضعاف مقارنةً بالأسس التي قارن بها. ووجد ToolRM أن نماذج المكافأة المدرَّبة على مخرجات لغوية تحكم على استدعاءات الأدوات حكماً سيئاً، فبنى نماذج مكافأة خاصة بالأدوات إلى جانب FC-RewardBench لتقييمها. وفي المقابل، يدرّب DeepSWE وكيل برمجة بمكافأة نتيجة متفرّقة وحدها، أي هل تنجح الاختبارات، ويبلغ 42.2% في Pass@1 و59% مع توسيع الحوسبة وقت الاختبار على SWE-bench Verified. هذا هو الإعداد القائم على النتيجة وحدها الذي تحاول مراقبة العملية تحسينه.
وما تحتاج إليه هذه الأعمال كلها هو تعليق بشري جيد على مستوى الخطوة، وهذا عادةً هو عنق الزجاجة. ومخططات مكافأة العملية في Potato مبنية لتسريع جمع تلك البيانات. للاطلاع على المخطط الأساسي، انظر وثائق تقييم المسار، ولتفاصيل إدخال التتبعات، وثائق تتبعات الوكلاء.
وضعا التعليق التوضيحي
في Potato وضعان لتعليق PRM يقايضان السرعة بالتفصيل. اختر ما يناسب ميزانية بياناتك وأهدافك.
وضع الخطأ الأول
في وضع الخطأ الأول، يقرأ المعلّق المسار من أعلاه إلى أسفله وينقر على أول خطوة أخطأ فيها الوكيل. ثم يعلّم Potato كل خطوة سابقة بأنها صحيحة، وكل خطوة من المنقور عليها فصاعداً بأنها خاطئة.
وهذا سريع لأن المعلّق لا يحتاج إلا إلى إيجاد نقطة قرار واحدة. وهو يعمل جيداً حين تتوالى الأخطاء، أي حين يندر أن يتعافى الوكيل بعد خروجه عن المسار، وهذه هي الحالة الشائعة عملياً.
annotation_schemes:
- annotation_type: process_reward
name: prm_first_error
mode: "first_error"
description: >
Review the agent's steps from top to bottom. Click on the
first step where the agent makes a mistake. All steps before
your selection will be marked correct; all steps after
(including the selected step) will be marked incorrect.يجري سير عمل التعليق بوضع الخطأ الأول هكذا:
- يفتح المعلّق تتبعاً فيرى كل الخطوات مصيَّرة بمكوّن CodingTraceDisplay.
- يقرأ الخطوات بالتسلسل، ويفحص الـ diffs ومخرجات الطرفية وخطوات التفكير.
- حين يجد أول خطوة خاطئة، ينقر على علامة الخطأ بجانبها.
- تتحول الخطوات من 0 إلى N-1 إلى الأخضر (صحيحة)، وتتحول الخطوات من N إلى النهاية إلى الأحمر (خاطئة).
- يراجع المعلّق التوسيم التلقائي وينقر على Submit للتأكيد.
إن كان التتبع كله صحيحاً (حلّ الوكيل المهمة على أتمّ وجه)، ينقر المعلّق على All Correct. وإن كانت الخطوة الأولى نفسها خاطئة، فينقر على الخطوة 0 أو يستخدم All Incorrect.
وهذه واجهة تعليق PRM أثناء العمل:
في وضع الخطأ الأول، انقر على أول خطوة خاطئة فتُوسم كل الخطوات التالية تلقائياً
وضع كل خطوة
في وضع كل خطوة، تنال كل خطوة وسمها الخاص. وهذا ينتج بيانات أغنى، لأنه يلتقط الحالات التي يتعافى فيها الوكيل جزئياً من خطأ، أو يسلك التفافاً غير ضار لكنه زائد، أو يخطو خطوة سليمة بذاتها لكنها خاطئة في سياقها.
annotation_schemes:
- annotation_type: process_reward
name: prm_per_step
mode: "per_step"إعداد مشروع تعليق PRM
الخطوة 1: جهّز بيانات التتبع
ينبغي أن تكون بيانات دخلك ملف JSONL يمثل كل سطر فيه كائن JSON يحتوي مسار الوكيل. والحقول الأساسية هي id وissue_description وstructured_turns:
{
"id": "trace_001",
"issue_description": "Fix the TypeError in parse_config() when config file is empty",
"repo": "myorg/myproject",
"base_commit": "a1b2c3d",
"structured_turns": [
{
"step_idx": 0,
"type": "thinking",
"content": "I need to understand what parse_config does and where the TypeError occurs. Let me read the file first."
},
{
"step_idx": 1,
"type": "file_read",
"path": "src/config.py",
"content": "def parse_config(path):\n with open(path) as f:\n data = json.load(f)\n return data['settings']",
"start_line": 1,
"end_line": 4
},
{
"step_idx": 2,
"type": "thinking",
"content": "The bug is on line 4. When the file is empty, json.load returns None, and None['settings'] raises TypeError. I should add a check."
},
{
"step_idx": 3,
"type": "file_edit",
"path": "src/config.py",
"diff": "--- a/src/config.py\n+++ b/src/config.py\n@@ -1,4 +1,6 @@\n def parse_config(path):\n with open(path) as f:\n data = json.load(f)\n+ if data is None:\n+ return {}\n return data['settings']"
},
{
"step_idx": 4,
"type": "bash_command",
"command": "python -m pytest tests/test_config.py -v",
"output": "tests/test_config.py::test_parse_config_empty PASSED\ntests/test_config.py::test_parse_config_valid PASSED\n\n2 passed in 0.12s",
"exit_code": 0
}
]
}وإن كنت تحوّل من صيغة وكيل موجودة، فاستخدم أداة تحويل التتبعات:
# Convert Claude Code traces
potato convert-traces \
--format claude_code \
--input ./raw_traces/ \
--output ./data/traces.jsonl
# Convert SWE-Agent trajectories
potato convert-traces \
--format swe_agent \
--input ./swe_agent_output/ \
--output ./data/traces.jsonlيصيّر Potato تتبعات وكلاء البرمجة مع إبراز سليم للـ diff:
فروق الكود ومخرجات الطرفية وقراءات الملفات تُصيَّر مع إبراز البنية اللغوية
الخطوة 2: أنشئ تهيئتك
هذه تهيئة مشروع كاملة لتعليق PRM بوضع الخطأ الأول:
# config.yaml
project_name: "PRM Data Collection - SWE-bench Traces"
port: 8000
data:
source: "local"
input_path: "./data/traces.jsonl"
data_format: "coding_trace"
coding_agent:
display:
diff_style: "unified"
context_lines: 3
syntax_highlighting: true
terminal_theme: "dark"
file_tree:
enabled: true
position: "left"
collapsible:
auto_collapse_thinking: true
auto_collapse_long_output: true
long_output_threshold: 50
annotation_schemes:
- annotation_type: process_reward
name: step_reward
mode: "first_error"
description: >
Review the agent's trajectory step by step. Click the first
step where the agent makes an error. If the entire trajectory
is correct, click "All Correct."
- annotation_type: radio
name: outcome
labels:
- value: "resolved"
text: "Fully Resolved"
- value: "partial"
text: "Partially Resolved"
- value: "not_resolved"
text: "Not Resolved"
- annotation_type: text
name: error_description
description: "If incorrect, briefly describe the error"
placeholder: "e.g., Agent edited the wrong file..."
output:
path: "./output/"
format: "jsonl"
quality_control:
inter_annotator_agreement: true
overlap_percentage: 15
minimum_time_per_instance: 20
annotators:
- username: "reviewer1"
- username: "reviewer2"
- username: "reviewer3"الخطوة 3: شغّل خادم التعليق
# Start the annotation server
potato start config.yaml -p 8000
# Or run in the background
nohup potato start config.yaml -p 8000 > potato.log 2>&1 &انتقل إلى http://localhost:8000، وسجّل الدخول بأحد حسابات المعلّقين المهيّأة، وابدأ مراجعة التتبعات.
الخطوة 4: تابع التقدّم
أثناء سير التعليق، تابع التقدّم ودرجة الاتفاق:
# Check annotation progress
potato status config.yaml
# View inter-annotator agreement
potato agreement config.yaml --metric krippendorff_alphaالتصدير إلى صيغ التدريب
بعد اكتمال التعليق، صدّر البيانات بالصيغة التي يتوقعها خط التدريب عندك.
صيغة PRM لتدريب نموذج المكافأة
تنتج صيغة تصدير PRM كائن JSON واحداً لكل تتبع مع وسوم على مستوى الخطوة:
potato export \
--format prm \
--project ./output/ \
--output ./training_data/prm_labels.jsonlوالمخرجات تبدو هكذا:
{
"trace_id": "trace_001",
"issue_description": "Fix the TypeError in parse_config() when config file is empty",
"total_steps": 5,
"first_error_step": null,
"all_correct": true,
"steps": [
{"step_idx": 0, "type": "thinking", "label": "correct", "reward": 1.0},
{"step_idx": 1, "type": "file_read", "label": "correct", "reward": 1.0},
{"step_idx": 2, "type": "thinking", "label": "correct", "reward": 1.0},
{"step_idx": 3, "type": "file_edit", "label": "correct", "reward": 1.0},
{"step_idx": 4, "type": "bash_command", "label": "correct", "reward": 1.0}
]
}أزواج التفضيل لـ DPO/RLHF
حين تكون لديك تتبعات متعددة للمشكلة نفسها (من وكلاء مختلفين أو تشغيلات مختلفة مثلاً)، يستطيع Potato توليد أزواج تفضيل اعتماداً على وسوم PRM:
potato export \
--format preference_pairs \
--project ./output/ \
--output ./training_data/preferences.jsonl \
--pair_by "issue_id"يقارن تصدير أزواج التفضيل بين التتبعات التي حاولت المهمة نفسها ويختار الأفضل منها اعتماداً على الوسوم على مستوى الخطوة:
{
"prompt": "Fix the TypeError in parse_config() when config file is empty",
"chosen_trace_id": "trace_001",
"rejected_trace_id": "trace_002",
"chosen_first_error": null,
"rejected_first_error": 3,
"chosen_steps": 5,
"rejected_steps": 7,
"margin": 0.8
}نتائج متوافقة مع SWE-bench
صدّر بصيغة SWE-bench لأغراض القياس المرجعي:
potato export \
--format swe_bench \
--project ./output/ \
--output ./training_data/swe_bench_results.jsonأمثلة تحليلية
بعد جمع التعليقات، استخدم مقاطع Python التالية لتحليل البيانات واستخراج الأنماط.
الدقة على مستوى الخطوة بحسب نوع الخطوة
import json
from collections import defaultdict
# Load PRM annotations
with open("training_data/prm_labels.jsonl") as f:
traces = [json.loads(line) for line in f]
# Compute accuracy by step type
type_stats = defaultdict(lambda: {"correct": 0, "total": 0})
for trace in traces:
for step in trace["steps"]:
step_type = step["type"]
type_stats[step_type]["total"] += 1
if step["label"] == "correct":
type_stats[step_type]["correct"] += 1
print("Step-Level Accuracy by Type:")
print("-" * 45)
for step_type, stats in sorted(type_stats.items()):
acc = stats["correct"] / stats["total"] * 100
print(f" {step_type:<20} {acc:5.1f}% ({stats['correct']}/{stats['total']})")إيجاد نقاط الفشل الشائعة
import json
from collections import Counter
with open("training_data/prm_labels.jsonl") as f:
traces = [json.loads(line) for line in f]
# Analyze where errors first occur
error_positions = []
error_types_at_first_error = Counter()
for trace in traces:
if trace["first_error_step"] is not None:
pos = trace["first_error_step"]
total = trace["total_steps"]
# Normalize position to 0-1 range
error_positions.append(pos / total)
# Track what type of step caused the first error
error_step = trace["steps"][pos]
error_types_at_first_error[error_step["type"]] += 1
if error_positions:
avg_pos = sum(error_positions) / len(error_positions)
print(f"Average first-error position: {avg_pos:.2f} (0=start, 1=end)")
print(f"Traces with errors: {len(error_positions)}/{len(traces)}")
print()
print("Most common step types at first error:")
for step_type, count in error_types_at_first_error.most_common(5):
print(f" {step_type}: {count}")حساب الاتفاق بين المعلّقين على وسوم PRM
import json
import numpy as np
from sklearn.metrics import cohen_kappa_score
def load_annotations(annotator_file):
"""Load annotations from a single annotator's output file."""
with open(annotator_file) as f:
data = {item["trace_id"]: item for item in
(json.loads(line) for line in f)}
return data
ann1 = load_annotations("output/reviewer1/annotations.jsonl")
ann2 = load_annotations("output/reviewer2/annotations.jsonl")
# Find overlapping traces
overlap_ids = set(ann1.keys()) & set(ann2.keys())
print(f"Overlapping traces: {len(overlap_ids)}")
# Compare first-error step labels
labels1 = []
labels2 = []
for trace_id in overlap_ids:
fe1 = ann1[trace_id].get("first_error_step", -1)
fe2 = ann2[trace_id].get("first_error_step", -1)
# Bin into: all_correct, early_error (first half), late_error (second half)
total = ann1[trace_id]["total_steps"]
for fe, labels in [(fe1, labels1), (fe2, labels2)]:
if fe is None or fe == -1:
labels.append("all_correct")
elif fe < total / 2:
labels.append("early_error")
else:
labels.append("late_error")
kappa = cohen_kappa_score(labels1, labels2)
print(f"Cohen's kappa (binned first-error): {kappa:.3f}")نصائح لجمع بيانات PRM بكفاءة
استخدم وضع الخطأ الأول للسرعة. إن كنت تدرّب PRM ليوجّه بحثاً (MCTS أو أخذ عيّنات best-of-N)، فوضع الخطأ الأول يعطيك إشارة كافية بسرعة تعليق تفوق وضع كل خطوة بمرتين إلى ثلاث مرات. ومعظم الوكلاء يفشلون على شكل سلسلة متتالية أصلاً: خطأ واحد يجرّ سلسلة من الخطوات السيئة.
استخدم وضع كل خطوة حين تحتاج إلى التفصيل. إن كان يعنيك التعافي الجزئي، أو الالتفافات غير الضارة، أو كنت تبني نموذج مكافأة على مستوى الخطوة بأكثر من وسمين، فوضع كل خطوة يستحق وقته الإضافي.
اجمع بين PRM والمقارنة الزوجية. وسّم التتبعات فرادى بـ PRM، ثم شغّل مقارنة زوجية على التتبعات التي حاولت المشكلة نفسها. تمريرة تعليق واحدة تعطيك مكافآت على مستوى الخطوة وأزواج تفضيل معاً.
ابدأ بمعلّقين ذوي خبرة. تعليق PRM يعني قراءة كود و diffs ومخرجات طرفية. ابدأ بمجموعة صغيرة من المطورين المتمرسين، وقس الاتفاق بينهم، وعايرهم على أمثلة، ثم وسّع.
اضبط حداً أدنى للزمن لكل عنصر. التتبعات تصير معقدة. وحدّ أدنى بثلاثين ثانية يمنع المعلّقين من الاندفاع دون قراءة التغييرات فعلاً. اضبطه بحسب متوسط طول تتبعاتك.
وفّر أمثلة معايرة. قبل التعليق الإنتاجي، اجعل الجميع يوسّمون العشرة أو العشرين تتبعاً نفسها ثم ناقشوا مواضع الاختلاف. لهذا أثر كبير في الاتساق.