跳到主要内容

指南

模拟用户

脚本化的对话用一个从不作出反应的用户来测试你的助手。模拟用户会作出反应:一个模型扮演每个用例所描述的人,根据你的助手刚说的话写出下一条消息,并在目标达成或放弃时停下。Oloproof 记录双方之间传递的内容,像度量任何对话一样度量它,并且只有在人们认定模拟用户真实之后,才让门禁依据它作出决策。教程演示了一个客服台与模拟顾客的对话。

角色设定

每个用例的输入包含一个角色设定(persona):用户是谁,想要什么,知道什么。

{"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"}
}}}
  • description 和 goal 是必需的。写一个读者只凭对话就能核对的目标:“让商店开始退款”可以根据助手说的话来判定,“拿到退款”则不行,因为钱是之后才到账的。
  • facts 是用户知道、只在对话需要时才透露的信息,例如订单号。你的助手从不会看到角色设定。
  • opening 是用户的第一条消息,逐字照录,用于每个系统都应以同样方式被问候的情况。
  • max_turns 限制这个用户的消息条数,覆盖项目的设置。

角色设定是数据集内容:它包含在测试套件摘要中,对依据它度量的每个系统都相同。

声明模拟用户

扮演每个角色设定的模型在系统上声明,与评判模型一样:

system:
  name: support-desk
  callable: desk:talk
  simulated_user:
    provider: openai_compatible
    base_url: http://localhost:11434/v1
    model: qwen2.5vl
    max_turns: 6

每个字段都在配置参考中。本地模型不花钱;托管模型对它写的每条消息计费,它的 token 记录在对话旁边,从不计入你的系统的成本。

你的系统保留自己的循环

Oloproof 不驱动对话。你的系统索取用户的第一条消息,用它平时回答用户的方式回答,并把每个回答交回去以获得下一条消息:

from oloproof import current_case

def talk(case):
    user = current_case().simulated_user()
    message = user.opening()
    while message is not None:
        message = user.reply(my_assistant.answer(message))
    return {}

一旦用户停下,reply 返回 None。你的系统可以用 user.end(answer) 自己结束对话,就像转交给真人那样;async def 系统则 await aopening()、areply(answer) 和 aend(answer)。Oloproof 把经过该句柄传递的内容记录为 conversation/v1,把对话如何结束记录为 simulated_user/v1,并拒绝来自你的系统的这两者(SDK 参考)。

对话如何结束

在你的系统每次回答之后,都会单独向模拟用户的模型提一个问题,温度为 0,且不带周围的角色扮演:这个回答是否完成了目标。如果完成了,对话以 goal_reached 结束。否则用户写出下一条消息,或者离开(user_left),直到它已发送 max_turns 条消息(turn_limit)。如果系统没有回答最后一条消息就停下,对话会处于 unanswered 状态并被截断。

单独提问正是让小型本地模型可用的原因:扮演一个角色设定和判断目标是否达成是两项不同的任务,被要求在一次回复中同时完成两者的模型,往往会在目标达成后继续闲聊,或者在已经满意时说自己要离开。

度量对话

对话是单位:每个评估器对每段对话给出一个结论,因此每个区间、比较和门禁都原样适用。

类型通过条件
conversation_completed你的系统回答了每条消息
goal_reached目标检查发现目标已达成
turns_to_goal在用户的 max_turns 条消息之内做到了这一点
every_turn每个回答都满足声明的条件:必须包含或不得包含的词、一个模式、一个长度
conversation_judge一个评判整段对话记录的模型评判让它通过,前提是已对照人的标签验证过

重复(replicates:)会遇到以不同种子初始化的用户,因此一个角色设定就是多段对话。模拟用户的版本(它的模型、提示词、温度和预算)是每次执行的缓存键的一部分,因此换一个模拟用户会重新度量你的系统。

验证模拟用户

基于模拟用户得出的结果,只有在模拟用户与真实用户相似的程度上才适用于真实用户。因此,对于用户是模拟出来的运行,每条规则都是 INSUFFICIENT_EVIDENCE,附带 simulated_user_not_validated,直到人们认定它的对话真实为止:

oloproof labels export RUN_ID --criterion simulated_user_realistic --sample 30 --local --out realism.csv
oloproof labels import realism.csv
oloproof simulated-users RUN_ID

表格展示每段抽中的对话,以及角色设定和对话如何结束。当一个真实顾客可能写出用户一方的内容,并在那里、以那种方式结束对话时,标注者把 passed 标为是,这也涵盖了目标检查:目标没有达成却以 goal_reached 结束的对话是不真实的。当真实对话所占比例的精确下界达到 release.yaml 中的 minimum_simulated_user_realism 时,该模拟用户即在该测试套件上通过验证。抽中但未标注的对话算作不真实,而一段对话只有在每位标注它的人都认为真实时才算真实。

一次验证涵盖一个模拟用户版本在一个测试套件上的情况,无论它与哪个系统对话,因此依据同样的角色设定度量的候选会依据基线的标签作出决策。新的模型、提示词设置或角色设定集需要新的标签。设置 require_validated_evaluators: false 会在没有这些标签的情况下依据模拟用户作出决策,这是知情的选择。

模拟与真实并列

oloproof simulated-users RUN_ID --real REAL_RUN_ID 把模拟对话的形态与一次真实对话的运行(作为 conversation/v1 重放的日志聊天记录)并列展示:用户发送了多少条消息,消息有多长,回答有多长,对话被截断的频率,以及模拟对话是如何结束的。这部分仅作描述,从不作出决策:比较两个未配对的样本需要一种 Oloproof 尚未采纳的方法。用它来发现真实感标签遗漏的东西,例如模拟用户用一句整齐的话写出的内容,真实用户会分散在三条消息里。

不支持的功能

  • 在托管工作区中依据模拟用户作出决策:在它的评审队列能够自行抽取并封存真实感标签之前,它的决策一直暂缓。
  • 基于模拟对话与真实对话比较结果的门禁。
  • 对话内部的轮次级比率,以及从某个轮次开始重放对话。
  • 为 HTTP 系统提供模拟用户:循环由你的系统自己的代码运行,所以它必须是可调用对象。