Google ADK 的 Replay 與 Live 模式:回歸測試門禁應設在哪一種模式?
Google ADK 的 Replay 与 Live 两种模式:门禁该挂在哪一个上
文中建議將 Google ADK 的 Replay 留作 CI 門禁,Live Mode 用於評估變化;後者官方仍標示為開發中。Replay 比對錄製交互中的 LLM 請求、回應文本及工具呼叫,不判斷業務結果是否正確。重錄 generated-recordings.yaml 而 spec.yaml 不變,測試仍可能轉綠,因此文中建議對基線產物變更設雙人覆核。
简介: 本文剖析Google ADK智能体回归测试机制,指出`adk conformance test`依赖录制基线(如`generated-recordings.yaml`),但“谁来录、如何复核、比对什么”三责缺位——重录即绿,掩盖真实风险。官方强调基线为运行产物而非人工编写,回放仅校验请求/响应/工具调用三要素一致性,不保业务正确性。绿灯本质是“有人录过”的凭证,非质量担保。
摘要:
generated-recordings.yaml整份重录一遍、用例正文一个字不动,adk conformance test照样报绿,这条回归在保护谁就成了问题。一手来源为 Google ADK 官方文档《Why evaluate agents》的 Conformance 一节,属官方文档原文口径。把tests/core/refund_window_001/generated-recordings.yaml整份重录一遍,用例正文一个字不改,这条挂在流水线末端的智能体回归照样报绿——它比对的对象就是这份录下来的产物,而"录"这一步从来没人签过字。
Google 在 ADK 官方文档《Why evaluate agents》的 Conformance 一节里把智能体回归收成一条 adk conformance test,同页还有一句更硬的限定:不该去手写或手工保存基线文件。机制给得很清楚,责任没有——基线谁录、Replay 到底比哪三样、录完谁来复核,这三段才是这条命令要求测试团队自己补齐的东西。
自动化测试里那套"断言、基线、门禁"的三步,到了智能体身上第一步变成了录制——判据、责任人和失效方式因此完全不同,混在一张检查表里就会得到一种看起来什么都在保护、实际上谁都不认领的绿。
官方的起点:过/不过这个信号不再免费
文档开头把对照关系摆得很直:那套靠清楚的过/不过信号撑起来的传统测试,到了概率性的 LLM 智能体身上不再够用,得改成对最终输出与执行轨迹同时做定性评估——轨迹按官方的解释就是智能体返回给用户之前走过的那串步骤,可以拿它去和期望的步骤清单比,示例清单给了四步。
紧接着是这篇里对测试团队最有用的那条命令:adk conformance test。官方对它的两句定性,一句是"验证你的 AI 智能体随时间保持一致行为",一句是"通过用当前输出去校验基线数据,确保代码或模型的改动不引入回归"。要跑这条命令,必须先有一份 golden baseline(黄金基线:把一次交互录下来当作对照物)。
基线是运行产物,不是用例正文
目录是硬约定:tests/类目名/用例名/ 下面放三个文件——spec.yaml、generated-recordings.yaml、generated-session.yaml,类目名和用例名由目录结构推断出来,不写在文件里。spec.yaml 的基本 schema 就三类字段:description(这条用例测什么)、agent(被测智能体)、user_messages 里每句话的 text。如果你的交互走 SSE,框架会去同目录另找 generated-recordings-sse.yaml 和 generated-session-sse.yaml。
真正该抄进流程文档的是这句:官方明写你不该去手写或手工保存基线文件。生成方式固定两步,先挂上录制插件起 web 预览,再对具体用例目录录制:
adk web -v --extra_plugins=google.adk.cli.plugins.recordings_plugin.RecordingsPlugin /path/to/agents
adk conformance record tests/core/refund_window_001 none
adk conformance record tests/core/refund_window_001 sse
尾部那个模式参数是必填的:none 生成 recordings 加 session 两个文件,sse 生成带 -sse 后缀的那两个变体,而 bidi 官方直接写明不支持录制。
图注:四步里只有第一步是人写的。
这对测试团队意味着一件事:一次"刷新基线"的提交里,唯一承载人的判断的文件只有 spec.yaml——那几句用户话术、那个被点名的 agent。另外两个文件是运行产物,是那次执行的结果记录。把三者混在同一个 diff 里看,典型结果就是四百行产物把一句话的意图淹掉,复核的人只剩下"看起来差不多"这一个判断依据。

图注:两种性质的改动挤进同一条 diff,抓手只剩"看起来差不多"。
智能体回归的基线谁来录、怎么复核,回放又在比哪三样
Replay Mode(回放模式)是默认口径。官方说法是:把现场的 LLM 请求、响应、工具调用与已录交互直接比对,用来发现意外偏离。它比的对象是三样东西,不是"回答对不对"。
| 比对对象 | 现场拿什么去比 | 会响的偏离 | 不会响的偏离 |
|---|---|---|---|
| LLM 请求 | 与录制时那条请求逐字段比 | 提示词模板换版、模型档位或参数被人动过 | 请求一致但下游数据本身变了 |
| LLM 响应 | 与录制下来的响应文本比 | 输出口径变了 | 换了措辞、意思没变——这一格最容易误伤 |
| 工具调用 | 调了谁、带什么参数、按什么顺序 | 参数改名、多走一步、调用顺序调换 | 工具与参数都对,但返回内容变了 |
表里最后一列是我们做回归设计时的推导,官方没有逐层写明判据粒度;"直接比对"这四个字能撑住的,是前面两列。
我把这套语义复刻成一个能跑的脚本(01_基线回放比对器.py,只用标准库):一个模拟智能体产出三层对象,一份录好的基线放在官方那套目录里,然后往现场运行里注入六种改动,看回放各自响在哪一层。
def replay(baseline, run): # 与官方 Replay 口径同形:逐字段比已录交互
fails, b_req, r_req = [], baseline["request"], run["request"]
for k in ("model", "instance", "system_sha", "temperature"):
if b_req.get(k) != r_req.get(k):
fails.append("LLM 请求层:%s 录制值 %s → 现场 %s" % (k, b_req.get(k), r_req.get(k)))
b_calls = [s for s in baseline["steps"] if s["kind"] == "tool_call"]
r_calls = [s for s in run["steps"] if s["kind"] == "tool_call"]
for i in range(max(len(b_calls), len(r_calls))):
b = b_calls[i] if i < len(b_calls) else None
r = r_calls[i] if i < len(r_calls) else None
if b is None:
fails.append("工具调用层:现场多出第 %d 步 %s" % (i + 1, r["name"]))
elif r is None:
fails.append("工具调用层:基线第 %d 步 %s 现场没走到" % (i + 1, b["name"]))
elif b["name"] != r["name"]:
fails.append("工具调用层:第 %d 步顺序变了 %s → %s" % (i + 1, b["name"], r["name"]))
elif b["args"] != r["args"]:
fails.append("工具调用层:%s 参数变了 %s → %s" % (b["name"], sorted(b["args"]), sorted(r["args"])))
b_resp = [s["text"] for s in baseline["steps"] if s["kind"] == "llm_response"]
r_resp = [s["text"] for s in run["steps"] if s["kind"] == "llm_response"]
if b_resp != r_resp:
fails.append("LLM 响应层:文本与录制不一致(逐字符)")
return fails
终端原文(完整输出五段 33 行,这里贴 [3] 这一段,[1] 目录发现与 [2] 缺件矩阵见脚本运行结果):
[3] Replay(默认):现场运行逐字段比已录交互
绿 未改动 预期 绿|三层与基线逐字段一致
红 提示词模板升版 预期 红·请求层|LLM 请求层:system_sha 录制值 sys-v3 → 现场 sys-v4
红 调用顺序换一下 预期 红·工具层|工具调用层:第 1 步顺序变了 order_lookup → refund_policy
↳ 工具调用层:第 2 步顺序变了 refund_policy → order_lookup
红 入参改名 预期 红·工具层|工具调用层:order_lookup 参数变了 ['order_id'] → ['orderId']
红 回答同义改写 预期 红·响应层|LLM 响应层:文本与录制不一致(逐字符)|录制:您的订单已签收 9 天,…|现场:订单签收已超过 9 天,…
红 中间步多走一次 预期 红·工具层|工具调用层:第 1 步顺序变了 order_lookup → risk_flag
↳ 工具调用层:第 2 步顺序变了 refund_policy → order_lookup
↳ 工具调用层:现场多出第 3 步 refund_policy
为什么这么写、踩了什么坑,四条。第一,"提示词模板升版"这一格是回放最有价值的地方:它不改变任何业务语义,但"和录下来那次一致"这件事先响了——这类改动在传统单测里根本无处可响,因为没人断言过请求。第二,"回答同义改写"也判红,这是回放的严,也是它的脆:口径变了它就得响,可你的业务可能只关心意思。这决定了回放该留在门禁层,别指望它替你判断质量。第三,一次中间步骤插入会被位置比对放大成三行红——先数"红了几格",再判断"改了几处",两个数不相等就说明比对是按位置走的,你要找的是那个插入点。第四,缺件比任何红都常见:录制时给的是 none,跑的时候按 sse 找变体,框架不会替你改名;反过来只录不跑也一样,用例名与类目是从目录推断的,目录一动,基线就成了孤儿。脚本的 [2] 段专门把这四种组合跑了一遍,bidi 那一行永远是"这条基线根本不存在"。
Live Mode 还不能当验收,但别只当它没有
官方对 Live Mode 的定性是:对活跃环境做 evaluation-based 验证,并专门括注了一句——这个模式仍在开发中(This mode is a work in progress)。所以我们的做法是把门禁留在 Replay,把 Live 当成"变化清单"的来源:回放说变了,Live 那侧再回答"变得好不好"。
同一次现场运行,换成 evaluation-based 的口径去判,脚本给的结论会翻面:
[4] 同一次现场运行,改走 Live 的 evaluation-based 口径
绿 回答同义改写 语义重合度 0.49|轨迹集合一致,判不出顺序与参数差异
绿 调用顺序换一下 语义重合度 1.00|轨迹集合一致,判不出顺序与参数差异
绿 入参改名 语义重合度 1.00|轨迹集合一致,判不出顺序与参数差异
这一段的判据(集合比对加一个重合度比值)不是官方 Live 的实现;它要说明的是比的对象一换,红绿就不等价。回放盯"是否偏离录制",评测盯"质量如何",两条结论谁也不能替谁签字。开销计数则另有它自己的一栏,本篇不碰。
谁录、谁签、谁复核
把上面这些落到流程里,就是三段。
图注:三段是三个人,不是一张表。
录制这一步要有登记。录的时候顺手记四件事:模型与档位、提示词版本、工具实现的版本号、用了哪套数据夹具。这份登记和基线文件放同一个目录,评审时才有对照物——否则"刷新基线"永远只能靠猜。
提交这一步要成对。三个文件一起进,spec.yaml 的改动单独写一句人话说明。我们的做法是把说明写成模板:场景没变/场景变了/模型档位升了/工具接口变了,四选一。产物本身不写说明,因为它不该有说明——它是被录出来的。
复核这一步要看的是 diff 的形状,不是行数。录制产生的 session 差异读起来几乎不可能,所以复核的抓手是报告:--generate_report 会产出一份 Markdown 汇总,--report_dir 指定落盘目录,让报告随构建产物一起存档,重录的提交必须附上新旧报告的 diff。这一步是本文的流程设计,官方给的是报告开关本身。
三段里最容易断的是第二段。录制入口挂在 adk web 的预览进程上,谁都能在自己机器上补录一次,而框架不会问这份产物是在哪个模型档位、哪版提示词、哪套夹具下录的——登记项在目录之外,成对提交只保证文件齐全,不保证环境一致。最常见的走形于是:跑不过就顺手 adk conformance record 重录一遍,产物变了、spec.yaml 没变,用例当场转绿。这类提交在 diff 上没有异常形状,只有把"产物文件被改动"当成一条独立的评审触发条件才拦得住;这也是第三段要双人复核、其中一人须是 spec.yaml 里那个 agent 负责人的理由:只有写意图的人能判断重录出来的交互还符不符合原场景。
重录之后怎么确认这是新基线而不是旧基线的复制?看回放那三样是否逐项对得上:请求的模型档位与提示词指纹、工具调用的名称与入参顺序、响应文本。任何一项在两份产物之间对不上,就说明环境换了而登记没写,正确动作是回到录制入口固定夹具与档位再录一次,而不是把比对改成忽略这些字段。
挂进 CI 的四个动作
不带路径时这条命令会在工作区自动找 tests/ 全跑,也可以只跑一个类目或一条用例:adk conformance test tests/core、adk conformance test tests/core/refund_window_001。四个动作依次是:其一,跑之前先校验成对,缺 -sse 变体或缺 session 文件就在配置层红掉,别让它变成一条"没有用例可跑"的绿。其二,PR 级只跑受影响类目,全量留给每日定时,且带 --generate_report 把报告留档。其三,提交里出现 generated-recordings 或 generated-session 被改动的,走双人评审,其中一人必须是 spec.yaml 里那个 agent 的负责人。其四,把每轮运行自带的调用与耗时计数单独存档、按跨运行排成趋势,别混进用例的通过失败里;这条趋势线怎么判红绿,得自己写。想把这些用例接进现成资产,官方也给了 pytest 形态的入口,定性就是可以并进 CI/CD 或更大的测试套件。
边界:这套回放管不到什么
第一,也是最重要的一条:Replay 比的是"和已录交互一致",它证明不了业务对错。第二,这一页没有给出任何效果数字、通过率或耗时基准,本文也不把它的机制换算成任何通过率;默认阈值与判卷标准怎么选属于另一层,跟回放机制不是一回事。第三,Live Mode 官方明写在开发中,现在把它写成发布门禁,等于把门禁挂在一个还在改的钩子上。第四,bidi 不支持录制,流式场景得按 SSE 变体各录一份,漏一份就是[2] 里那种缺件。第五,这套目录约定与命令口径标注的是 ADK 的 Python 支持;脚本里的模拟智能体、场景名、文件命名与三段责任流都是本文示例,别当成官方规范搬进仓库。
下一步
把仓库里那份基线文件的提交记录翻出来,只看三件事:最后一次重录是谁、那条提交的说明里有没有写原因、spec.yaml 里那几句用户话术最后一次被人改动是什么时候。三个问题有一个答不上来,你们这条回归缺的就不是用例,是责任人。
绿灯只覆盖你断言过的地方;而智能体回归的绿灯,覆盖的是"有人录下来的那一次"。
本文部分内容参考了霍格沃兹测试开发学社整理的相关技术资料,主要涉及软件测试、自动化测试、测试开发及 AI 测试等内容,侧重测试实践、工具应用与工程经验整理。
來源:阿里雲開發者社區 AI · developer.aliyun.com
