وكيلك ينجح في 80% من اختباراتك. يبدو هذا رقماً جيداً. لكن تخيّل مستخدماً يطلب منه ثلاث مهام في جلسة واحدة: إذا كانت كل محاولة تنجح بنسبة 80%، فاحتمال أن تنجح الثلاث كلها حوالي النصف فقط. أغلب الفرق تكتشف هذا من شكاوى العملاء لا من الاختبارات، لأنها قاست الرقم الخطأ.
في هذا الدليل نشرح كيف تقيّم الوكيل بالطريقة الصحيحة: ماذا تقيس، وكيف تصحّح النتائج، وكيف تبني أداة تقييم صغيرة بلغة Python تعمل دون أي مفتاح API. كل ما هنا يتبع إرشادات Anthropic نفسها في تقييم الوكلاء، وكل رقم فيه خرج من كود شغّلناه فعلاً.
لماذا اختبار الوكيل أصعب من اختبار الكود العادي؟
الدالة العادية ترجع النتيجة نفسها كلما أعطيتها المدخلات نفسها، فنجاح اختبار واحد له معنى. أما الوكيل فلا. هو يأخذ عينات من نموذج لغوي، ويختار الأدوات، ويتفاعل مع ما ترجعه، وقد يسلك طريقاً مختلفاً في كل مرة. تشغيل الاختبار مرة واحدة يخبرك بما حدث مرة واحدة فقط.
فريق الهندسة في Anthropic نشر في يناير 2026 دليلاً عن هذا بعنوان "Demystifying evals for AI agents"، وفيه مفردات تستحق أن تتبناها لأنها توضّح المشكلة:
- المهمة (Task): اختبار واحد له مدخلات محددة ومعايير نجاح واضحة.
- المحاولة (Trial): تشغيل واحد للمهمة. ولأن المخرجات تختلف بين تشغيل وآخر، تشغّل عدة محاولات.
- المُقيِّم (Grader): منطق يعطي درجة لجانب معين من أداء الوكيل، وقد يكون للمهمة الواحدة أكثر من مُقيِّم.
- السجل (Transcript): التسجيل الكامل للمحاولة: المخرجات، واستدعاءات الأدوات، والتفكير، والنتائج الوسيطة.
- النتيجة النهائية (Outcome): حالة البيئة عند انتهاء المحاولة.
- أداة التقييم (Harness): الكود الذي يشغّل المهام، ويسجّل كل خطوة، ويصحّح النتائج، ويجمعها.
الرقم الذي يهم فعلاً: pass@k مقابل pass^k
عندما تشغّل المهمة عدة مرات، تستطيع أن تسأل سؤالين مختلفين جداً، والدليل يسمّي الاثنين:
- pass@k يقيس احتمال أن ينجح الوكيل مرة واحدة على الأقل في k محاولات. وهو يرتفع كلما زاد k. يناسب الحالات التي تستطيع فيها اختيار أفضل محاولة من عدة محاولات، مثل توليد كود ستفحصه اختبارات تلقائية.
- pass^k يقيس احتمال أن تنجح المحاولات الـ k كلها. وهو ينخفض كلما زاد k لأن الثبات أصعب. يناسب أي وكيل يعتمد عليه عميل، لأن العميل لا يعيد المحاولة حتى تنجح.
رأينا واضح: أي وكيل يتصرف نيابة عن مستخدم، قِس له pass^k. المقياس pass@k مفيد في الأبحاث وفي المهام التي لها فاحص تلقائي، لكنه يجامل الوكيل الذي ينجح أحياناً فقط. في التجربة التي ستراها بعد قليل، الوكيل نفسه حصل على 1.00 في pass@3 وعلى 0.49 في pass^3. واحد فقط من هذين الرقمين يصف تجربة مستخدميك.
لتقدير الاثنين من n محاولة نجح منها c، نستخدم المقدِّر غير المتحيز المعروف من ورقة Codex التي نشرتها OpenAI عام 2021 ("Evaluating Large Language Models Trained on Code"): pass@k = 1 − C(n−c, k) / C(n, k). والتقدير المقابل لـ pass^k هو C(c, k) / C(n, k)، أي احتمال أن تكون k محاولات مأخوذة من محاولاتك الـ n كلها ناجحة.
قيّم النتيجة لا الطريق
أشهر خطأ في اختبار الوكلاء هو التحقق من أن الوكيل استدعى أدوات معينة بترتيب معين. دليل Anthropic يحذّر من هذا تحديداً، ويصف هذه الاختبارات بأنها هشّة أكثر من اللازم، وينصح بأن تقيّم ما أنتجه الوكيل لا الطريق الذي سلكه. الوكيل الذي يقرأ التقويم قبل تفضيلات المستخدم بدل أن يقرأه بعدها ليس مخطئاً. أما الذي يحجز في اليوم الخطأ فهو المخطئ.
لذلك يجب أن يفحص المُقيِّم الحالة النهائية: السجل في قاعدة البيانات، أو الملف على القرص، أو الرسالة في صندوق الصادر. وهناك ثلاثة أنواع من المُقيِّمين، والدليل واضح في مزايا وعيوب كل منها:
- المُقيِّم البرمجي (فحص دقيق للحالة النهائية): سريع ورخيص وموضوعي وقابل للتكرار، لكنه يتأثر بالاختلافات غير المهمة. استخدمه في كل مكان يمكن فيه فحص النتيجة بقاعدة واضحة.
- المُقيِّم بالنموذج (نموذج لغوي مع معايير تقييم): يتعامل مع المخرجات المفتوحة والتفاصيل الدقيقة، لكنه غير ثابت وله تكلفة. استخدمه لتقييم النبرة أو الاكتمال أو جودة النصوص المكتوبة.
- المُقيِّم البشري: المعيار الذهبي، لكنه بطيء ومكلف. استخدمه لمعايرة المُقيِّم بالنموذج، لا لتقييم كل تشغيل.
لنبنِ أداة تقييم صغيرة بلغة Python
الأداة التالية لا تحتاج أي مكتبة غير مكتبات Python القياسية. تشغّل كل مهمة عدة مرات، وتطبّق كل المُقيِّمين على الحالة النهائية، وتحفظ سجل كل محاولة، ثم تعرض نسبة النجاح وpass@k وpass^k. أنشئ الملف harness.py:
"""A minimal eval harness for agents: tasks, trials, outcome graders, pass@k and pass^k."""
import json
import zlib
from dataclasses import dataclass, field
from math import comb
from typing import Callable
@dataclass
class Task:
id: str
prompt: str
graders: list[Callable[[dict], tuple[bool, str]]] # each checks the final state
kind: str = "regression" # "regression" (should pass ~100%) or "capability"
@dataclass
class Trial:
task_id: str
passed: bool
failures: list[str] = field(default_factory=list)
transcript: list[str] = field(default_factory=list)
def pass_at_k(n: int, c: int, k: int) -> float:
"""Chance that at least one of k trials passes (unbiased estimate from n trials, c passed)."""
if n - c < k:
return 1.0
return 1 - comb(n - c, k) / comb(n, k)
def pass_hat_k(n: int, c: int, k: int) -> float:
"""Chance that all k trials pass (pass^k), estimated from n trials with c passes."""
return comb(c, k) / comb(n, k)
def run_eval(agent, tasks: list[Task], trials: int) -> list[Trial]:
results = []
for task in tasks:
for i in range(trials):
seed = zlib.crc32(f"{task.id}:{i}".encode()) # same seeds every run
state, transcript = agent(task.prompt, seed=seed)
failures = []
for grader in task.graders:
ok, why = grader(state)
if not ok:
failures.append(why)
results.append(Trial(task.id, not failures, failures, transcript))
return results
def report(tasks: list[Task], results: list[Trial], k: int) -> None:
print(f"{'task':<26}{'kind':<12}{'passed':>8}{'pass@'+str(k):>9}{'pass^'+str(k):>9}")
for task in tasks:
runs = [r for r in results if r.task_id == task.id]
n, c = len(runs), sum(r.passed for r in runs)
print(f"{task.id:<26}{task.kind:<12}{f'{c}/{n}':>8}"
f"{pass_at_k(n, c, k):>9.2f}{pass_hat_k(n, c, k):>9.2f}")
failed = [r for r in results if not r.passed]
if failed:
print("\nFirst failure to read in full:")
print(json.dumps(failed[0].__dict__, indent=2, ensure_ascii=False))
هناك تفصيلان مهمان. الأول: كل محاولة تأخذ بذرة عشوائية (seed) ثابتة مشتقة من اسم المهمة ورقم المحاولة، فيكون التشغيل قابلاً للتكرار: الكود نفسه يعطي التقرير نفسه في كل مرة، وهذا ما يسمح لك بمقارنة أي تعديل بالنسخة السابقة. الثاني: التقرير يطبع سجل محاولة فاشلة كاملاً، وهذا مقصود كما سترى.
اكتب المهام والمُقيِّمين
الآن المهام. حتى يعمل المثال دون مفتاح API، نستخدم وكيلاً بديلاً يدير قائمة مهام ويرتكب الأخطاء نفسها التي ترتكبها الوكلاء الحقيقية: أحياناً ينسى تاريخ الاستحقاق، وأحياناً يعيد استدعاءً فيكرر المهمة، وفي الطلبات متعددة الخطوات يعلّم أحياناً المهمة الخطأ كمنجزة. في مشروعك، استبدل demo_agent باستدعاء لوكيلك الحقيقي يرجع حالته النهائية وسجله. أنشئ run_eval.py:
import random
from harness import Task, report, run_eval
# --- Stand-in agent ---------------------------------------------------------
# Replace this function with a call to your real agent. It must return the
# final state it produced and a transcript of what it did. This stand-in
# makes the same kinds of mistakes real agents make, at fixed rates.
def demo_agent(prompt: str, seed: int):
rng = random.Random(seed)
tasks, transcript = [], [f"user: {prompt}"]
def add(title, due=None):
tasks.append({"title": title, "due": due, "done": False})
transcript.append(f"tool add_task(title={title!r}, due={due!r})")
if "invoice" in prompt:
add("Send invoice to Acme", None if rng.random() < 0.10 else "2026-10-10")
if rng.random() < 0.05: # retries after a timeout and duplicates the task
add("Send invoice to Acme", "2026-10-10")
if "three tasks" in prompt:
for title in ("Book venue", "Order catering", "Send invites"):
add(title)
target = 1 if rng.random() > 0.35 else 2 # sometimes marks the wrong one
tasks[target]["done"] = True
transcript.append(f"tool complete_task(task_id={target + 1})")
transcript.append("assistant: Done.")
return {"tasks": tasks}, transcript
# --- Graders: check the final state (the outcome), not the exact steps ------
def has_task(title):
def grade(state):
found = any(t["title"] == title for t in state["tasks"])
return found, f"missing task {title!r}"
return grade
def due_date(title, expected):
def grade(state):
due = next((t["due"] for t in state["tasks"] if t["title"] == title), None)
return due == expected, f"{title!r} due {due!r}, expected {expected!r}"
return grade
def no_duplicates(state):
titles = [t["title"] for t in state["tasks"]]
return len(titles) == len(set(titles)), f"duplicate tasks: {titles}"
def only_done(title):
def grade(state):
done = [t["title"] for t in state["tasks"] if t["done"]]
return done == [title], f"done tasks {done}, expected [{title!r}]"
return grade
TASKS = [
Task("invoice-with-date",
"Add a task to send the invoice to Acme, due 2026-10-10.",
[has_task("Send invoice to Acme"),
due_date("Send invoice to Acme", "2026-10-10"),
no_duplicates]),
Task("three-tasks-complete-2nd",
"Add three tasks for the event, then mark ordering catering as done.",
[has_task("Order catering"), only_done("Order catering"), no_duplicates],
kind="capability"),
]
if __name__ == "__main__":
results = run_eval(demo_agent, TASKS, trials=20)
report(TASKS, results, k=3)
لاحظ ما يفحصه المُقيِّمون: أن المهمة موجودة، وأن تاريخها صحيح، وأنه لا يوجد تكرار، وأن المهمة الصحيحة فقط هي المنجزة. لا أحد منهم يهتم بأي أداة استُدعيت أولاً. ولاحظ أن المهمتين مصنّفتان بشكل مختلف كما يوصي الدليل: مهام الانحدار (regression) يجب أن تنجح بنسبة قريبة من 100% لأنها تغطي أشياء يفعلها الوكيل أصلاً، ومهام القدرات (capability) تبدأ بنسبة نجاح منخفضة لأنها تقيس ما زلت تحاول أن تجعله يفعله.
اقرأ النتائج، ثم اقرأ السجلات
تشغيل python run_eval.py بعشرين محاولة لكل مهمة أعطى:
task kind passed pass@3 pass^3
invoice-with-date regression 16/20 1.00 0.49
three-tasks-complete-2nd capability 14/20 0.98 0.32
First failure to read in full:
{
"task_id": "invoice-with-date",
"passed": false,
"failures": [
"'Send invoice to Acme' due None, expected '2026-10-10'"
],
"transcript": [
"user: Add a task to send the invoice to Acme, due 2026-10-10.",
"tool add_task(title='Send invoice to Acme', due=None)",
"assistant: Done."
]
}
ثلاث ملاحظات واضحة:
- pass@3 يخفي المشكلة. المهمتان حصلتا على 0.98 أو أكثر، وهذا يبدو جاهزاً للإطلاق. لكن pass^3 يقول إن المستخدم الذي يكرر مهمة الفاتورة ثلاث مرات احتمال أن تنجح الثلاث كلها حوالي 49%، وفي المهمة متعددة الخطوات حوالي 32%. وتحققنا من الرقمين يدوياً: C(16,3)/C(20,3) = 560/1140 ≈ 0.49.
- مهمة انحدار بنسبة 80% إنذار أحمر. شيء يجب أن ينجح فيه الوكيل دائماً يفشل مرة من كل خمس. هذا أول ما يجب إصلاحه، قبل أي قدرة جديدة.
- السجل يخبرك بالسبب. الفشل الظاهر أعلاه ليس غامضاً: الوكيل استدعى
add_taskمعdue=Noneمع أن المستخدم ذكر التاريخ. الآن تعرف هل تصلح وصف الأداة أم التعليمات أم مخطط المُعاملات. الدرجة وحدها لا تخبرك بهذا أبداً.
ولهذا يؤكد الدليل أنك لن تعرف إن كان المُقيِّمون يعملون جيداً إلا إذا قرأت السجلات والدرجات من محاولات كثيرة. وقراءة السجلات تكشف أيضاً المُقيِّمين المعطوبين: مُقيِّم يرفض "96.12" لأنه ينتظر "96.124991" سيعطيك حالات فشل هي في الحقيقة أخطاء في اختبارك أنت.
عندما لا يمكن فحص النتيجة بقاعدة ثابتة
بعض المخرجات، مثل رد على بريد إلكتروني أو ملخص، ليس لها إجابة واحدة صحيحة. لهذه استخدم نموذجاً لغوياً كمُقيِّم، مع معايير صارمة ومخرجات محددة. وثائق الاختبار في Anthropic توصي بمعايير مفصلة ومحددة، ودرجات بسيطة ("صحيح"/"خطأ" أو مقياس من 1 إلى 5)، وأن تسمح للمُقيِّم بالتفكير قبل أن يجيب. وهذا هو النمط الموجود في تلك الوثائق:
def build_grader_prompt(answer, rubric):
return f"""Grade this answer based on the rubric:
<rubric>{rubric}</rubric>
<answer>{answer}</answer>
Output 'correct' or 'incorrect' in <result> tags."""
وقبل أن تعتمد على مُقيِّم بالنموذج على نطاق واسع، قيّم أنت 20 أو 30 مخرجاً بنفسك وقارن. نصيحة الدليل أن يكون المُقيِّم بالنموذج معايَراً بدقة مع خبراء بشريين. وإذا اختلفت أنت والمُقيِّم كثيراً، فأصلح معايير التقييم قبل أن تثق بالدرجات.
خطة تستطيع البدء بها هذا الأسبوع
- ابدأ صغيراً ومن أخطاء حقيقية. الدليل يوصي بالبدء بـ 20 إلى 50 مهمة بسيطة مأخوذة من حالات فشل حقيقية. كل بلاغ عن خطأ وكل عرض تجريبي محرج يتحول إلى مهمة.
- اكتب مهاماً لا لبس فيها. إذا كان شخصان قد يختلفان في هل نجح الوكيل أم لا، فالمشكلة في المهمة. ويشير الدليل إلى أن نسبة نجاح 0% عبر محاولات كثيرة مع نموذج قوي تعني غالباً مهمة معطوبة لا وكيلاً عاجزاً.
- شغّل كل مهمة 10 إلى 20 مرة على الأقل، واحسب pass^k بعدد المرات التي يكرر فيها المستخدم الحقيقي الطلب.
- افصل مهام الانحدار عن مهام القدرات. الأولى تقرر هل تطلق التحديث، والثانية تتابع التقدم.
- اقرأ عينة من السجلات في كل مرة، لا جدول الملخص فقط.
- شغّل الاختبارات بعد كل تعديل على التعليمات أو الأدوات أو النموذج. تعديل بسيط في التعليمات يصلح مهمة قد يكسر أخرى، ولا يكشف ذلك إلا مجموعة اختبارات.
الخلاصة
الوكيل يصبح جاهزاً عندما يكون ثابتاً، لا عندما يستطيع النجاح. قِس الثبات بـ pass^k، وقيّم النتيجة لا الطريق، واقرأ السجل خلف كل فشل. أداة التقييم أعلاه حوالي 60 سطراً من Python القياسية. اربط بها وكيلك الحقيقي، واكتب 20 مهمة من بلاغات الأخطاء في الشهر الماضي، وستعرف عن موثوقية وكيلك بحلول الغد أكثر مما تخبرك به مئة تجربة يدوية.
والتقييم يكمّل الركيزتين اللتين شرحناهما سابقاً: إعطاء الوكيل أدوات عبر MCP وإعطاؤه ذاكرة تبقى. وكلاهما من نوع التعديلات التي يجب أن تمر على مجموعة اختباراتك.
المصادر: مدونة Anthropic الهندسية، "Demystifying evals for AI agents" (9 يناير 2026)؛ وثائق Anthropic عن تطوير الاختبارات (platform.claude.com)؛ وChen وآخرون، "Evaluating Large Language Models Trained on Code" (2021) لمقدِّر pass@k. جرى اختبار الكود بتاريخ 2026-10-05 على Python 3.13؛ ونسب الفشل في الوكيل البديل ثابتة في الكود وهي للتوضيح فقط، وليست قياساً لأي نموذج حقيقي.