指南
教學:以模擬顧客測試客服台
以會對助理所說的話作出反應的使用者來評估一個對話助理。三十位模擬顧客,各有一個目標和自己知道的資訊,與一家商店的客服台對話;Oloproof 量測每位顧客是否得到了他們想要的東西,在有人認定這些模擬顧客真實之前暫緩每項決策,然後對一次發布和一次比較作出決策。每一步都在本機上執行,無需模型也無需連網,另有一個可選步驟會換用真實的模型來扮演顧客。
您將建置什麼
一家商店的客服台處理退款、配送問題和地址變更。三個問題決定一個版本能否發布:
- 每位顧客是否得到了他們想要的東西?goal_reached,來自每次回答之後的一項檢查。
- 是否有回答承諾了商店無法承諾的事情?every_turn 在每個回答中尋找「guarantee」這個詞。
- 這些回答能否被信任來說明真實顧客的任何情況?只有在有人讀過對話的一份樣本、並認定這些模擬顧客真實之後才能。
案例、執行、閘門和套件等術語在核心概念中有定義;關於模擬使用者的一切都在模擬使用者中。
事前準備
- Python 3.11 或更高版本,以及 Oloproof:
python3 -m venv .venv
. .venv/bin/activate
pip install oloproof- 範例專案,它隨軟體套件一起提供。把它複製到一個新目錄並在那裡工作:
oloproof init --example simulated-users support-desk
cd support-desk- 為替身模擬使用者空出 8797 連接埠(如果不行,就在兩處同時修改)。
在「可選:以真實模型扮演顧客」之前的每一步都是離線且確定性的:沒有 API 金鑰,沒有提供者帳號,沒有費用。
檔案
support-desk/
desk.py the support desk under test (baseline)
desk_v2.py the candidate change
user_server.py a stand-in simulated user speaking the OpenAI API on 127.0.0.1
oloproof.yaml the suite: the simulated user and four evaluators
release.yaml rules for a run, with the realism bar
compare.yaml a rule for a comparison
data/customers.jsonl 30 personas
labels/realism_verdicts.csv one person's realism verdicts on the baseline's conversations
fill_realism.py copies those verdicts into a labelling sheet
logged.py replays logged chats with human agents
real.yaml a suite of 20 of those chats
logged_release.yaml a rule for the logged chats
data/logged_chats.jsonl the 20 chats在 support-desk/ 中執行每條命令。
客服台保留自己的迴圈
# desk.py (excerpt)
def talk(case: dict[str, Any]) -> dict[str, Any]:
user = current_case().simulated_user()
desk = Desk()
message = user.opening()
while message is not None:
message = user.reply(desk.answer(message))
return {"need": desk.need, "order": desk.order}客服台索取顧客的第一則訊息,回答它,並把每個回答交回去以取得下一則訊息,直到顧客停下。它從不會看到角色設定。Oloproof 記錄雙方之間傳遞的內容;客服台自己什麼都不記錄。這個基準無法變更配送地址,並且它承諾退款「guaranteed within 5 days」。
角色設定
每個案例是一位顧客:12 位想要退款,10 位想知道訂單何時送達,8 位已經搬家。例如:
{"id": "c25", "input": {"persona": {"description": "A retired teacher who ordered a cookbook last week. Writes short, polite messages.", "goal": "Have the order of a cookbook delivered to my new address.", "facts": {"order": "B-35", "postcode": "10275"}}}}顧客只在被問到時才提供訂單編號和郵遞區號。有三個角色設定還設定了 opening,即他們逐字照錄的第一則訊息。
套件
version: 1
project: support-desk
dataset: data/customers.jsonl
system:
name: support-desk
version: baseline
callable: desk:talk
# A model plays each case's persona; here the stand-in in user_server.py.
simulated_user:
provider: openai_compatible
model: stand-in-user
base_url: http://127.0.0.1:8797/v1
max_turns: 6
evaluators:
# The desk answered every message the customer sent.
- type: conversation_completed
# The customer said they got what they came for.
- type: goal_reached
# ... within three of their own messages.
- type: turns_to_goal
max_turns: 3
# No answer promises what the shop cannot promise.
- type: every_turn
criterion: no_promises
not_contains: [guarantee]
casefold: true替身模擬使用者
模擬使用者是扮演每個角色設定的模型。這裡它是 user_server.py,它在 127.0.0.1 上使用同樣的 API,但它不是模型:它從 Oloproof 傳送的提示詞中讀取角色設定,說出自己的目標,在被問到時提供訂單編號或郵遞區號,在客服台幫不上忙時離開,而在被問到目標是否達成時,如果客服台的最後一個回答以「Done:」開頭,就回答是。在第二個終端機中啟動它並讓它保持執行:
python user_server.py --port 8797政策
version: 1
confidence_level: 0.95
block_on: [FAIL, INSUFFICIENT_EVIDENCE, MANUAL_REVIEW]
require_validated_evaluators: true
minimum_simulated_user_realism: 0.80
rules:
- id: no-promises
metric: no_promises
kind: observed_count
max_failures: 0
- id: resolved
metric: goal_reached
min: 0.80任何回答都不得作出承諾。至少 80% 的顧客必須達成目標,只有當區間的下界達到 80% 時才作出決策。並且在人們認定至少 80% 的對話真實之前(按同一種下界計算),沒有任何規則會依據模擬顧客作出決策。
執行
oloproof runGate: BLOCK (exit 3)
│ no-promises │ no_promises │ INSUFFICIENT_EVIDENCE │ simulated_user_not_validated │
│ resolved │ goal_reached │ INSUFFICIENT_EVIDENCE │ simulated_user_not_validated │
│ conversation_completed │ 100.0% │ [88.4%, 100.0%] │ 30 / 30 observed · 0 missing · 0 excluded │
│ goal_reached │ 73.3% │ [54.1%, 87.8%] │ 22 / 30 observed · 0 missing · 0 excluded │
│ goal_within_3_turns │ 73.3% │ [54.1%, 87.8%] │ 22 / 30 observed · 0 missing · 0 excluded │
│ no_promises │ 60.0% │ [40.6%, 77.4%] │ 18 / 30 observed · 0 missing · 0 excluded │每位顧客都得到了回答,30 位中有 22 位達成了目標,12 段對話包含承諾。目前還沒有作出任何決策:這些顧客是模擬的,還沒有人核實過他們的行為像真實顧客。
閱讀模擬顧客
oloproof simulated-users RUN_ID Realism: no conversation has been drawn and labelled yet.
Not validated: nothing labelled yet, against a bar of 80.0%.
Features (median [quartiles]; descriptive, never a gate):
user messages 2 [1, 2]
user message length 49 [10, 67]
How the simulated conversations ended: goal_reached 73%, user_left 27%離開的八位是已經搬家的顧客:基準無法變更地址。
為對話標註真實感
從執行的對話中隨機抽取一份樣本放進一張表,讓一個人閱讀每段對話,連同角色設定和對話如何結束,並標註一個真實顧客是否可能寫出顧客一方的內容,並在那裡、以那種方式結束對話。這裡 fill_realism.py 代替那個人,從 labels/realism_verdicts.csv 複製他們的結論:
oloproof labels export RUN_ID --criterion simulated_user_realistic --sample 30 --local --out realism.csv
python fill_realism.py realism.csv
oloproof labels import realism.csv
oloproof simulated-users RUN_ID Realism: 29 of 30 drawn conversations labelled realistic (30 labelled), [82.8%, 99.9%] at 95%
Validated: the lower bound clears the bar of 80.0%.這個人認定一段對話不真實:顧客 c05 以「Order: F-15. Refund.」開場,讀起來像一張表單,而不像一個人。30 段中有 29 段真實,精確下界為 82.8%,高於門檻,所以現在規則可以依據這些顧客作出決策。
oloproof gate RUN_ID --policy release.yamlno-promises: FAIL (observed_failures_exceed_limit)
resolved: INSUFFICIENT_EVIDENCE (interval_overlaps_threshold)
Gate: BLOCK (exit 1)做一次真正的修改
desk_v2.py 在拿到訂單編號和新的郵遞區號後就變更地址,並說明它已開始辦理一筆「usually arrives within 5 days」的退款,而不是作出承諾。把它複製到 desk.py 上覆蓋,在 oloproof.yaml 的 system 下設定 version: candidate,保持替身執行,然後:
oloproof runGate: ALLOW (exit 0)
│ no-promises │ no_promises │ PASS │ observed_failures_within_limit │
│ resolved │ goal_reached │ PASS │ lower_bound_meets_minimum │候選在同一個套件上遇到了同樣的模擬顧客,因此在基準的對話上量測的真實感也涵蓋它:一次驗證涵蓋一個模擬使用者在一個套件上的情況,無論它與哪個系統對話。
把候選與基準進行比較
oloproof compare CANDIDATE_RUN_ID BASELINE_RUN_ID --policy compare.yamlgoal_reached: +26.7 points [+1.2, +54.1] · 30 paired · 0 missing · 0 excluded
resolves-more goal_reached superiority PASS difference_above_zero每位顧客都遇到了兩個客服台,因此比較是逐位顧客配對的。
模擬與真實並列
真實感標籤單獨評判每段對話。把模擬顧客與真實顧客並列,可以顯示一份樣本可能遺漏的東西。real.yaml 重播了商店的真人客服與真實顧客的 20 段聊天:
oloproof run --config real.yaml --policy logged_release.yaml
oloproof simulated-users BASELINE_RUN_ID --real REAL_RUN_ID simulated (run_01M52Q1C) real (run_01M52Q25)
user messages 2 [1, 2] 3 [3, 4]
user message length 49 [10, 67] 13 [4, 32]真實顧客傳送的訊息更多也更短:他們分三行說出自己想要什麼,而替身用一句整齊的話說完。對任何模擬使用者來說這都值得了解,而它在這裡從不決定任何事情:比較兩個未配對的樣本需要一種 Oloproof 尚未採納的方法。
可選:以真實模型扮演顧客
這一步離開了離線路徑。它需要一個聊天模型:本機可以用 Ollama(ollama pull qwen2.5vl,或任何聊天模型),無需金鑰也無需費用;雲端可以用 provider: anthropic 或 openai 並帶上 api_key_env,按每則訊息計費。在 oloproof.yaml 中,讓模擬使用者指向它:
simulated_user:
provider: openai_compatible
model: qwen2.5vl
base_url: http://localhost:11434/v1
max_turns: 6Ollama 一次只回答一個請求,所以設定 concurrency: {system: 1},並把 timeout_s 設為幾分鐘。不同的模型就是不同的模擬使用者:上面的真實感標籤不涵蓋它,所以在任何規則依據它的對話作出決策之前,這些對話需要自己的樣本。
在本機上由 qwen2.5vl 扮演其中十個角色設定、與候選客服台對話,有 8 段對話達成了目標,而替身在全部 30 段中都達成了目標。模型發現了替身發現不了的問題:兩位顧客詢問訂單的「status」和「shipping status」,這些是客服台不認識的詞,每次得到的都只是它的問候語。其中一位用完了訊息則數。客服台聽懂了另一位的第三次嘗試,但那位顧客還是離開了:目標檢查,即模型的判斷,漏掉了那個回答。真實感標籤正是顯露這一點的地方:一個被告知訂單何時送達的真實顧客,不會不滿地離開。與替身不同,模型還經常在被問到之前就給出訂單編號,並且在替身寫兩則短訊息的地方寫一則較長的訊息。
疑難排解
| 症狀 | 原因與修復 |
|---|---|
| 每條規則都是 INSUFFICIENT_EVIDENCE,附帶 simulated_user_not_validated | 還沒有人在這個套件上標註過這個模擬使用者的對話,或者真實對話所占比例沒有達到門檻。按上文標註一份樣本。 |
| a case for a simulated user needs a 'persona' object in its input | 某個案例沒有角色設定,或者角色設定位於另一個鍵下:設定 persona_field。 |
| 每個案例都出現 provider connection failed | 替身沒有執行,或者監聽的連接埠與 base_url 指明的不同。 |
| the simulated user's goal check answered ... | 模型沒有以是或否回答目標檢查。換一個模型,或者換一個更大的模型。 |
| conversation/v1 is written by Oloproof | 客服台自己記錄了對話。移除這部分:Oloproof 會記錄經過模擬使用者傳遞的內容。 |
侷限
- 替身顧客是一條規則。它的對話示範的是機制;真實模型的對話更雜亂,正如可選步驟所示。
- 目標是否達成是模擬使用者的模型的判斷,只透過真實感標籤來核對。
- 託管工作區目前還不會依據模擬使用者作出決策。
- 模擬顧客不是真實顧客:一個通過驗證的模擬使用者,是關於與其角色設定相似的顧客的證據,以人們認定它真實的程度為限,僅此而已。