وكيلك أتقن المهمة. هل سيفعلها مرة أخرى؟

قد ينجح الوكيل مرة واحدة ثم يفشل في التشغيل التالي. بالاستناد إلى عمل الاتساق ALTK-Evolve من IBM Research، يستعرض هذا المقال لماذا تُضلِّل معايير التقييم القائمة على تشغيل واحد، وكيف تكشف التجارب المتكررة التباين في استخدام الأدوات والاستدلال، وما عادات التقييم العملية التي تساعد الفرق على الحكم على ما إذا كان نجاح الوكيل سيتكرر.

القراءة الصوتية غير متاحة في هذا المتصفح
وكيلك أتقن المهمة. هل سيفعلها مرة أخرى؟

الوسوم

ملخص سريع

قد ينجح الوكيل مرة واحدة ثم يفشل في التشغيل التالي. بالاستناد إلى عمل الاتساق ALTK-Evolve من IBM Research، يستعرض هذا المقال لماذا تُضلِّل معايير التقييم القائمة على تشغيل واحد، وكيف تكشف التجارب المتكررة التباين في استخدام الأدوات والاستدلال، وما عادات التقييم العملية التي تساعد الفرق على الحكم على ما إذا كان نجاح الوكيل سيتكرر.

وكيلك أتقن المهمة. هل سيفعلها مرة أخرى؟

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

يتناول هذا المقال قياس تلك الفجوة، ثم إغلاقها. إنه دليل هندسي عملي: أداة اختبار صغيرة، وحفنة من المقاييس، ومجموعة تجارب يمكنك تشغيلها بعد ظهر هذا اليوم لتعرف ما إذا كان نجاح وكيلك قدرة أم مصادفة.

نقطة البداية سؤال طرحته IBM Research على مدونة Hugging Face: وكيلك أتقن المهمة. هل سيفعلها مرة أخرى؟ (المصدر). يطرح ذلك المنشور مشكلة اتساق الوكيل. كل ما يلي — أداة الاختبار، والأوامر، وتعريفات المقاييس — هو هندسة موثوقية قياسية مطبقة على الوكلاء؛ وهو ليس ملخصاً لطرائق ذلك المنشور، وينبغي أن تقرأ المصدر مباشرةً لرؤية تأطيره الخاص.

فخ العرض التوضيحي

عروض الوكلاء التوضيحية مُحسَّنة لمسارات نجاح واحدة. تختار مهمة، وتشغلها، فتنجح، فتنشرها. العرض التوضيحي عينة حجمها واحد، وحجم العينة واحد لا أشرطة خطأ له.

توجد ثلاث خصائص تجعل هذا أسوأ مع الوكلاء مقارنةً بمعظم البرمجيات:

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

الوكلاء يعتمدون على العالم. استدعاء أداة يصل إلى واجهة برمجة تطبيقات تعيد بيانات مختلفة، أو فهرس بحث أُعيدت فهرسته، أو قاعدة بيانات تغيرت صفوفها. قد لا يكون تشغيلان لـ"المهمة نفسها" هما المهمة نفسها على الإطلاق.

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

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

ما الذي يعنيه "مرة أخرى" فعلاً

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

  • التوافر — يكمل الوكيل دون استثناء غير معالج.
  • الصحة — يحقق الناتج مدققاً آلياً.
  • الاتساق — إنتاج تنفيذات متكررة للمهمة نفسها نتائج مكافئة.

تفشل هذه الخصائص بشكل مستقل. يمكن أن يكون الوكيل متسقاً تماماً وخاطئاً باستمرار. ويمكن أن يكون صحيحاً في المتوسط وغير قابل للاستخدام عملياً لأن 20% من التشغيلات تفشل. ويمكن أن يكون متاحاً 100% من الوقت بينما ينتج إجابات مختلفة في كل تشغيل.

المقياس الذي يلتقط السؤال ذا الصلة بالإنتاج هو ما سأسميه pass^k: شغّل المهمة نفسها k مرات، واحسب المهمة ناجحة فقط إذا نجحت جميع المحاولات k. هذا أقسى عمداً من pass@1 (متوسط معدل النجاح)، لأنه في معظم عمليات نشر الوكلاء لا تكون المهمة "منتهية" إذا نجحت ثلاث مرات من خمس. يعاقب pass^k التذبذب مباشرة، وهو يتدهور سريعاً: وكيل بمعدل نجاح 90% لكل تشغيل ينجح في فحص من 10 تشغيلات فقط حوالي 35% من الوقت.

هذه الحسابات هي الحجة الكاملة لهذا المقال. وكيل بنسبة 90% يبدو جيداً ويتصرف بشكل سيئ.

المتطلبات

تحتاج إلى وكيل عامل يمكنك استدعاؤه كدالة، ومدقق يمكنه تقرير ما إذا كان الناتج صحيحاً. كل ما عدا ذلك أدوات قياسية.

  • Python 3.10 أو أحدث.
  • pip (أو uv، إذا كنت تفضل عمليات تثبيت أسرع).
  • مجموعة مهام: يُفضّل 10–50 مهمة تمثيلية بنتائج جيدة معروفة.
  • مدقق برمجي لكل مهمة. مطابقة النصوص، أو التحقق من المخطط، أو اختبارات وحدة على الناتج، أو نص صغير من التأكيدات. إذا كان المدقق الوحيد لديك هو إنسان يقرأ الناتج، فابدأ من هناك — لكن أتمت الحالات السهلة أولاً.
  • اختياري: Docker، إذا أردت أن تكون بيئة التشغيل متطابقة عبر الأجهزة.
  • Git، حتى يكون كل تشغيل مرتبطاً بإيداع (commit).

المدقق هو الجزء الصعب. خصص معظم جهدك هناك، لا في أداة الاختبار.

التثبيت خطوة بخطوة

أنشئ بيئة معزولة حتى لا تتعارض تبعيات أداة الاختبار مع تبعيات وكيلك.

python -m venv .venv && source .venv/bin/activate

على Windows، فعّلها باستخدام .venv\Scripts\activate بدلاً من ذلك. إذا كنت تفضل uv، فالمكافئ هو:

uv venv && source .venv/bin/activate

ثبّت أدوات الاختبار والتحليل. يشغّل pytest الفحوصات، ويعيد pytest-repeat تشغيل اختبار واحد N مرة، ويتولى pandas/numpy التجميع.

pip install "pytest>=8" pytest-repeat pandas numpy

جمّد البيئة الدقيقة حتى يمكن إعادة إنتاج تشغيل مستقبلي. هذا الملف أثر يجب أن تُودعه.

pip freeze > requirements.lock

سجّل إصدار الشيفرة إلى جانب كل مجموعة نتائج. بدون هذا، يكون التغير في الاتساق غير قابل للنسبة.

git rev-parse HEAD > .run-commit && cat .run-commit

إذا أردت عزل بيئة التشغيل أيضاً، فابنِ صورة مرة واحدة وشغّل كل المحاولات داخلها.

docker build -t agent-under-test:1.0 .

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

export PYTHONHASHSEED=0

هذه هي سلسلة الأدوات بأكملها. لا حاجة إلى إطار عمل للوكلاء.

بناء أداة اختبار قابلية التكرار

لأداة الاختبار مهمة واحدة: استدعاء الوكيل k مرات لكل مهمة، وتسجيل كل شيء، وعدم التخلص من أي فشل أبداً. ضع هذا في consistency/harness.py.

# consistency/harness.py
from __future__ import annotations

import json
import time
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Callable


@dataclass
class RunRecord:
    task_id: str
    attempt: int
    success: bool
    latency_s: float
    output: str
    error: str | None = None


def run_attempts(
    agent: Callable[[str], str],
    task_id: str,
    task_input: str,
    checker: Callable[[str], bool],
    attempts: int = 10,
    out_path: Path = Path("runs.jsonl"),
) -> list[RunRecord]:
    """Execute one task `attempts` times and append every result to disk."""
    records: list[RunRecord] = []
    for i in range(attempts):
        t0 = time.perf_counter()
        output, error = "", None
        try:
            output = agent(task_input)
        except Exception as exc:  # record, never swallow silently
            error = f"{type(exc).__name__}: {exc}"
        latency = time.perf_counter() - t0
        records.append(
            RunRecord(
                task_id=task_id,
                attempt=i,
                success=bool(error is None and checker(output)),
                latency_s=round(latency, 3),
                output=output,
                error=error,
            )
        )

    with out_path.open("a", encoding="utf-8") as fh:
        for rec in records:
            fh.write(json.dumps(asdict(rec)) + "\n")
    return records

ثلاثة قرارات تصميم مهمة هنا. يتم تخزين النواتج كاملة، لأنك لا تستطيع تصحيح عيب متقطع لم تلتقطه. يتم تسجيل حالات الفشل بدلاً من رفعها، لأن انهياراً في المحاولة 3 يجب ألا يخفي بيانات المحاولتين 1 و2. تُضاف النتائج إلى JSONL، بحيث لا تفقد عملية منهارة سوى المهمة الحالية.

اربطها باختبار يؤكد على التوزيع، لا على تشغيل واحد.

# tests/test_consistency.py
from consistency.harness import run_attempts


def test_refund_agent_is_stable(agent, refund_checker):
    records = run_attempts(
        agent,
        task_id="refund-order-42",
        task_input="Refund order 42 in full.",
        checker=refund_checker,
        attempts=10,
    )
    failures = [r for r in records if not r.success]
    assert not failures, (
        f"{len(failures)}/10 attempts failed. "
        f"First error: {failures[0].error or failures[0].output[:200]}"
    )

ينجح هذا الاختبار فقط عندما ينجح الوكيل في 10 من 10. على وكيل حقيقي، سيفشل أول مرة تشغله، وهذا الفشل هو الناتج المفيد.

أمثلة الاستخدام

المثال 1: بوّب تغييراً في المطالبة

أعدت كتابة مطالبة النظام. هل تساعد؟ احسب pass^k قبل وبعد، على مجموعة المهام نفسها.

import json
import pandas as pd

rows = [json.loads(line) for line in open("runs.jsonl", encoding="utf-8")]
df = pd.DataFrame(rows)

summary = (
    df.groupby("task_id")["success"]
      .agg(attempts="size", passes="sum")
      .assign(pass_at_1=lambda d: d["passes"] / d["attempts"])
      .assign(stable=lambda d: d["passes"] == d["attempts"])
)
print(summary)
print("pass^k (all attempts passed):", summary["stable"].mean().round(3))

شغّل الكتلة مرة على runs.jsonl الخاص بالمطالبة القديمة ومرة على الجديدة. إذا تحسن pass@1 من 0.82 إلى 0.85 لكن انخفض pass^k من 0.60 إلى 0.45، فإن المطالبة الجديدة تشتري أداءً متوسطاً بتباين — وهي عادة مقايضة خاطئة.

المثال 2: إجراء اختبار كناري لترقية تبعية أو نموذج

شغّل أداة الاختبار على المرشح والحالي في الجلسة نفسها، ثم قارن لكل مهمة بدلاً من المقارنة الإجمالية. تخفي الأرقام الإجمالية التغييرات المتعارضة: خمس مهام أُصلحت، وخمس مهام تعطلت، بمتوسط لا يمكن تمييزه.

pivot = (
    df.pivot_table(index="task_id", columns="variant",
                   values="success", aggfunc="mean")
)
pivot["delta"] = pivot["candidate"] - pivot["baseline"]
print(pivot.sort_values("delta").head(10))  # regressions first

الفرز تصاعدياً يُظهر الانحدارات أولاً، وهذا ما تريد النظر إليه.

المثال 3: إعادة إنتاج فشل متقطع

عندما تفشل مهمة 3 مرات من 10، يكون ناتج الفشل في runs.jsonl لكن السبب عادة في التتبع. غلّف استدعاءات أدواتك بطبقة تسجيل/إعادة تشغيل حتى يمكن إعادة تنفيذ محاولة فاشلة دون الوصول إلى العالم الحي.

import hashlib
import json
from pathlib import Path

CASSETTE = Path("cassettes/tools.json")


def tool_key(name: str, args: dict) -> str:
    payload = json.dumps({"name": name, "args": args}, sort_keys=True)
    return hashlib.sha256(payload.encode()).hexdigest()[:16]


def load_cassette() -> dict:
    return json.loads(CASSETTE.read_text()) if CASSETTE.exists() else {}

في وضع إعادة التشغيل، ابحث عن tool_key(name, args) في الكاسيت وأعد الاستجابة المسجلة. في وضع التسجيل، استدعِ الأداة الحقيقية واكتب الاستجابة مرة أخرى. يحوّل هذا عرَضاً غير قابل لإعادة الإنتاج إلى اختبار وحدة حتمي، وهو الخطوة الأعلى تأثيراً في سير العمل بأكمله.

من أين يأتي التباين

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

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

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

عدم حتمية الأدوات. تعيد واجهات API الحية بيانات مختلفة. تزيل طبقة التسجيل/إعادة التشغيل هذا كعامل إرباك — إنها لا تصلحه في الإنتاج، لكنها تخبرك ما إذا كان تذبذبك من عندك أم من العالم.

انحراف الاسترجاع. إذا استعلم الوكيل عن فهرس يُعاد بناؤه بين التشغيلات، يتغير السياق المسترجع. خذ لقطة من الفهرس لتشغيل معياري.

تدفق التحكم. لدى الوكلاء متعددي الخطوات نقاط قرار كثيرة. تتراكم معدلات الخطأ الصغيرة لكل خطوة: عشر خطوات بموثوقية 98% لكل خطوة تعطي نحو 82% من البداية إلى النهاية. قِس لكل خطوة، لا من البداية إلى النهاية فقط، وإلا لن تعرف أين تبذل الجهد.

قراءة الأرقام بأمانة

تنتج قيم k الصغيرة تقديرات مشوشة، ومن السهل المبالغة في تفسيرها. استخدم فترة ثقة.

import math


def wilson(passes: int, n: int, z: float = 1.96) -> tuple[float, float]:
    """95% Wilson score interval for a binomial proportion."""
    if n == 0:
        return (0.0, 1.0)
    p = passes / n
    denom = 1 + z**2 / n
    centre = (p + z**2 / (2 * n)) / denom
    half = (z * math.sqrt(p * (1 - p) / n + z**2 / (4 * n**2))) / denom
    return (round(max(0.0, centre - half), 3), round(min(1.0, centre + half), 3))


print(wilson(8, 10))  # e.g. (0.49, 0.94)

معدل نجاح "80%" مقاس على 10 تشغيلات متوافق مع معدل حقيقي يتراوح بين نحو 50% و95%. هذه ليست أدلة كافية لاعتماد إصدار. عشر محاولات لكل مهمة عبر 20 مهمة — 200 تشغيل — يعطي صورة أضيق بكثير، بتكلفة 20 ضعفاً. اختر حجم العينة عن قصد، واذكره كلما أبلغت عن معدل.

عادتان أخريان في الإبلاغ:

افصل الحقائق المتحقق منها عن التفسير. "فشل الوكيل في 4 من 50 تشغيلاً في المهمة X، بالخطأ Y" حقيقة. "المسترجع هو عنق الزجاجة" تفسير حتى تختبره.

أبلغ عن أسوأ مهمة، لا عن المتوسط فقط. pass@1 بقيمة 0.95 مع مهمة واحدة عند 0.30 يمثل نظاماً مختلفاً عن 0.95 المنتظم. يخفي المتوسط الذيل الذي سيجده المستخدمون.

قائمة تحقق عملية

  • لكل مهمة مدقق آلي، حتى لو كان بدائياً.
  • كل تشغيل يُسجّل كاملاً، بما في ذلك الناتج الفاشل.
  • كل مجموعة نتائج تُوسم بتجزئة إيداع (commit hash) وملف تبعيات مقفل.
  • يُبلّغ عن pass^k إلى جانب pass@1، وليس بدلاً منه أبداً.
  • تُسجّل استدعاءات الأدوات وتكون قابلة لإعادة التشغيل.
  • يُجمّد الوقت واللغة ومعرّفات الجلسة أو تُحقن.
  • تُراجع الانحدارات لكل مهمة، مرتبة حسب الفرق (delta)، قبل أي تجميع.

الخاتمة

"لقد نجح" فرضية، وليس نتيجة. السؤال الذي يطرحه منشور IBM Research — هل سيفعلها مرة أخرى؟ — هو السؤال الصحيح لطرحه على أي وكيل قبل أن يلمس الإنتاج، والإجابة عنه لا تتطلب إطار عمل جديداً. إنها تتطلب حلقة، ومدققاً، وسجلاً، واستعداداً للإبلاغ عن رقم pass^k بدلاً من أفضل تشغيل.

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

المصادر