指南
教學:多輪對話
一份使用 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.jsonl | 40 段腳本化的對話 |
| 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 是展示給評審的參考。這 40 段對話中,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,不是公開 API)。它按一條固定規則回答每個評判提示:當每個助手輪次都指明瞭參考所指明的套餐時就通過。這讓評判結果是確定性的,教學是可復現的。它完全不能量測真實評審的表現。
政策
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.pybaseline 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'}如何閱讀:
- 完成率是 40 中的 36,即那四次轉人工。它的下界越過了 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_00output: {
"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使用者問的是企業版套餐,而追問卻按入門版套餐來回答。oloproof inspect RUN_ID --failures 列出每段失敗的對話;全部 24 個沒有套餐名稱的追問都以同樣的方式失敗。下一步在應用裡:把套餐保存在會話狀態中。
一次候選變更,以及比較
systems.py 中的 candidate 設置了 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 中 |
按輪次的替代做法排除了那四段轉人工的對話,所以它的 72 個輪次來自 36 段對話。在這裡,它能在評審無法決策的地方作出決策,因為 ExactMatch 是確定性的,不需要驗證。
可選:實時評審
這一步需要一個在你的機器上提供服務的模型。離線教學及其測試不會執行它。在 Ollama 執行並已拉取 llama3.1 的情況下:
python evaluate.py --live然後評審以 provider="openai_compatible" 呼叫 http://localhost:11434/v1。迴環地址上的伺服器不需要密鑰,也不會把任何東西發出本機。雲端提供者需要在環境中提供它的密鑰,會把每段對話記錄發送給該提供者,並且每次評判都要花錢。真實模型的結論與替身的不同,所以上面的數字會改變,而在你驗證它之前,它的規則仍然顯示 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 | 在 @system 裝飾器中加入 records=(CONVERSATION,)。 |
| oloproof run 報出 Input tag 'conversation_completed' found using 'type' does not match any of the expected tags | 對話評估器僅限 SDK。像這裡一樣使用腳本。 |
| 回答在對話之間洩漏 | 案例之間共享了一個會話。為每個案例建立一個。 |
| 連貫性規則從不作出決策 | 評審未經驗證。驗證它,或在知情的情況下設置 require_validated_evaluators: false。 |
侷限
- 沒有使用者模擬器:每個使用者輪次都來自資料集腳本,所以對話無法根據助手說了什麼而產生分支。
- 在一段記錄下來的對話內部沒有輪次級指標;改為把輪次作為叢集案例,並接受上面的取捨。
- 沒有對話重放:記錄下來的對話無法針對另一個系統重新執行。比較兩個系統意味著每個系統都重放同一個腳本。
- ConversationCompleted 和 ConversationJudge 僅限 Python SDK。
- 僅限文字:展示給評審的是 JSON 文字,從不是圖像或音頻。
- 離線評審是一條腳本化的規則。它的結論展示的是機制,而不是真實評審的準確性。
下一步
- 評審介紹提供者、驗證和重新校準。
- 叢集案例介紹 group_id 以及近似方法的選擇性啟用。
- Python API介紹 evaluate 和 evaluate_comparison。
- 代理介紹代理工具循環的同一邊界。