跳到主要内容

指南

教程:用模拟顾客测试客服台

用会对助手所说的话作出反应的用户来评估一个对话助手。三十位模拟顾客,各有一个目标和自己知道的信息,与一家商店的客服台对话;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 会记录经过模拟用户传递的内容。

局限

  • 替身顾客是一条规则。它的对话演示的是机制;真实模型的对话更杂乱,正如可选步骤所示。
  • 目标是否达成是模拟用户的模型的判断,只通过真实感标签来核对。
  • 托管工作区目前还不会依据模拟用户作出决策。
  • 模拟顾客不是真实顾客:一个通过验证的模拟用户,是关于与其角色设定相似的顾客的证据,以人们认定它真实的程度为限,仅此而已。