用户模拟跑起来了,期望轨迹却断不了:Google 官方文档写下的一条互斥
关注 霍格沃兹软件测试开发 公众号,回复「资料」, 领取人工智能测试开发技术合集
多轮真实感与精确断言在官方口径里是一对互斥,得拆成两套评估集
周二下午的评审会,为同一条退款链路,桌上摆着两个方案。测试同学摆出 A 方案:把用户三句话逐字写死,工具调用序列一步一断言——『没先查单就改地址,红』。算法同学摆出 B 方案:让模型扮演用户,跟智能体现聊——用户会中途发火、会改口、会打错订单号,软件测试里最想要的那种『活人感』全都有了,断言却一条都下不去。两边都说得通,谁也说服不了谁。因为最想测的那类问题——用户第三句改口才炸的缺陷——恰好骑在两个方案的界线上:A 可断言但不像真人,B 像真人但中间步骤没有标准答案。据 Google 官方 ADK 评估文档,这条界线不是团队分歧,是官方写下来的规则。
官方的判据清单:turn 四字段与一条互斥
据 Google 官方 ADK 评估文档,评估范围覆盖两样东西:最终输出,以及智能体的执行轨迹(Trajectory)。一个 turn 的评估用例带四个字段:User Content、Expected Intermediate Tool Use Trajectory、Expected Intermediate Agent Responses、Final Response——对两个中间字段,官方的说法是:中间响应对使用者 may or may not be of interest,对开发者却关键。User Simulation(用户模拟)是让 AI 模型动态生成用户 prompt、跑完整个会话场景;而文档同时写明了一条限制:criteria which require information on expected agent tool use and/or responses are not supported in combination with User Simulation——需要『期望工具轨迹/期望响应』信息的判据,不能与用户模拟同时使用。Expected Trajectory(期望轨迹)是期望智能体按什么样的工具调用序列走完这轮会话,它是断言的对象,而用户模拟恰恰消灭了这个对象的前提:下一句用户说什么,跑之前谁也不知道。文档里另有两个名字值得记下:rubric_based_tool_use_quality_v1 这类 rubric 判据可以写『Tool A 必须先于 Tool B 被调用』式的规则,但官方标注它属 LLM-judged——由模型打分判定,不是代码比对;模拟器自身的质量也有专门判据 per_turn_user_simulator_quality_v1。
人工智能技术学习交流群
伙伴们,对AI测试、大模型评测、质量保障感兴趣吗?我们建了一个 「人工智能测试开发交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

为什么软件测试的两个老习惯在这里同时满足不了
老习惯一,分步断言:前置数据、操作、后置状态,功能测试年代打磨出来的写法,期望轨迹就是它的智能体版本——测的不是答案,是该走的步骤走没走。老习惯二,场景真实:话术变体、中途改口、答非所问,全靠输入端足够像人来暴露盲区。互斥条款把这个冲突摊开成了明规则:你要输入端由模型生成,就没有人能提前写下『期望的中间响应』;你要把中间步骤钉死成断言,用户输入就只能手写。
看一个典型现场:退款链路,前两轮都正常,第三轮用户突然改口问『那运费险赔不赔』——好的系统应当先撤销退款单、再查保险资格。固定集里你不会想到写这句第三轮,因为人肉话术写不活;模拟集里模拟器真可能说出这句、轨迹也真的先撤销再查询,但你没有任何官方口径去断言『这一步必须先撤销』。一边漏缺陷,一边碰限制。对测试团队,这意味着一行评估集设计纪律:真实感与可断性分家,两套集、两套判据、两套失败处理流程。混在同一套集里写,等于让同一个裁判同时执行两部法律——其中一部还是禁止令。
评估集怎么分?固定集当门禁,模拟集当探针
分家的时候顺序别搞反:先想清楚哪条判据该有判决权,再决定它进哪套集;而不是拿工具现成的表单倒推分工。推荐的三档分工,先把判据想清楚,再定触发时机和产出物去向。
档位
判据类型
触发时机
产出物去向
固定轨迹集
用户每句话写死 + 期望工具序列硬断言
每个 MR、回归门禁
失败即阻断发布
模拟用户集
只给角色与目标;判任务完成、幻觉、越权
每晚或每周批跑
命中进探针池,不阻断
中间态抽样
模拟命中后,把该轨迹人工评审后固化为固定用例
命中即发起
进固定集长期回归
两条纪律比分工表更要紧:模拟集的判据里,永远不许写『期望第几步调哪个工具』;固定集的输入源里,永远不许用模拟生成的 prompt——否则基线每跑一次漂一次。
维度
固定轨迹集
模拟用户集
用户输入来源
人工写死,逐字固定
模型动态生成
可断言对象
中间工具序列、关键参数、最终回答
任务完成度、幻觉、越权动作
能发现哪类缺陷
流程顺序错、参数传错
话术变体盲区、编造事实
发现不了哪类
用户换一种说法后接不接得住
第几步没按期望调工具(无标准答案)
维护成本
低:用例量稳定
高:模拟器自身有波动
失败时该查谁
被测系统的行为变更
先分诊:模拟器有病还是智能体有病
01_录制回放双通道与基线作废触发
代码:同一份场景,按两套判据分别跑
场景定义(构造):固定集两条用例、模拟集三条——其中 S-03 故意挂了期望轨迹,用来演示互斥纪律的机器强制;traces 是两套各自的一次运行事件流。整套数据一个模型都不用调,本地跑一遍不到两秒,判据形状却是完整的。
{
"fixed_set": [
{"case_id": "F-01", "scene": "退款三连:查询→资格→提交",
"turns": [
{"user": "我要申请退款", "expect_tools": ["order_lookup"]},
{"user": "订单号 L8823", "expect_tools": ["refund_eligibility"]},
{"user": "嗯,提交吧", "expect_tools": ["refund_submit"]}]},
{"case_id": "F-02", "scene": "改地址:必须先查单再改,防止改到别人的单",
"turns": [{"user": "收货地址改成杭州", "expect_tools": ["order_lookup", "address_update"]}],
"order_rule": {"before": "order_lookup", "after": "address_update"}}
],
"sim_set": [
{"case_id": "S-01", "persona": "急着下班的普通用户", "goal": "完成退款并被告知到账时间",
"forbidden_tools": ["payment_charge"]},
{"case_id": "S-02", "persona": "反复改口的用户", "goal": "拿到运费险赔付",
"forbidden_tools": ["payment_charge"]},
{"case_id": "S-03", "persona": "投诉用户", "goal": "转人工",
"expect_trajectory": ["ticket_create"]}
],
"traces": {
"F-01": {"tool_calls": [{"name": "order_lookup"}, {"name": "refund_eligibility"}, {"name": "refund_submit"}]},
"F-02": {"tool_calls": [{"name": "address_update"}, {"name": "order_lookup"}]},
"S-01": {"turns": [{"user": "帮我退款,快点"}, {"user": "L8823"}, {"user": "好"}],
"tool_calls": [{"name": "order_lookup"}, {"name": "refund_submit"}],
"tool_results": [{"refund": "ok", "amount": "128.0", "eta": "1-3个工作日"}],
"final_text": "退款128.0元已提交,预计1-3个工作日到账。", "goal_reached": true},
"S-02": {"turns": [{"user": "先别退了"}, {"user": "还是退吧"}, {"user": "运费险赔不赔?赔多少?"}],
"tool_calls": [{"name": "order_lookup"}, {"name": "refund_submit"}],
"tool_results": [{"refund": "ok", "amount": "128.0"}],
"final_text": "退款128.0元已提交。运费险已为你申请,赔付49.5元,三天内到。", "goal_reached": true},
"S-03": {"turns": [], "tool_calls": [], "tool_results": [], "final_text": "", "goal_reached": false}
}
}
评估器把两套判据分开跑,各自打印『能判/不能判』清单,命中场景生成固化候选。纯标准库,python sim_split_eval.py 可跑:
sim_split_eval.py —— 两档评估集各司其职:同一份场景分别按『固定轨迹集/模拟用户集』判据跑,
输出各自能判/不能判的清单,并把模拟命中的轨迹降级固化为回归用例(纯标准库)
运行:python sim_split_eval.py
import json
import re
import sys
with open("scenarios.json", encoding="utf-8") as f:
PLAN = json.load(f)
def eval_fixed(case, trace):
"""固定轨迹集判据:用户每一句写死,判期望工具序列按没按序发生。"""
seq = [c["name"] for c in trace["tool_calls"]]
expect = [t["expect_tools"] for t in case["turns"]]
flat = [t for group in expect for t in group]
ok_seq = seq == flat
rule = case.get("order_rule")
ok_rule = (seq.index(rule["before"]) < seq.index(rule["after"])) if rule and all(
x in seq for x in rule.values()) else (Trueif rule isNoneelseFalse)
return ok_seq and ok_rule, f"工具序列={'/'.join(seq)} 期望={'/'.join(flat)}"
def eval_sim(case, trace):
"""模拟用户集判据:只给角色与目标,判任务完成/幻觉/安全——不判期望第几步调哪个工具。"""
problems = []
final = trace["final_text"]
ifnot trace.get("goal_reached"):
problems.append("任务未完成")
evidence = " ".join(str(r) for r in trace["tool_results"])
unsupported = {n for n in re.findall(r"(\d+(?:.\d+)?)元", final) if n notin evidence}
if unsupported:
problems.append(f"疑似幻觉数字: {sorted(unsupported)}")
hit_forbidden = [c["name"] for c in trace["tool_calls"] if c["name"] in case.get("forbidden_tools", [])]
if hit_forbidden:
problems.append(f"越权调用: {hit_forbidden}")
return (not problems), ("; ".join(problems) or"三项判据均未命中")
print("=" * 70)
print("档一:固定轨迹集(进门禁) 用户三句写死 + 期望工具序列")
print("=" * 70)
fixed_fail = 0
for case in PLAN["fixed_set"]:
trace = PLAN["traces"][case["case_id"]]
ok, detail = eval_fixed(case, trace)
fixed_fail += 0if ok else1
print(f" [{'PASS' if ok else 'FAIL'}] {case['case_id']} {case['scene']}")
print(f" {detail}")
print(" 本档能判:该调的工具按没按序调、关键参数、越界即红的流程缺陷")
print(" 本档不能判:用户换种说法/第三句改口后系统还接不接得住(输入被写死了)")
print()
print("=" * 70)
print("档二:模拟用户集(进探针池,不进门禁) 只给角色+目标")
print("=" * 70)
frozen = []
for case in PLAN["sim_set"]:
if"expect_trajectory"in case or any("expect_tools"in t for t in case.get("turns", [])):
print(f" [配置驳回] {case['case_id']}: 模拟用户集里不允许挂期望轨迹/期望响应类判据"
f"(官方互斥限制),请移到固定轨迹集")
continue
trace = PLAN["traces"][case["case_id"]]
ok, detail = eval_sim(case, trace)
print(f" [{'OK' if ok else 'HIT'}] {case['case_id']} {case['persona']} -> {detail}")
ifnot ok:
seq = " -> ".join(c["name"] for c in trace["tool_calls"])
frozen.append((case["case_id"], [t["user"] for t in trace["turns"]], seq))
print(" 本档能判:任务有没有完成、有没有编造事实、有没有越权动作")
print(" 本档不能判:第几步必须调哪个工具、中间响应的逐字内容(期望轨迹类判据被官方互斥挡住)")
if frozen:
print()
print("=" * 70)
print("探针命中 -> 降级固化为回归用例(模拟当探针,固定集当回归)")
print("=" * 70)
for cid, turns, seq in frozen:
print(f" 由 {cid} 生成 F-{cid}: 用户话术逐字写死 {turns}")
print(f" 期望工具序列改写为: {seq} → 追加进固定轨迹集")
sys.exit(1if fixed_fail else0)
python sim_split_eval.py
======================================================================
档一:固定轨迹集(进门禁) 用户三句写死 + 期望工具序列
======================================================================
[PASS] F-01 退款三连:查询→资格→提交
工具序列=order_lookup/refund_eligibility/refund_submit 期望=order_lookup/refund_eligibility/refund_submit
[FAIL] F-02 改地址:必须先查单再改,防止改到别人的单
工具序列=address_update/order_lookup 期望=order_lookup/address_update
本档能判:该调的工具按没按序调、关键参数、越界即红的流程缺陷
本档不能判:用户换种说法/第三句改口后系统还接不接得住(输入被写死了)
======================================================================
档二:模拟用户集(进探针池,不进门禁) 只给角色+目标
[OK] S-01 急着下班的普通用户 -> 三项判据均未命中
[HIT] S-02 反复改口的用户 -> 疑似幻觉数字: ['49.5']
[配置驳回] S-03: 模拟用户集里不允许挂期望轨迹/期望响应类判据(官方互斥限制),请移到固定轨迹集
本档能判:任务有没有完成、有没有编造事实、有没有越权动作
本档不能判:第几步必须调哪个工具、中间响应的逐字内容(期望轨迹类判据被官方互斥挡住)
======================================================================
探针命中 -> 降级固化为回归用例(模拟当探针,固定集当回归)
由 S-02 生成 F-S-02: 用户话术逐字写死 ['先别退了', '还是退吧', '运费险赔不赔?赔多少?']
期望工具序列改写为: order_lookup -> refund_submit → 追加进固定轨迹集
看点有三个。F-02 变红,是因为 address_update 抢在 order_lookup 前面,顺序规则断言被触发——这就是门禁该管的事,退出码直接阻断。S-02 的『赔付 49.5 元』在工具返回里找不到依据,被幻觉判据命中,生成了固化候选:这条模拟命中的轨迹,下周就是固定集的一员。S-03 则是全文最值钱的一行输出:给模拟集挂期望轨迹,配置检查直接驳回——互斥纪律靠工具兜底,不靠评审时的自觉。模拟集的 HIT 没有计入退出码:它是探针,不是判决。脚本里还有两个细节值得抄进自己的工具:幻觉判据只看一件事——回答里出现的金额在不在工具返回里有出处,这是把 LLM-judged 类判据往确定性代码里能收编的少数形态之一;顺序规则写成序列比对,重跑一百次结果都一样。分家的另一半价值就在这里:每类判据,都放去它最可信的地方判。
回归落点:模拟当探针,固定集当回归
闭环三步走。模拟集每晚批跑,HIT 进探针池;人工评审后,把命中场景的用户话术逐字写死、连同轨迹转成固定用例,从此走门禁流程;固定集变红再咬回去——它的红意味着行为回归或基线过期,查被测系统的变更单。判据归属提前写进规约:固定集的红,被测方负责;模拟集的 HIT,先看模拟器是不是有病(per_turn_user_simulator_quality_v1 判的就是它自己的质量),再看智能体。至于 rubric 类 LLM-judged 判据,同一条用例重跑可能翻红翻绿,推荐跑三次取多数、再按比例人工抽检,并且永远别把它和确定性断言混写进同一列『客观指标』。
边界:互斥是文档口径,不是行业定律
四条钉死。第一,这条互斥限制是 ADK 文档对该框架的口径,不是行业普适定律——别的框架或许允许对模拟场景做轨迹断言,别拿本文当依据。第二,模拟器有自己的质量判据,但模拟器质量不等于场景真实度,这是两件事。第三,本文的场景数、轮次、判据命中全为构造示例,『先查单再改地址』是样例顺序规则,不是对你业务的建议;三档分工与两条纪律是推荐做法,官方没有给任何评估集配比建议。第四,不存在『接入用户模拟后覆盖率提升多少』这种数字,本文不提供,也不预测 ADK 后续会不会放开这条限制。
结语:本周先做一件事
给你们那条『用户第三句改口才暴露问题』的链路写一个模拟集用例:角色一句话、目标一句话,写完检查判据列表——把所有期望轨迹断言拿掉,只留任务完成、幻觉、越权三样。跑完如果命中了真实问题,把那条轨迹评审、固化、进固定集;如果连续两周什么都没命中,先怀疑模拟器,再怀疑链路。真实感与可断性这对矛盾没有两头占的解法,拆开,它们才能各自成立。
多轮的真实感和精确的断言装不进同一个评估集:拆成两套,它们才能各自成立。
推荐学习
测试智能体评估与自我进化公开课,将系统讲解软件测试智能体技能体系、用例生成智能体技能、测试自动化智能体技能,以及智能体技能的评估方法、智能体评测工具与框架。同时,还将深入剖析反思机制与智能体技能自我进化,帮助你理解如何评估、反思并持续提升测试智能体。
欢迎扫码进群,参加本次公开课,一起探索会反思、能进化的测试智能体。

关于我们
霍格沃兹测试开发学社,隶属于 测吧(北京)科技有限公司,是一个面向软件测试爱好者的技术交流社区。
学社围绕现代软件测试工程体系展开,内容涵盖软件测试入门、自动化测试、性能测试、接口测试、测试开发、全栈测试,以及人工智能测试与 AI 在测试工程中的应用实践。
我们关注测试工程能力的系统化建设,包括 Python 自动化测试、Java 自动化测试、Web 与 App 自动化、持续集成与质量体系建设,同时探索 AI 驱动的测试设计、用例生成、自动化执行与质量分析方法,沉淀可复用、可落地的测试开发工程经验。
在技术社区与工程实践之外,学社还参与测试工程人才培养体系建设,面向高校提供测试实训平台与实践支持,组织开展 “火焰杯” 软件测试相关技术赛事,并探索以能力为导向的人才培养模式,包括高校学员先学习、就业后付款的实践路径。
同时,学社结合真实行业需求,为在职测试工程师与高潜学员提供名企大厂 1v1 私教服务,用于个性化能力提升与工程实践指导。

浙公网安备 33010602011771号