الأدلة الإرشادية
درس تعليمي: تطبيق خلف نقطة نهاية HTTP
قيّم خدمة تصل إليها عبر HTTP، دون استيراد شيفرتها: وجّه Oloproof إلى عنوان URL، وشغّل مجموعة الاختبار، واعثر على ما يفوتها، وانشر تغييرًا، وقارن. تحل خدمة محلية صغيرة محل خدمتك، فيعمل كل شيء دون اتصال.
ما الذي ستبنيه
خدمة لاستقبال الطلبات تقرأ رسالة عميل وتستخرج حقلين: intent (where_is_order أو cancel أو return أو other) وorder_id (أربعة أرقام، أو null). ستفحص تنسيق كل استجابة، وهو ما لا يحتاج إلى مرجع، وتقيس هل كل حقل صحيح، وهو ما يحتاج إليه. والمصطلحات مثل الحالة والتشغيل والمقياس والبوابة معرَّفة في المفاهيم.
المتطلبات المسبقة
- Python 3.11 أو أحدث، وOloproof في بيئة افتراضية:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- مشروع المثال، وهو يأتي مع الحزمة. انسخه إلى مجلد جديد واعمل هناك (httpx، الذي يستورده client.py، يُثبَّت مع Oloproof):
oloproof init --example http order-intake
cd order-intake- المنفذ 8765 شاغر على هذا الجهاز. وإن كان مشغولًا، فاختر غيره وغيّره في أمر الخادم وفي oloproof.yaml كليهما.
لا شيء هنا يستخدم مزوِّدًا أو مفتاح API أو الإنترنت: تستمع الخدمة على 127.0.0.1.
الملفات
order-intake/
server.py the stand-in service (Python standard library only)
client.py a callable that calls the service with a token (used near the end)
oloproof.yaml the suite: dataset, HTTP system, evaluators
release.yaml rules for a run
compare.yaml a rule for a comparison
data/messages.jsonl 20 casesشغّل الأوامر من order-intake/. شغّل الخدمة في طرفية ثانية واتركها تعمل:
python server.py --port 8765intake service (v1) on http://127.0.0.1:8765/extractعقد HTTP
لكل حالة يرسل Oloproof طلبًا واحدًا: input الحالة جسمًا بصيغة JSON، بالطريقة التي تعلنها (POST افتراضيًا). ويقرأ الاستجابة بوصفها JSON. والحالة 400 أو أعلى، أو انتهاء المهلة، أو رفض الاتصال، يُسجَّل خطأ تنفيذ لتلك الحالة، لا إجابة خاطئة أبدًا.
الطلب والاستجابة لحالة واحدة:
POST /extract
{"message": "Where is order 1042? It has not arrived."}
200 OK
{"result": {"intent": "where_is_order", "order_id": "1042"}, "service": {"rules": "v1"}}يخبر output_path: result Oloproof بألّا يحتفظ إلا بـ result مُخرَجًا للحالة؛ ومن دونه يكون الجسم كله هو المُخرَج. ومسار منقوط مثل data.answer يصل إلى عمق أكبر.
version: 1
project: order-intake
dataset: data/messages.jsonl
system:
name: order-intake
http:
url: http://127.0.0.1:8765/extract
method: POST
version: rules-v1
output_path: result
timeout_s: 30
evaluators:
- type: json_schema
criterion: format_valid
field: null
schema:
type: object
required: [intent, order_id]
properties:
intent: {enum: [where_is_order, cancel, return, other]}
order_id: {type: [string, "null"], pattern: '^\d{4}$'}
additionalProperties: false
- type: exact_match
criterion: intent_correct
field: intent
- type: exact_match
criterion: order_id_correct
field: order_idيجب أن يعلن نظام HTTP version. لا يستطيع Oloproof رؤية عملية نشر: فهو يخزّن مُخرَج كل حالة مؤقتًا وفق عنوان URL والطريقة ومسار المُخرَج وتلك النسخة، لذا فالنسخة هي طريقتك لإخباره بأن الخدمة تغيّرت. وإن نسيت تغييرها فلن تُستدعى عملية النشر الجديدة أبدًا.
لا يمكن بعد تهيئة الترويسات والمصادقة على system.http. ويبيّن القسم الخاص بالرموز المميزة أدناه طريقة الالتفاف على ذلك.
مجموعة البيانات
{"id":"m04","input":{"message":"Has order #5120 shipped yet?"},"expected":{"intent":"where_is_order","order_id":"5120"}}
{"id":"m07","input":{"message":"Do you ship to Canada?"},"expected":{"intent":"other","order_id":null}}
{"id":"m16","input":{"message":"Please refund and take back the lamp from order #1560."},"expected":{"intent":"return","order_id":"1560"}}input هو جسم الطلب بالضبط. وexpected يحمل المرجع لكل حقل؛ وnull قيمة مرجعية حقيقية، تعني "لا يوجد معرّف طلب في هذه الرسالة".
اختيار المُقيِّمات
| المعيار | المُقيِّم | يحتاج إلى expected | يقيس |
|---|---|---|---|
| format_valid | json_schema على المُخرَج | لا | التنسيق: نية معروفة ومعرّف سليم الشكل |
| intent_correct | exact_match على intent | نعم | نجاح المهمة للحقل الأول |
| order_id_correct | exact_match على order_id | نعم | نجاح المهمة للحقل الثاني |
كان المخطط سيُنجح {"intent": "other", "order_id": null} لكل رسالة: سليم الشكل وعديم الفائدة. وفحوص المرجع وحدها تقول هل أدّت الخدمة عملها. وتقييم الحقلين منفصلين يُظهر أيهما يُخفق، وهو ما كان فحص مجمَّع واحد سيخفيه.
لا يسمح release.yaml بأي إخفاق في التنسيق ويطلب أن يكون كل حقل صحيحًا في 70% من المرات على الأقل، محكومًا عليه بفترة 95%:
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
- id: valid-format
metric: format_valid
kind: observed_count
max_failures: 0
- id: intent-floor
metric: intent_correct
min: 0.70
- id: order-id-floor
metric: order_id_correct
min: 0.70شغّله
oloproof runRun run_01M4... [DECIDED/COMPLETE]
Gate: BLOCK (exit 3)
│ valid-format │ format_valid │ PASS │ observed_failures_within_limit │
│ intent-floor │ intent_correct │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ order-id-floor │ order_id_correct │ INSUFFICIENT_EVIDENCE │ interval_overlaps_threshold │
│ format_valid │ 100.0% │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ intent_correct │ 75.0% │ [50.8%, 91.4%] │ 15 / 20 observed · 0 missing · 0 excluded │
│ order_id_correct │ 75.0% │ [50.8%, 91.4%] │ 15 / 20 observed · 0 missing · 0 excluded │
Cache: execution 0 hit/20 miss; judgment 0 hit/60 missكل استجابة سليمة الشكل. وكل حقل صحيح 15 مرة من 20، لكن 20 حالة تترك فترة تنزل حتى 50.8%، فلا يثبت أيٌّ من الحدين الأدنيين 70%: INSUFFICIENT_EVIDENCE، وتحجب البوابة برمز الخروج 3.
افحص الإخفاقات
oloproof inspect RUN_ID --failures8 of 20 cases failed, errored or did not finish
m04
output: {"intent": "where_is_order", "order_id": null}
order_id_correct: failed
m06
output: {"intent": "other", "order_id": "7011"}
intent_correct: failed
...
m18
output: {"intent": "other", "order_id": null}
intent_correct: failed
order_id_correct: failedاقرأ المدخلات بجانبها (oloproof inspect RUN_ID --case m04) فيظهر عيبان: نمط المعرّف لا يطابق إلا "order 1234"، لا "order #5120" ولا "order no. 8123" ولا "#1673" وحدها؛ وصيغ مثل "send back" و"stop order" و"where's my parcel" لا تقابل أي نية. ذلك هو الإجراء التالي: وسّع القاعدتين.
انشر تغييرًا
أوقف الخدمة وشغّل المرشَّح، الذي يحمل تلك الإصلاحات:
python server.py --port 8765 --rules v2غيّر version: rules-v1 إلى version: rules-v2 في oloproof.yaml، لأن عنوان URL لم يتغير وإلا لأعاد Oloproof استخدام المُخرَجات القديمة. ثم:
oloproof runGate: ALLOW (exit 0)
│ intent_correct │ 100.0% │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │
│ order_id_correct │ 100.0% │ [83.1%, 100.0%] │ 20 / 20 observed · 0 missing · 0 excluded │ينجح الحدّان الأدنيان كلاهما ويخرج الأمر بالرمز 0.
قارن عمليتي النشر
يسأل compare.yaml هل نوايا المرشَّح أفضل من نوايا خط الأساس:
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
rules:
- id: intent-better
kind: superiority
metric: intent_correctoloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yamlformat_valid: +0.0 points [-23.6, +23.6] · 20 paired · 0 missing · 0 excluded
intent_correct: +25.0 points [-8.5, +58.3] · 20 paired · 0 missing · 0 excluded
order_id_correct: +25.0 points [-8.5, +58.3] · 20 paired · 0 missing · 0 excluded
Decisions
intent-better intent_correct superiority INSUFFICIENT_EVIDENCE interval_overlaps_zero
Gate: BLOCK (exit 3)يجتاز المرشَّح حدّيه الأدنيين، لكن المقارنة لا تستطيع أن تُظهر أنه أفضل: تغيّرت خمس حالات، وعلى 20 حالة مزدوجة ما زالت فترة المكسب تتضمن الصفر. والعبارتان صحيحتان معًا. "يلبّي المتطلب" و"يتفوق على خط الأساس" سؤالان منفصلان، ومجموعة اختبار بهذا الصغر لا تجيب عن الثاني إلا للآثار الكبيرة. والعلاج مزيد من الرسائل الحقيقية.
خدمة تحتاج إلى رمز مميز
شغّل الخدمة بحيث تطلب رمزًا مميزًا من نوع bearer:
INTAKE_TOKEN=s3cret python server.py --port 8765 --rules v2 --require-tokenلا يرسل system.http ترويسات مخصّصة، لذا مع version جديدة (لنقل rules-v2-auth) يُرفض كل استدعاء:
Gate: BLOCK (exit 3)
│ valid-format │ format_valid │ INSUFFICIENT_EVIDENCE │ no_observations │
│ intent_correct │ │ [0.0%, 100.0%] │ 0 / 0 observed · 20 missing · 0 excluded │ويُظهر oloproof inspect RUN_ID --failures الرسالة execution ERROR: TransientError: system returned HTTP 401 في كل حالة. وأخطاء التنفيذ أدلة مفقودة، لا إخفاقات: لم يُرصد شيء، فتكون كل قاعدة INSUFFICIENT_EVIDENCE.
والالتفاف هو كائن Python قابل للاستدعاء يرسل الطلب بنفسه. يضيف client.py الترويسة من متغير بيئة، فلا يدخل الرمز المميز أبدًا في oloproof.yaml ولا في أي سجل مخزَّن:
URL = os.environ.get("INTAKE_URL", "http://127.0.0.1:8765/extract")
def extract(case: dict[str, Any]) -> dict[str, Any]:
headers = {"Authorization": f"Bearer {os.environ['INTAKE_TOKEN']}"}
response = httpx.post(URL, json=case, headers=headers, timeout=30)
response.raise_for_status()
return response.json()["result"]استبدل كتلة http: في oloproof.yaml بـ:
system:
name: order-intake
version: rules-v2
callable: client:extractوشغّل مع الرمز المميز في البيئة:
INTAKE_TOKEN=s3cret oloproof runتُرصد الحالات العشرون كلها من جديد وتسمح البوابة. ويتبع التخزين المؤقت للكائن القابل للاستدعاء مصدره وversion الخاصة به، لا الخدمة التي خلفه، فتنطبق القاعدة نفسها: غيّر version حين تنشر.
استكشاف الأخطاء وإصلاحها
| العَرَض | السبب والإصلاح |
|---|---|
| system connection failed (ConnectError) في كل حالة | الخدمة لا تعمل، أو تستمع على منفذ آخر. |
| system connection failed (RemoteProtocolError) | شيء آخر يجيب على ذلك المنفذ. اختر منفذًا شاغرًا. |
| KeyError: "missing output path 'results'" | يسمّي output_path حقلًا ليس في الاستجابة. |
| system returned HTTP 401 أو 403 | تحتاج نقطة النهاية إلى بيانات اعتماد: استخدم التفاف الكائن القابل للاستدعاء. |
| نشرت تغييرًا وسطر التخزين المؤقت كله إصابات | لم تتغير version، فأُعيد استخدام المُخرَجات المخزّنة. |
| an HTTP system needs a declared version | أضف version تحت system أو system.http. |
القيود
- لا ترويسات مخصّصة ولا مصادقة ولا معاملات استعلام ولا قوالب طلبات على system.http: input الحالة هو جسم JSON كما هو. استخدم كائنًا قابلًا للاستدعاء لأي شيء آخر.
- يجب أن تكون الاستجابات JSON. ولا تُقرأ الاستجابات المتدفقة بوصفها تدفقًا.
- لا يستطيع Oloproof اكتشاف عملية نشر؛ وversion المُعلَنة هي الهوية الكاملة لما أجاب.
- تملك خدمتك حالتها: يرسل Oloproof الطلبات ويسجّل الإجابات؛ ولا يعيد ضبط أي شيء يغيّره طلب، ولا يعزله ولا يتراجع عنه.