الأدلة الإرشادية
درس تعليمي: مُصنِّف أو مُخرَج منظَّم
قيّم دالة 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_label | exact_match على label | نعم | نجاح المهمة: الوسم هو الصحيح |
| format_valid | json_schema على المُخرَج كله | لا | التنسيق: للكائن الحقلان النصيان بالضبط |
| pii_free | regex على 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_04RUN_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: failedcase 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 runGate: 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.10oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yamlComparison 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).