跳至主要內容

指南

教學: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
  • 範例專案,它隨軟體包一起提供。把它複製到一個新目錄並在那裡工作(client.py 導入的 httpx 會隨 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 8765
intake 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 上設定請求頭和身份驗證。下面關於token的一節展示了變通辦法。

資料集

{"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 是一個真正的參考值,意思是“這條消息中沒有訂單 id”。

選擇評估器

判據評估器需要 expected量測
format_valid對輸出使用 json_schema否格式:一個已知的意圖和一個格式正確的 id
intent_correct對 intent 使用 exact_match是第一個欄位的任務成功
order_id_correct對 order_id 使用 exact_match是第二個欄位的任務成功

對每條消息都返回 {"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 run
Run 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

每個響應的格式都正確。每個欄位在 20 次中正確 15 次,但 20 個案例留下的區間向下延伸到 50.8%,所以兩個 70% 的下限都無法證明:INSUFFICIENT_EVIDENCE,閘門以結束代碼 3 阻止。

檢視失敗項

oloproof inspect RUN_ID --failures
8 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),就會出現兩個缺陷:id 模式只匹配“order 1234”,而不匹配“order #5120”、“order no. 8123”或單獨的“#1673”;而像“send back”、“stop order”和“where's my parcel”這樣的說法映射不到任何意圖。這就是下一步:放寬這兩條規則。

部署一次變更

停止服務並啟動候選版本,它帶有這些修復:

python server.py --port 8765 --rules v2

在 oloproof.yaml 中把 version: rules-v1 改為 version: rules-v2,因為 URL 沒有變化,否則 Oloproof 會複用舊的輸出。然後:

oloproof run
Gate: 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_correct
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yaml
format_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 個配對案例上,提升的區間仍然包含零。兩種說法同時成立。“滿足要求”和“勝過基準”是兩個不同的問題,而這麼小的套件只能對很大的效應回答第二個問題。更多真實消息才是解決辦法。

需要token的服務

啟動服務,讓它要求一個 bearer token:

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 從環境變數中添加請求頭,所以token永遠不會進入 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"]

把 oloproof.yaml 中的 http: 塊替換為:

system:
  name: order-intake
  version: rules-v2
  callable: client:extract

並在環境中帶上token執行:

INTAKE_TOKEN=s3cret oloproof run

全部 20 個案例再次被觀測到,閘門放行。可呼叫物件的快取跟隨它自己的原始碼和 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在 system 或 system.http 下添加 version。

侷限

  • system.http 上沒有自定義請求頭、身份驗證、查詢參數或請求模板:案例的 input 原樣作為 JSON 請求體。其他任何需求都請使用可呼叫物件。
  • 響應必須是 JSON。流式響應不會作為流來讀取。
  • Oloproof 無法檢測部署;宣告的 version 就是應答者的全部身份。
  • 你的服務自己負責其狀態:Oloproof 發送請求並記錄應答;它不會重置、隔離或回滾請求所改變的任何東西。