跳至主要內容

指南

教學:以模擬顧客測試客服台

以會對助理所說的話作出反應的使用者來評估一個對話助理。三十位模擬顧客,各有一個目標和自己知道的資訊,與一家商店的客服台對話;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 run
Gate: 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.yaml
no-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 run
Gate: 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.yaml
goal_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: 6

Ollama 一次只回答一個請求,所以設定 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 會記錄經過模擬使用者傳遞的內容。

侷限

  • 替身顧客是一條規則。它的對話示範的是機制;真實模型的對話更雜亂,正如可選步驟所示。
  • 目標是否達成是模擬使用者的模型的判斷,只透過真實感標籤來核對。
  • 託管工作區目前還不會依據模擬使用者作出決策。
  • 模擬顧客不是真實顧客:一個通過驗證的模擬使用者,是關於與其角色設定相似的顧客的證據,以人們認定它真實的程度為限,僅此而已。