Skip to content
Guides10 min read

كيف تجمع بيانات مكافأة العملية لتدريب وكلاء برمجة أفضل

دليل عملي خطوة بخطوة لجمع إشارات المكافأة لكل خطوة من أجل تدريب PRM باستخدام Potato. يغطي وضع الخطأ الأول، والتعليق لكل خطوة، والتصدير إلى خطوط التدريب.

Potato Team

ما نماذج مكافأة العملية؟

طريقتان لتوسيم مكافآت العملية: وضع الخطأ الأول يحدد نقطة انكسار واحدة، ووضع كل خطوة يقيّم كل خطوةطريقتان لتوسيم مكافآت العملية

لا تنظر نماذج مكافأة النتيجة (ORMs) إلا إلى نهاية مسار وكيل البرمجة: هل تُرجم الكود؟ هل نجحت الاختبارات؟ هل حُلّت المشكلة؟ أما نماذج مكافأة العملية (PRMs) فتعطي درجة لكل خطوة وسيطة بدل ذلك. ومع إشارة مكافأة عند كل خطوة، تستطيع طرق التدريب تحديد موضع خطأ الوكيل بدقة، وهذا يجعل التعلّم أكفأ في استهلاك العينات ويساعد على التعميم.

دفعت أعمال حديثة هذا الاتجاه. يعيد AgentPRM تعريف مكافآت العملية لمهام الوكلاء، فيقيّم كل إجراء بمقدار التقدّم الذي يحرزه نحو الهدف بدلاً من صوابه، ويذكر كفاءة حوسبية أفضل بأكثر من 8 أضعاف مقارنةً بالأسس التي قارن بها. ووجد ToolRM أن نماذج المكافأة المدرَّبة على مخرجات لغوية تحكم على استدعاءات الأدوات حكماً سيئاً، فبنى نماذج مكافأة خاصة بالأدوات إلى جانب FC-RewardBench لتقييمها. وفي المقابل، يدرّب DeepSWE وكيل برمجة بمكافأة نتيجة متفرّقة وحدها، أي هل تنجح الاختبارات، ويبلغ 42.2% في Pass@1 و59% مع توسيع الحوسبة وقت الاختبار على SWE-bench Verified. هذا هو الإعداد القائم على النتيجة وحدها الذي تحاول مراقبة العملية تحسينه.

وما تحتاج إليه هذه الأعمال كلها هو تعليق بشري جيد على مستوى الخطوة، وهذا عادةً هو عنق الزجاجة. ومخططات مكافأة العملية في Potato مبنية لتسريع جمع تلك البيانات. للاطلاع على المخطط الأساسي، انظر وثائق تقييم المسار، ولتفاصيل إدخال التتبعات، وثائق تتبعات الوكلاء.

وضعا التعليق التوضيحي

في Potato وضعان لتعليق PRM يقايضان السرعة بالتفصيل. اختر ما يناسب ميزانية بياناتك وأهدافك.

وضع الخطأ الأول

في وضع الخطأ الأول، يقرأ المعلّق المسار من أعلاه إلى أسفله وينقر على أول خطوة أخطأ فيها الوكيل. ثم يعلّم Potato كل خطوة سابقة بأنها صحيحة، وكل خطوة من المنقور عليها فصاعداً بأنها خاطئة.

وهذا سريع لأن المعلّق لا يحتاج إلا إلى إيجاد نقطة قرار واحدة. وهو يعمل جيداً حين تتوالى الأخطاء، أي حين يندر أن يتعافى الوكيل بعد خروجه عن المسار، وهذه هي الحالة الشائعة عملياً.

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

يجري سير عمل التعليق بوضع الخطأ الأول هكذا:

  1. يفتح المعلّق تتبعاً فيرى كل الخطوات مصيَّرة بمكوّن CodingTraceDisplay.
  2. يقرأ الخطوات بالتسلسل، ويفحص الـ diffs ومخرجات الطرفية وخطوات التفكير.
  3. حين يجد أول خطوة خاطئة، ينقر على علامة الخطأ بجانبها.
  4. تتحول الخطوات من 0 إلى N-1 إلى الأخضر (صحيحة)، وتتحول الخطوات من N إلى النهاية إلى الأحمر (خاطئة).
  5. يراجع المعلّق التوسيم التلقائي وينقر على Submit للتأكيد.

إن كان التتبع كله صحيحاً (حلّ الوكيل المهمة على أتمّ وجه)، ينقر المعلّق على All Correct. وإن كانت الخطوة الأولى نفسها خاطئة، فينقر على الخطوة 0 أو يستخدم All Incorrect.

وهذه واجهة تعليق PRM أثناء العمل:

تعليق مكافأة العملية يعرض التقييم على مستوى الخطوةفي وضع الخطأ الأول، انقر على أول خطوة خاطئة فتُوسم كل الخطوات التالية تلقائياً

وضع كل خطوة

في وضع كل خطوة، تنال كل خطوة وسمها الخاص. وهذا ينتج بيانات أغنى، لأنه يلتقط الحالات التي يتعافى فيها الوكيل جزئياً من خطأ، أو يسلك التفافاً غير ضار لكنه زائد، أو يخطو خطوة سليمة بذاتها لكنها خاطئة في سياقها.

yaml
annotation_schemes:
  - annotation_type: process_reward
    name: prm_per_step
    mode: "per_step"

إعداد مشروع تعليق PRM

الخطوة 1: جهّز بيانات التتبع

ينبغي أن تكون بيانات دخلك ملف JSONL يمثل كل سطر فيه كائن JSON يحتوي مسار الوكيل. والحقول الأساسية هي id وissue_description وstructured_turns:

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

وإن كنت تحوّل من صيغة وكيل موجودة، فاستخدم أداة تحويل التتبعات:

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

تتبع وكيل برمجة مع تصيير الـ diffفروق الكود ومخرجات الطرفية وقراءات الملفات تُصيَّر مع إبراز البنية اللغوية

الخطوة 2: أنشئ تهيئتك

هذه تهيئة مشروع كاملة لتعليق PRM بوضع الخطأ الأول:

yaml
# 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: شغّل خادم التعليق

bash
# 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: تابع التقدّم

أثناء سير التعليق، تابع التقدّم ودرجة الاتفاق:

bash
# Check annotation progress
potato status config.yaml
 
# View inter-annotator agreement
potato agreement config.yaml --metric krippendorff_alpha

التصدير إلى صيغ التدريب

بعد اكتمال التعليق، صدّر البيانات بالصيغة التي يتوقعها خط التدريب عندك.

صيغة PRM لتدريب نموذج المكافأة

تنتج صيغة تصدير PRM كائن JSON واحداً لكل تتبع مع وسوم على مستوى الخطوة:

bash
potato export \
  --format prm \
  --project ./output/ \
  --output ./training_data/prm_labels.jsonl

والمخرجات تبدو هكذا:

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

bash
potato export \
  --format preference_pairs \
  --project ./output/ \
  --output ./training_data/preferences.jsonl \
  --pair_by "issue_id"

يقارن تصدير أزواج التفضيل بين التتبعات التي حاولت المهمة نفسها ويختار الأفضل منها اعتماداً على الوسوم على مستوى الخطوة:

json
{
  "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 لأغراض القياس المرجعي:

bash
potato export \
  --format swe_bench \
  --project ./output/ \
  --output ./training_data/swe_bench_results.json

أمثلة تحليلية

بعد جمع التعليقات، استخدم مقاطع Python التالية لتحليل البيانات واستخراج الأنماط.

الدقة على مستوى الخطوة بحسب نوع الخطوة

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

إيجاد نقاط الفشل الشائعة

python
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

python
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 ومخرجات طرفية. ابدأ بمجموعة صغيرة من المطورين المتمرسين، وقس الاتفاق بينهم، وعايرهم على أمثلة، ثم وسّع.

اضبط حداً أدنى للزمن لكل عنصر. التتبعات تصير معقدة. وحدّ أدنى بثلاثين ثانية يمنع المعلّقين من الاندفاع دون قراءة التغييرات فعلاً. اضبطه بحسب متوسط طول تتبعاتك.

وفّر أمثلة معايرة. قبل التعليق الإنتاجي، اجعل الجميع يوسّمون العشرة أو العشرين تتبعاً نفسها ثم ناقشوا مواضع الاختلاف. لهذا أثر كبير في الاتساق.