用户模拟跑起来了,期望轨迹却断不了:Google 官方文档写下的一条互斥

简介: 官方ADK明确:真实感(用户模拟)与精确断言(期望轨迹)互斥,不可共存于同一评估集。必须拆分为两套——固定轨迹集(人工写死输入+硬断言,用于门禁)和模拟用户集(AI生成话术+任务/安全判据,用于探针),各司其职,方保评估有效可靠。(239字)

多轮真实感与精确断言在官方口径里是一对互斥,得拆成两套评估集

周二下午的评审会,为同一条退款链路,桌上摆着两个方案。测试同学摆出 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。

image.png

image.png

为什么软件测试的两个老习惯在这里同时满足不了

老习惯一,分步断言:前置数据、操作、后置状态,功能测试年代打磨出来的写法,期望轨迹就是它的智能体版本——测的不是答案,是该走的步骤走没走。老习惯二,场景真实:话术变体、中途改口、答非所问,全靠输入端足够像人来暴露盲区。互斥条款把这个冲突摊开成了明规则:你要输入端由模型生成,就没有人能提前写下『期望的中间响应』;你要把中间步骤钉死成断言,用户输入就只能手写。看一个典型现场:退款链路,前两轮都正常,第三轮用户突然改口问『那运费险赔不赔』——好的系统应当先撤销退款单、再查保险资格。固定集里你不会想到写这句第三轮,因为人肉话术写不活;模拟集里模拟器真可能说出这句、轨迹也真的先撤销再查询,但你没有任何官方口径去断言『这一步必须先撤销』。一边漏缺陷,一边碰限制。对测试团队,这意味着一行评估集设计纪律:真实感与可断性分家,两套集、两套判据、两套失败处理流程。混在同一套集里写,等于让同一个裁判同时执行两部法律——其中一部还是禁止令。

评估集怎么分?固定集当门禁,模拟集当探针

分家的时候顺序别搞反:先想清楚哪条判据该有判决权,再决定它进哪套集;而不是拿工具现成的表单倒推分工。推荐的三档分工,先把判据想清楚,再定触发时机和产出物去向。

档位 判据类型 触发时机 产出物去向
固定轨迹集 用户每句话写死 + 期望工具序列硬断言 每个 MR、回归门禁 失败即阻断发布
模拟用户集 只给角色与目标;判任务完成、幻觉、越权 每晚或每周批跑 命中进探针池,不阻断
中间态抽样 模拟命中后,把该轨迹人工评审后固化为固定用例 命中即发起 进固定集长期回归

两条纪律比分工表更要紧:模拟集的判据里,永远不许写『期望第几步调哪个工具』;固定集的输入源里,永远不许用模拟生成的 prompt——否则基线每跑一次漂一次。

维度 固定轨迹集 模拟用户集
用户输入来源 人工写死,逐字固定 模型动态生成
可断言对象 中间工具序列、关键参数、最终回答 任务完成度、幻觉、越权动作
能发现哪类缺陷 流程顺序错、参数传错 话术变体盲区、编造事实
发现不了哪类 用户换一种说法后接不接得住 第几步没按期望调工具(无标准答案)
维护成本 低:用例量稳定 高:模拟器自身有波动
失败时该查谁 被测系统的行为变更 先分诊:模拟器有病还是智能体有病

image.png

代码:同一份场景,按两套判据分别跑

场景定义(构造):固定集两条用例、模拟集三条——其中 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 (True if rule is None else False)
    return ok_seq and ok_rule, f"工具序列={'/'.join(seq)} 期望={'/'.join(flat)}"

def eval_sim(case, trace):
    """模拟用户集判据:只给角色与目标,判任务完成/幻觉/安全——不判期望第几步调哪个工具。"""
    problems = []
    final = trace["final_text"]
    if not 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 not in 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 += 0 if ok else 1
    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}")
    if not 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(1 if fixed_fail else 0)
>>> 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 测试等内容,侧重测试实践、工具应用与工程经验整理。

相关文章
|
2月前
|
运维 监控 安全
国家评测机构首次给智能体分五级:这套五维框架,测试团队明天就能抄成上线验收清单
智能体上线评审常陷“五人五义”困局。全国首个五级智能体评测证书揭示破局关键:将主观判断拆为功能性、稳定性、安全、工程化、场景适配五大可证维度,每维设独立判据与证据链,推动验收从经验走向可审计、可复现、可追责。(239字)
国家评测机构首次给智能体分五级:这套五维框架,测试团队明天就能抄成上线验收清单
|
21小时前
|
存储 人工智能 测试技术
GitHub扩容400万CPU核心:AI编程的效率账,该重新算了
AI编程加速代码生成,但GitHub单月73.8亿次提交暴露出验证、审查与基础设施成本激增。提交量≠交付效率,低质重试、CI资源挤占、审查负担加重正侵蚀自动化红利。真正衡量AI编程价值,应聚焦“可靠交付成本”,而非单纯提交数。
GitHub扩容400万CPU核心:AI编程的效率账,该重新算了
|
20小时前
|
存储 人工智能 测试技术
GitHub紧急扩容400万核:AI Agent把CI跑成试错场,测试该夺回质量门禁了
GitHub 9月单月Commit达74亿次(同比+400%),驱动因素是AI Agent无成本高频试错式提交,致平台可用性一度跌至84%。这暴露“验证外移”危机:质量左移防线被冲垮,测试工程师亟需从找Bug转向设计反馈规则、构建门禁体系与升级度量标准。
GitHub紧急扩容400万核:AI Agent把CI跑成试错场,测试该夺回质量门禁了
|
4天前
|
人工智能 测试技术 Python
Google ADK 的 Replay 与 Live 两种模式:门禁该挂在哪一个上
本文剖析Google ADK智能体回归测试机制,指出`adk conformance test`依赖录制基线(如`generated-recordings.yaml`),但“谁来录、如何复核、比对什么”三责缺位——重录即绿,掩盖真实风险。官方强调基线为运行产物而非人工编写,回放仅校验请求/响应/工具调用三要素一致性,不保业务正确性。绿灯本质是“有人录过”的凭证,非质量担保。
Google ADK 的 Replay 与 Live 两种模式:门禁该挂在哪一个上
|
4天前
|
存储 人工智能 机器人
Google 评估指南点名的能力,用四条 pytest 真测一遍
本文提出智能体错误恢复能力的实战测试方法:通过故障注入(timeout/empty/error_code/corrupt四档)将Mock从“稳定替身”升级为“会出故障的替身”,用结构化断言(而非话术)验证其失败路径行为,让“错误恢复”从口号变为可量化、可变红的工程实践。
|
2月前
|
自然语言处理 安全 测试技术
别再只测『答得对不对』:给大模型应用建一套 Prompt 注入红队回归集,把越权/泄密挡在上线前
本文揭示RAG客服应用因缺乏Prompt注入防护而致系统提示词泄露的事故,指出问题根源在于测试只关注“答得对”,却忽视“会不会答不该答的”。提出将注入测试升级为可回归的红队用例集:结构化存于jsonl,覆盖四类注入;用pytest参数化断言输出、工具调用与拒答行为;接入CI自动拦截。安全不是模型天赋,而是靠可执行、可演进的断言守出来的。
别再只测『答得对不对』:给大模型应用建一套 Prompt 注入红队回归集,把越权/泄密挡在上线前
|
20天前
|
缓存 人工智能 测试技术
同一条Agent评测跑两次结果不同,问题可能根本不在模型
Cloudflare提出Agent评测需环境一致性:通过文件系统快照固定代码、依赖、工具、数据、网络及运行策略六类变量,避免环境漂移干扰结果,确保模型对比真实可信。
同一条Agent评测跑两次结果不同,问题可能根本不在模型
|
22天前
|
人工智能 安全 测试技术
微软 Agent 评测指南里,藏着一条让测试少背一次锅的结论
本文提出可辩护的Agent Experience(AX)评测方法,聚焦AI编程中“代码可运行但选错SDK/认证模式”的典型问题。基于微软AX实践手册,构建含Criteria(技术事实判定)与Gate(真实运行验证)的测试闭环,通过基线/候选对照、Trajectory根因分析及九类失败分类,推动测试前移至Agent技术选型环节,实现来源可控、证据可溯、修复可验。
微软 Agent 评测指南里,藏着一条让测试少背一次锅的结论
|
24天前
|
人工智能 运维 测试技术
UI 智能遍历测试:状态识别、路径规划与覆盖率评估
UI智能遍历测试是一种基于状态识别、路径探索与动态反馈的自动化测试方法,解决传统Monkey测试覆盖不均、重复无效操作等问题。通过构建UI状态图、智能去重、覆盖驱动路径规划及多维度异常检测,显著提升业务路径覆盖率与问题发现效率。
UI 智能遍历测试:状态识别、路径规划与覆盖率评估