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

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

درس تعليمي: مُصنِّف أو مُخرَج منظَّم

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

ما الذي ستبنيه

روبوت دعم يعيد كائن JSON فيه answer وlabel (refund أو account أو other). ستُخضعه لثلاثة متطلبات: أن يكون الوسم صحيحًا بما يكفي من المرات، وأن يكون للمُخرَج دائمًا الشكل الصحيح، وألّا تُسرِّب أي إجابة شيئًا يشبه رقم ضمان اجتماعي أمريكي. اثنان منها فحصان للتنسيق لا يحتاجان إلى إجابة مرجعية؛ وواحد يقيس نجاح المهمة مقابل وسم مرجعي. والفرق مهم، وهذه الصفحة تُبقيهما منفصلين.

المصطلحات المستخدمة أدناه (الحالة، التشغيل، المقياس، الفترة، القاعدة، البوابة) معرَّفة في المفاهيم.

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

  • Python 3.11 أو أحدث.
  • Oloproof، مثبَّتًا في بيئة افتراضية:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof
  • مشروع المثال والتغيير المرشَّح، وهما يأتيان مع الحزمة. انسخهما إلى مجلدين جديدين واعمل في الأول؛ وكل ملف مسرود أدناه أيضًا، فيمكنك كتابته بنفسك بدلًا من ذلك:
oloproof init --example support_bot support-classifier
oloproof init --example classification support-change
cd support-classifier

لا يُستخدم في أي موضع من هذه الصفحة مفتاح API ولا حساب لدى مزوِّد ولا وصول إلى الشبكة.

الملفات

support-classifier/
  app.py              the application under test (a Python callable)
  oloproof.yaml       the suite: dataset, system, evaluators
  release.yaml        the release policy: rules the run is decided against
  data/support.jsonl  18 cases, one JSON object per line
  rubrics/helpful.md  a judge rubric, unused here

شغّل كل أمر من المجلد support-classifier/. يحفظ Oloproof مخزنه في .oloproof/ هناك؛ احذف ذلك المجلد لتبدأ من جديد من لا شيء.

التطبيق ومحوِّله

يُوصَل إلى تطبيقك عبر محوِّل. وفي تطبيق Python يكون المحوِّل هو الدالة نفسها: يستوردها Oloproof، ويستدعيها مرة لكل حالة مع input الحالة، ويسجّل القاموس الذي تعيده بوصفه مُخرَج تلك الحالة.

# app.py
from typing import Any

from oloproof import system


@system(name="support-bot", version="slice-a-example")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if "refund" in question:
        return {"answer": "Refunds are available within 30 days when the order is eligible.",
                "label": "refund"}
    if "password" in question or "login" in question:
        return {"answer": "Use password reset, then contact support if the login still fails.",
                "label": "account"}
    return {"answer": "A support specialist will follow up with the next step.", "label": "other"}

لتقييم مُصنِّفك أنت، أبقِ شيفرته حيث هي واكتب دالة رفيعة كهذه تستدعيه وتعيد قاموسًا. ويجوز أن تكون الدالة async. يستدعيها Oloproof؛ لكنه لا يستضيف تطبيقك ولا يعزله ولا يعيد ضبطه، فأي حالة يحتفظ بها تطبيقك بين الاستدعاءات مسؤوليتك أنت.

يسمّي oloproof.yaml تلك الدالة والمُقيِّمات:

version: 1
project: support-bot-example
dataset: data/support.jsonl
system:
  name: support-bot
  version: slice-a-example
  callable: app:answer
  timeout_s: 30
evaluators:
  - type: exact_match
    criterion: exact_label
    field: label
  - type: json_schema
    criterion: format_valid
    field: null
    schema:
      type: object
      required: [answer, label]
      properties:
        answer: {type: string}
        label: {type: string}
      additionalProperties: false
  - type: regex
    criterion: pii_free
    field: answer
    pattern: '\b\d{3}-\d{2}-\d{4}\b'
    pass_if: no_match

تُخزَّن المُخرَجات مؤقتًا وفق مصدر الدالة وversion المُعلَنة وconfig. وإذا كانت الدالة تقرأ ملفات أخرى (موجّهًا، أو جدول قواعد)، فاسردها تحت system.code_paths، كي يؤدي تعديلها إلى تشغيل النظام من جديد.

مجموعة البيانات

حالة واحدة في كل سطر. input هو بالضبط ما تتلقاه دالتك بوصفه case؛ وexpected هو المرجع الذي يقارن به المُقيِّم exact_match:

{"id":"refund_00","input":{"question":"Can I get a refund for yesterday's order?"},"expected":{"label":"refund"}}
{"id":"account_04","input":{"question":"I can't sign in on my new phone."},"expected":{"label":"account"}}
{"id":"other_04","input":{"question":"I don't want a refund, I just need a copy of my receipt."},"expected":{"label":"other"}}

تعيد الدالة، لكل حالة، كائنًا مثل {"answer": "Use password reset, ...", "label": "account"}.

اختيار المُقيِّمات

المعيارالمُقيِّميحتاج إلى expectedما يقيسه
exact_labelexact_match على labelنعمنجاح المهمة: الوسم هو الصحيح
format_validjson_schema على المُخرَج كلهلاالتنسيق: للكائن الحقلان النصيان بالضبط
pii_freeregex على answer، pass_if: no_matchلاخاصية أمان في النص

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

السياسة

release.yaml هو ما يُحسم التشغيل مقابله:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
warn_on: []
rules:
  - id: exact-label-floor
    metric: exact_label
    min: 0.70
  - id: valid-format
    metric: format_valid
    kind: observed_count
    max_failures: 0
  - id: pii-free
    metric: pii_free
    kind: observed_count
    max_failures: 0

تقول exact-label-floor إن الوسم يجب أن يكون صحيحًا في 70% من المرات على الأقل، ولا تنجح إلا حين تكون فترة 95% كلها عند 0.70 أو فوقها. ولا تسمح قاعدتا observed_count بأي إخفاق على الإطلاق في الحالات التي شغّلتها؛ فهما تصفان هذه الحالات، لا كل سؤال سيطرحه المستخدمون.

شغّله

oloproof run

المُخرَج الحقيقي، مختصرًا:

Run run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ exact-label-floor │ exact_label  │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold    │
│ valid-format      │ format_valid │ PASS                  │ observed_failures_within_limit │
│ pii-free          │ pii_free     │ PASS                  │ observed_failures_within_limit │
│ exact_label  │ 72.2%    │ [46.5%, 90.4%]  │ 13 / 18 observed · 0 missing · 0 excluded │
│ format_valid │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
│ pii_free     │ 100.0%   │ [81.4%, 100.0%] │ 18 / 18 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/18 miss; judgment 0 hit/54 miss

كيف تقرؤه:

  • 13 من 18 وسمًا صحيحة، أي 72.2%. هذا أعلى من 0.70، لكن الفترة تنزل حتى 46.5%: لا تستطيع 18 حالة أن تُظهر أن المعدل الحقيقي لا يقل عن 0.70. لذا فالقاعدة INSUFFICIENT_EVIDENCE، لا PASS ولا FAIL.
  • كل مُخرَج له الشكل الصحيح ولا يحتوي أي منها على رقم يشبه SSN، فتنجح قاعدتا التنسيق كلتاهما.
  • يسرد block_on الحالة INSUFFICIENT_EVIDENCE، فتحجب البوابة ويخرج الأمر بالرمز 3. والخروج 0 يعني أنه لا شيء مما تحجب عليه السياسة؛ ويسرد الحجب بالبوابة في CI كل الرموز.

شغّله مرة أخرى فيقرأ سطر التخزين المؤقت execution 18 hit/0 miss: لم يتغير شيء، فلا تُستدعى الدالة.

افحص الإخفاقات

oloproof inspect RUN_ID --failures
oloproof inspect RUN_ID --case refund_04

RUN_ID هو المعرّف في السطر الأول من مُخرَج التشغيل.

5 of 18 cases failed, errored or did not finish

refund_04
  output: {"answer": "A support specialist will follow up with the next step.", "label": "other"}
  exact_label: failed
...
other_04
  output: {"answer": "Refunds are available within 30 days when the order is eligible.", "label": "refund"}
  exact_label: failed
case refund_04
input: {
  "question": "I was charged twice this month and want my money back."
}
expected: {
  "label": "refund"
}
execution: OK, 1 ms
output: {
  "answer": "A support specialist will follow up with the next step.",
  "label": "other"
}
judgments:
  exact_label: failed
  format_valid: passed
  pii_free: passed

يتضح النمط حين تقرأ المدخلات: "money back" و"reverse the payment" و"sign in" و"two-factor" ليست في قوائم الكلمات المفتاحية، وother_04 تقول "I don't want a refund"، وهو ما تطابقه كلمة "refund" على أي حال. لاحظ أن refund_04 تجتاز فحصَي التنسيق وهي خاطئة: تلك هي الفجوة بين فحص التنسيق وقياس النجاح.

ثمة إجراءان تاليان لهما معنى هنا. أصلح الأخطاء (أدناه)، أو أضف حالات: مع حالات أكثر بالدقة نفسها تضيق الفترة، ويقدّر oloproof plan RUN_ID --run كم حالة.

أجرِ تغييرًا حقيقيًا

انسخ ../support-change/app.py فوق app.py. وهو يضيف الصيغ التي فاتت:

REFUND_WORDS = ("refund", "money back", "reverse the payment")
ACCOUNT_WORDS = ("password", "login", "sign in", "two-factor")


@system(name="support-bot", version="keywords-v2")
def answer(case: dict[str, Any]) -> dict[str, str]:
    question = str(case["question"]).lower()
    if any(word in question for word in REFUND_WORDS):
        ...

واضبط version: keywords-v2 تحت system في oloproof.yaml، كي يُسجَّل التشغيل بوصفه النسخة الجديدة. ثم:

oloproof run
Gate: ALLOW (exit 0)
│ exact-label-floor │ exact_label  │ PASS  │ lower_bound_meets_minimum      │
│ exact_label  │ 94.4%    │ [72.7%, 99.9%]  │ 17 / 18 observed · 0 missing · 0 excluded │

17 من 18 صحيحة والحد الأدنى للفترة، 72.7%، يتجاوز 0.70، فتنجح القاعدة ويخرج الأمر بالرمز 0. وما زالت other_04 تُخفق: لم يمسّ الإصلاح النفي.

قارن المرشَّح بخط الأساس

تسأل قاعدة التشغيل هل يبلغ المرشَّح الحد الأدنى الذي وضعته. أما المقارنة فتسأل كيف يختلف عن خط الأساس، حالةً حالة. انسخ ../support-change/compare.yaml إلى المشروع؛ وهو يحمل قاعدة مقارنة واحدة:

version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
  - id: label-no-regression
    kind: non_inferiority
    metric: exact_label
    margin: 0.10
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
Comparison sha256:de76... of run_01M4...TJAD against run_01M4...ECVEF · 18 paired cases
exact_label: +22.2 points [-12.9, +57.0] · 18 paired · 0 missing · 0 excluded
format_valid: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
pii_free: +0.0 points [-25.8, +25.8] · 18 paired · 0 missing · 0 excluded
Decisions
  label-no-regression  exact_label  non-inferiority, margin 10.0 points  INSUFFICIENT_EVIDENCE  interval_overlaps_margin
    about 3 more paired cases would decide it, if the difference holds (21 in total at 22% discordance)
Gate: BLOCK (exit 3)

أصلح المرشَّح أربع حالات ولم يُفسد أيًا منها، بمكسب مقدَّر بـ 22 نقطة. لكن أربع حالات فقط تغيّرت، و18 حالة مزدوجة تترك فترة من أسوأ بـ 12.9 نقطة إلى أفضل بـ 57 نقطة، وهي تعبر الهامش البالغ 10 نقاط. لا تستطيع المقارنة بعد أن تستبعد أن يكون المرشَّح أسوأ بأكثر مما تقبله، لذا فهي INSUFFICIENT_EVIDENCE وتخرج بالرمز 3. والسطر الذي تحتها هو تقدير الحجم. ومن دون --policy، يطبع compare الفروق، ويقول إن release.yaml في المشروع لا يعلن أي قاعدة مقارنة، ويخرج بالرمز 0 لأنه لم يُقرَّر شيء.

يشرح مقارنة مرشَّح بخط أساس الهامش وأنواع القواعد الأخرى.

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

العَرَضالسبب والإصلاح
ModuleNotFoundError لـ appشغّل من المجلد الذي يحتوي app.py، أو أعطِ callable مسار وحدة يمكن استيراده من هناك.
قاعدة تسمّي مقياسًا لا ينتجه أي مُقيِّميجب أن يساوي metric في القاعدة criterion أحد المُقيِّمات؛ والخطأ يسرد المقاييس الموجودة.
عدّلت المُصنِّف وأعاد التشغيل استخدام كل مُخرَجيتبع التخزين المؤقت مصدر الكائن القابل للاستدعاء؛ والملف المساعد الذي يقرؤه يجب أن يُسرد تحت system.code_paths.
يُبلغ exact_label عن حالات مفقودةرفعت عمليات التنفيذ تلك استثناءً أو انتهت مهلتها؛ ويعرض oloproof inspect RUN_ID --failures كل خطأ.
يخرج التشغيل بالرمز 3 مع تقدير مرتفعالفترة هي التي تحسم، لا التقدير. أضف حالات أو اقبل حدًا أدنى أقل، يُقرَّر قبل التشغيل.

القيود

  • يُبلغ SDK وYAML عن معدلات النجاح لكل مُقيِّم. ولا توجد مصفوفة التباس ولا دقة واستدعاء لكل فئة لمُصنِّف كهذا؛ تقدّم كتلة predictive: ذلك لنموذج يعطي درجات (النماذج التنبؤية).
  • تصف قواعد observed_count الحالات التي شغّلتها؛ ولا تدّعي شيئًا عن مدخلات لم تُرَ.
  • لا تفصل مقارنة على 18 حالة إلا الفروق الكبيرة. وخمسون حالة حقيقية أو أكثر حدٌّ أدنى أنفع.
  • لا يستطيع oloproof.yaml تسمية @evaluator مخصّص؛ فذلك يحتاج إلى SDK (SDK).