الانتقال إلى المحتوى

الأدلة الإرشادية

درس تعليمي: محادثة متعددة الأدوار

جولة قابلة للتشغيل لتقييم مساعد محادثة باستخدام Python SDK: يعيد تطبيقك تشغيل محادثة مكتوبة سلفًا على جلسة جديدة، ويسجّل ما أجاب به بوصفه مُخرَجًا فنيًا من نوع conversation/v1، ويحكم مُقيِّمان على كل محادثة كاملة. تعمل دون اتصال مع بديل مكتوب سلفًا لنموذج الحَكَم، ثم تقارن إصلاحًا مرشَّحًا، وتنتهي بالبديل المتمثل في تقييم الأدوار بوصفها حالات عنقودية.

المصطلحات مثل الفترة وحالة القرار وإجراء الإصدار معرَّفة في المفاهيم؛ ويغطي تسجيل ما فعله النظام المُخرَجات الفنية.

ما يفعله Oloproof وما لا يفعله هنا

يفعل Oloproofيفعل تطبيقك
يخزّن المحادثات المكتوبة سلفًا بوصفها مجموعة البيانات، متطابقة لكل نظاميقود المحادثة: يطرح كل دور مكتوب بالترتيب
يتحقق من أن كل دور مكتوب قد أُجيب عنه (ConversationCompleted)يملك حالة الجلسة، ويبدأ جلسة جديدة لكل حالة، ويعيد ضبطها
يحكم على النص الكامل بنموذج (ConversationJudge)يقرر ما يحدث حين لا يستطيع المتابعة، ويسجّل أنه توقف
يحسب الفترات، ويقارن نظامين، ويحسم مقابل سياسةيسجّل المُخرَج الفني conversation/v1

ليس في Oloproof محاكٍ للمستخدم: فهو لا يكتب أبدًا دورًا للمستخدم، لذا فجانب المستخدم هو ما تكتبه مجموعة البيانات سلفًا. وليس فيه مقياس على مستوى الدور داخل محادثة مسجَّلة، ولا يستطيع إعادة تشغيل محادثة مسجَّلة على نظام جديد. ومُقيِّمات المحادثة موجودة في Python SDK فقط: ConversationCompleted وConversationJudge ليسا من أنواع المُقيِّمات في oloproof.yaml، لذا يستخدم هذا الدرس سكربتًا بدلًا من oloproof run.

المتطلبات المسبقة

  • Python 3.11 أو أحدث وpip install oloproof، كما في البدء السريع.
  • ملفات المثال، وهي تأتي مع الحزمة. انسخها إلى مجلد جديد كي يستقر مخزن التشغيل هناك:
oloproof init --example conversation ~/oloproof-conversation
cd ~/oloproof-conversation
الملفما هو
assistant.pyالتطبيق قيد الاختبار: مساعد للخطط مع حالة جلسة
systems.pyالمحوِّل: يعيد تشغيل سيناريو، ويسجّل conversation/v1
judge_offline.pyالبديل المكتوب سلفًا لنموذج الحَكَم
evaluate.pyيشغّل التقييم والمقارنة وبديل الأدوار
release.yamlالسياسة لتشغيل واحد
comparison.yamlالسياسة للمرشَّح مقابل خط الأساس
turns_release.yamlالسياسة لبديل الأدوار
data/conversations.jsonl40 محادثة مكتوبة سلفًا
data/turns.jsonlالمحادثات نفسها، حالة واحدة لكل دور

لا مفتاح، ولا شبكة، ولا تكلفة مزوِّد، حتى الخطوة الحية الاختيارية في النهاية.

التطبيق

يجيب assistant.py عن أسئلة حول ثلاث خطط تسعير. ويحتفظ بقطعة واحدة من الحالة، هي الخطة التي تدور حولها المحادثة، كي يستطيع سؤال متابعة مثل "Does that include SSO?" أن يحدد ما تعنيه "that". ولخط الأساس عيب متعمَّد: لا يتذكر الخطة، فيُجاب عن سؤال المتابعة بشأن الخطة الافتراضية. والمستخدم الذي يطلب شخصًا ينهي المحادثة بـ HandoffRequested.

class PlanAssistant:
    def __init__(self, *, remembers_plan):
        self.remembers_plan = remembers_plan
        self.reset()

    def reset(self):
        """Forget everything, so one conversation never leaks into the next."""
        self.current_plan = None

    def ask(self, question): ...

هذا هو الجزء الذي تستبدل به تطبيقك أنت: عميل روبوت دردشة، أو جلسة وكيل، أو جلسة HTTP إلى خدمتك. وأيًا كان، فهو يملك حالته وإعادة ضبطها؛ ولا يرى Oloproof إلا ما يسجّله المحوِّل.

مجموعة البيانات: السيناريو هو المُدخَل

سطر واحد من data/conversations.jsonl محادثة واحدة:

{"expected": {"plan": "enterprise"}, "id": "conv_00", "input": {"turns": ["What does the enterprise plan cost?", "Does that include SSO?"]}, "metadata": {"pattern": "pronoun_followup"}}

أدوار المستخدم محتوى من مجموعة البيانات، تغطيه بصمة مجموعة الاختبار، ومتطابق لكل نظام يُقاس عليه؛ وهذا ما يجعل نظامين قابلين للمقارنة. وexpected هو المرجع الذي يُعرض على الحَكَم. والمحادثات الأربعون هي 24 فيها سؤال متابعة لا يسمّي أي خطة، و12 تسمّي الخطة في كل دور، و4 تطلب شخصًا في الدور الثاني من ثلاثة.

المحوِّل

يبدأ systems.py جلسة جديدة لكل حالة، ويطرح كل دور مكتوب بالترتيب، ويسجّل ما عاد:

from oloproof import CONVERSATION, current_case, system


def replay(case, *, remembers_plan):
    script = [str(turn) for turn in case["turns"]]
    session = PlanAssistant(remembers_plan=remembers_plan)  # a new session per case
    turns = []
    truncated = False
    for index, question in enumerate(script, start=1):
        try:
            reply = session.ask(question)
        except HandoffRequested:
            truncated = True  # the recording stops here and says so
            break
        turns.append({"index": index, "asked": question, "answer": reply["answer"]})
    current_case().artifact(
        CONVERSATION,
        {"turns": turns, "declared_turns": len(script), "truncated": truncated},
    )
    last = turns[-1]["answer"] if turns else None
    return {"answer": last, "turns_answered": len(turns)}


@system(name="plan-assistant", version="baseline", records=(CONVERSATION,))
def baseline(case):
    return replay(case, remembers_plan=False)


@system(name="plan-assistant", version="candidate-remembers-plan", records=(CONVERSATION,))
def candidate(case):
    return replay(case, remembers_plan=True)

الجلسة الجديدة لكل حالة مهمة: يشغّل Oloproof الحالات بالتزامن ودون ترتيب ثابت، والجلسة المشتركة بين الحالات قد تسمح لحالة محادثة بالتسرب إلى أخرى. ويعلن records= أن النظام يسجّل المُخرَج الفني؛ ومن دونه تُرفض مُقيِّمات المحادثة قبل تشغيل أي شيء، بدلًا من احتساب كل حالة مفقودة.

المُخرَج الفني conversation/v1

ما سجّله خط الأساس لـ conv_00، من oloproof export RUN_ID (cases.jsonl في الحزمة):

{"declared_turns": 2, "truncated": false, "turns": [{"answer": "The enterprise plan costs a price agreed per contract.", "asked": "What does the enterprise plan cost?", "index": 1, ...}, {"answer": "The starter plan does not include SSO.", "asked": "Does that include SSO?", "index": 2, ...}]}

ولمحادثة طلبت شخصًا:

{"declared_turns": 3, "truncated": true, "turns": [{"answer": "The team plan costs $20 a month.", "asked": "What does the team plan cost?", "index": 1, ...}]}
الحقلالمعنى
turns[].indexأي دور مكتوب يجيب عنه هذا؛ متتالٍ بدءًا من 1
turns[].answerما أعاده المساعد، أي JSON
turns[].askedاختياري، للقراءة فقط؛ يطابق المحرك على index
turns[].retrievalاختياري، ما استرجعه ذلك الدور، بشكل retrieval/v1
declared_turnsكم دورًا أعلن السيناريو
truncatedيتوقف التسجيل قبل اكتمال السيناريو، أيًا كان ما قطعه

يُفحص المُخرَج الفني حين يُسجَّل: التسجيل الذي فيه أدوار أقل مما أُعلن يجب أن يقول truncated: true، والفهارس يجب أن تكون متتالية، ولا يستطيع التسجيل الإجابة عن أدوار أكثر مما طُرح عليه. والمُخرَج الفني المشوَّه يوقف التشغيل بـ SystemContractError.

المُقيِّمان، ولماذا كلاهما

  • ConversationCompleted حتمي: هل أجاب المساعد عن كل دور مكتوب؟ ويعمل أولًا لأن أي ادعاء آخر عن محادثة توقفت في الدور الأول من ثلاثة هو ادعاء عن محادثة مختلفة. والمحادثة المقطوعة تُخفق فيه؛ وهذه نتيجة، لا حالة مفقودة.
  • ConversationJudge حَكَم نموذج على النص الكامل، كل دور للمستخدم وللمساعد، لأن الإخفاقات التي يُلام عليها منتج محادثة علائقية: الإجابة التي تناقض إجابة من الدور السابق لا تكون خاطئة إلا بجانبها. ويُحكم على المحادثة المقطوعة بما سُجّل، ويخبر النص الحَكَمَ أين توقفت.
evaluators = [
    ConversationCompleted(),
    ConversationJudge(criterion="plan_coherent", provider=..., model=..., rubric_text=RUBRIC),
]

الحكم دون اتصال

يحتاج الحَكَم إلى نموذج. وللعمل دون شبكة، يمرّر judge_offline.py مزوِّدًا مكتوبًا سلفًا، وهو المساعد نفسه الذي تستخدمه اختبارات Oloproof ذاتها (FakeProvider من داخليات المحرك، لا من الواجهة العامة). ويجيب عن كل موجّه حَكَم بقاعدة ثابتة واحدة: النجاح حين يسمّي كل دور للمساعد الخطة التي يسمّيها المرجع. وهذا يجعل الأحكام حتمية والدرس قابلًا للتكرار. ولا يقيس شيئًا عن سلوك نموذج حَكَم حقيقي.

السياسة

release.yaml:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - {id: completion-floor, metric: conversation_completed, min: 0.75}
  - {id: coherence-floor, metric: plan_coherent, min: 0.80}

شغّله

python evaluate.py
baseline run run_...
  conversation_completed: 0.900 [0.763, 0.972] over 40 conversations
  plan_coherent: 0.400 [0.249, 0.567] over 40 conversations
  gate BLOCK (exit 3)
  completion-floor: PASS (lower_bound_meets_minimum)
  coherence-floor: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
  first failing conversation:
    conversation_completed: passed=True {'turns_recorded': 2, 'turns_declared': 2, 'truncated': False}
    plan_coherent: passed=False {'provider_model': 'offline-rule'}

كيف تقرؤه:

  • الاكتمال 36 من 40، وهي حالات التسليم الأربع. وحده الأدنى يتجاوز 0.75، فتلك القاعدة PASS.
  • الاتساق 40%، وقاعدته تقرأ INSUFFICIENT_EVIDENCE مع evaluator_not_validated، لا FAIL. فقواعد الحَكَم لا تحسم حتى يُقاس الحَكَم مقابل وسوم بشرية (require_validated_evaluators مفعَّل افتراضيًا؛ ويشرحه الحُكّام). ويظل التقدير معروضًا، ويظل دليلًا: لكنه لا يستطيع وحده أن يُصدر أو يحجب.
  • gate BLOCK (exit 3): تحجب السياسة على INSUFFICIENT_EVIDENCE. والخروج 3 هو تلك الحالة، لا إخفاق.

التحقق من البديل غير المتصل لا معنى له، فهو قاعدة كُتبت لهذا المثال. ومع حَكَم حقيقي، ضع وسومًا على عيّنة من التشغيل بـ oloproof review RUN_ID --criterion plan_coherent --by YOU --sample 20 ثم شغّل oloproof evaluators validate EVALUATOR_ID --by YOU.

افحص محادثة مُخفِقة

يكتب SDK في المخزن نفسه الذي يقرؤه CLI، .oloproof/ في المجلد الذي شغّلت منه:

oloproof inspect RUN_ID --case conv_00
output: {
  "answer": "The starter plan does not include SSO.",
  "turns_answered": 2
}
judgments:
  conversation_completed: passed
  plan_coherent: failed
    judge text, not verified:
      every answer is about enterprise: False

سأل المستخدم عن خطة enterprise وأُجيب عن سؤال المتابعة بشأن خطة starter. يسرد oloproof inspect RUN_ID --failures كل محادثة مُخفِقة؛ وأسئلة المتابعة الأربعة والعشرون الخالية من اسم الخطة تُخفق كلها بالطريقة نفسها. والإجراء التالي في التطبيق: احتفظ بالخطة في حالة الجلسة.

تغيير مرشَّح، والمقارنة

يضبط candidate في systems.py القيمة remembers_plan=True. ويشغّل evaluate.py النظامين على السيناريوهات نفسها ويقارنهما حالةً حالة وفق comparison.yaml:

rules:
  - {id: coherence-better, kind: superiority, metric: plan_coherent}
  - {id: completion-no-worse, kind: non_inferiority, metric: conversation_completed, margin: 0.05}
comparison, candidate minus baseline
  conversation_completed: +0.000 [-0.127, +0.127] over 40 pairs
  plan_coherent: +0.600 [+0.337, +0.817] over 40 pairs
  gate BLOCK (exit 3)
  coherence-better: INSUFFICIENT_EVIDENCE (evaluator_not_validated)
  completion-no-worse: INSUFFICIENT_EVIDENCE (interval_overlaps_margin)

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

البديل: الأدوار حالات، تُحلَّل عناقيد

الحكم على مستوى المحادثة يقول كم مرة سارت محادثة على ما يرام، لا أي دور أخطأ. ولأن Oloproof ليس فيه مقياس على مستوى الدور داخل محادثة مسجَّلة، فالطريق الآخر هو جعل كل دور حالة قائمة بذاتها وربط أدوار المحادثة الواحدة معًا بـ group_id:

{"expected": {"plan": "enterprise"}, "group_id": "conv_00", "id": "conv_00_t2", "input": {"history": ["What does the enterprise plan cost?"], "question": "Does that include SSO?"}}

يعيد النظام تشغيل التاريخ المكتوب سلفًا في جلسة جديدة، ثم يجيب عن الدور:

@system(name="plan-assistant-turns", version="baseline")
def turn_baseline(case):
    session = PlanAssistant(remembers_plan=False)
    for earlier in case["history"]:
        session.ask(str(earlier))
    return session.ask(str(case["question"]))

أدوار المحادثة الواحدة ليست مستقلة، لذا حين يكون لأي حالة group_id تُحلَّل مجموعة الاختبار بحسب العنقود بطريقة تقريبية يجب أن تقبلها السياسة (turns_release.yaml يضبط allow_approximate_methods: true؛ ويشرحه الحالات العنقودية):

turns as cases: turn_plan 0.667 [0.588, 0.749] over 72 turns
  gate BLOCK (exit 1)
  turn-plan-floor: FAIL (upper_bound_below_minimum)

المفاضلة:

حالة واحدة لكل محادثةحالة واحدة لكل دور، عنقودية
وحدة المعدلالمحادثات التي سارت جيدًاالأدوار المُجاب عنها بشكل صحيح
حجم العيّنة الفعليعدد المحادثاتما زال عدد المحادثات، لا الأدوار
أي دور أخفقاقرأ النص الكامللكل دور حكمه الخاص
التاريخ الذي يراه كل دورإجابات المساعد السابقة نفسهأدوار المستخدم السابقة في السيناريو، مُعاد تشغيلها
يلتقط الانجراف الناتج عن إجاباته السابقةنعملا، كل دور يبدأ من تاريخ مكتوب سلفًا
المُقيِّماتConversationCompleted، ConversationJudge (SDK فقط)أي مُقيِّم، في YAML أو SDK

يستبعد بديل الأدوار محادثات التسليم الأربع، فتأتي أدواره الاثنان والسبعون من 36 محادثة. وهنا يستطيع أن يحسم حيث لم يستطع الحَكَم، لأن ExactMatch حتمي ولا يحتاج إلى تحقق.

اختياري: نموذج حَكَم حي

تحتاج هذه الخطوة إلى نموذج مُقدَّم على جهازك. ولا يشغّلها الدرس غير المتصل ولا اختباره. ومع تشغيل Ollama وسحب llama3.1:

python evaluate.py --live

يستدعي الحَكَم حينها http://localhost:11434/v1 مع provider="openai_compatible". والخادم المحلي لا يحتاج إلى مفتاح ولا يرسل شيئًا خارج الجهاز. أما المزوِّد السحابي فيحتاج إلى مفتاحه في البيئة، ويرسل كل نص إلى ذلك المزوِّد ويكلّف مالًا لكل حكم. وتختلف أحكام النموذج الحقيقي عن أحكام البديل، لذا ستتغير الأرقام أعلاه، وتظل قواعده تقرأ evaluator_not_validated حتى تتحقق منه.

استكشاف الأخطاء وإصلاحها

العَرَضالسبب والإصلاح
this evaluator needs exactly one conversation/v1 artifact; the case recorded 0لم يستدعِ المحوِّل current_case().artifact(CONVERSATION, ...)، أو رفع استثناءً قبله. سجّل حتى حين تتوقف المحادثة مبكرًا.
malformed conversation/v1 artifact: ... 0 of 2 turns recorded and truncated is falseيتوقف التشغيل بـ SystemContractError. والتسجيل الذي فيه أدوار أقل من declared_turns يجب أن يضبط truncated: true.
conversation turn indexes must be contiguous starting at 1رقّم الأدوار 1، 2، 3 بحسب الدور المكتوب الذي تجيب عنه.
evaluator 'conversation_completed' needs conversation/v1 artifacts, but system ... does not declare that it records themأضف records=(CONVERSATION,) إلى المُزخرِف @system.
Input tag 'conversation_completed' found using 'type' does not match any of the expected tags من oloproof runمُقيِّمات المحادثة في SDK فقط. استخدم سكربتًا كما هنا.
تتسرب الإجابات بين المحادثاتجلسة مشتركة بين الحالات. أنشئ واحدة لكل حالة.
قواعد الاتساق لا تحسم أبدًاالحَكَم غير مُتحقَّق منه. تحقق منه، أو اضبط require_validated_evaluators: false عن وعي.

القيود

  • لا محاكي للمستخدم: كل دور للمستخدم يأتي من سيناريو مجموعة البيانات، فلا تستطيع المحادثة التفرع بحسب ما قاله المساعد.
  • لا مقياس على مستوى الدور داخل محادثة مسجَّلة؛ استخدم الأدوار حالات عنقودية بدلًا من ذلك، مع المفاضلة أعلاه.
  • لا إعادة تشغيل للمحادثات: لا يمكن إعادة تشغيل محادثة مسجَّلة على نظام آخر. ومقارنة نظامين تعني أن يعيد كل منهما تشغيل السيناريو نفسه.
  • ConversationCompleted وConversationJudge في Python SDK فقط.
  • نص فقط: يُعرض على الحَكَم نص JSON، لا صور ولا صوت أبدًا.
  • الحَكَم غير المتصل قاعدة مكتوبة سلفًا. وأحكامه تُظهر الآلية، لا دقة حَكَم حقيقي.

إلى أين بعد ذلك