ADK AgentEvaluator 实战指南:在 pytest 中用回放对话为 AI Agent 建立质量回归测试
2026/9/13 21:11:26 网站建设 项目流程

ADK AgentEvaluator 实战指南:在 pytest 中用回放对话为 AI Agent 建立质量回归测试

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

AgentEvaluator是 google-adk-python 中google.adk.evaluation模块唯一对外导出的类,它以"回放已录制的对话 + 按指标评分 + 阈值断言"的方式,把 Agent 质量验证无缝嵌入 pytest 套件。本文基于 docs/guides/evaluation/agent_evaluator/index.md 展开,结合 src/google/adk/evaluation/ 下的源码与 contributing/samples/evaluation 示例,完整讲解它的目录约定、test_config.json配置、evaluate/evaluate_eval_set两个入口、底层五阶段执行流程与全部参数,并给出无法用文件承载场景(代码生成用例、旧数据迁移)的进阶用法。

为什么 Agent 质量不能用普通单元测试断言

模型每次运行都会换一种措辞,assert response == "..."这种等价断言会在无害的措辞变化上失败,而且它完全无法回答"Agent 是否用对了工具、传对了参数"这一更关键的问题。AgentEvaluator用"带评分的比较"取代这种断言:

  • 你先把一次对话录制下来,包含用户轮次、期望的最终回复、期望的工具调用;
  • 评估器回放这段对话,让真实 Agent 逐轮作答并产出实际结果;
  • 工具调用做结构化比较,回复文本用ROUGE-1 词重叠分数比较,从而容忍改写;
  • 每个指标都有一个阈值,方法以assert收尾,所以整个评估就是测试函数里的一行await

阈值是把"一个数字"变成"一个回归测试"的关键:单独跑一次只得到一个数字,数字本身不说明 Agent 好不好;而把它与你信任的那次运行得到的阈值比较,就能得出"最新改动让质量向哪个方向移动"这一可行动的结论。正因如此,选择"分数含义符合你预期的指标"是绝大部分工作量——下面 Get started 会详细解释两个默认指标。

从零开始:三个磁盘要素与两个默认指标

评估器在磁盘上需要三样东西:一个可被导入的 agent 包、一个名为*.test.json的已录制对话文件、以及紧挨着该文件的test_config.json

my_agents/ home_automation/ __init__.py # from . import agent agent.py # defines root_agent tests/ eval/ home_automation/ simple.test.json test_config.json test_home_automation.py

test_config.json为每个指标命名并给出阈值。绝大多数套件从下面这两个指标起步,因为它们恰好覆盖一轮回复的两半——Agent"做了什么"和"说了什么":

{ "criteria": { "tool_trajectory_avg_score": 1.0, "response_match_score": 0.6 } }

指标一:tool_trajectory_avg_score(做了什么)

对每个用户轮次,把 Agent 实际发出的工具调用与录制中期望的工具调用做比较,工具名和参数都要匹配,整轮完全匹配记 1.0,否则记 0.0。名字里的"avg"是对轮次求平均,而不是在一轮内部给部分分:得 0.5 意味着"有一半轮次完全正确",而不是"每一轮都答对了一半"。这就是为什么1.0是常见阈值——任何更低的数字,都是在声明"你愿意容忍多少轮出错"。

该指标的误导场景:Agent 用多条路径到达同一答案时,一次无害的多余查询就会让这轮得 0.0。正确的解法是调整match_type(见 eval config 指南),而不是调低阈值。源码层面,ToolTrajectoryCriterion(eval_metrics.py)支持三种MatchType

  • EXACT(默认):实际与期望工具调用序列完全一致才算匹配;
  • IN_ORDER:要求实际调用按与期望相同的顺序发生,允许中间夹杂额外调用。例如期望[T1, T2, T3]、实际[T1, T1.1, T2, T2.1, T2.2, T3, T3.1]即满足;但期望里出现过的T4缺失则不满足;
  • ANY_ORDER:只要求期望的工具都出现过、顺序不限,同样允许额外调用,适合"发了 5 次搜索、但不在乎先后"这类场景。

此外ignore_args=True可让指标只比较工具名、忽略参数。分支实现在 trajectory_evaluator.py,而真实示例见 basic_criteria/eval_config.json,它把match_type显式写成了"EXACT"

指标二:response_match_score(说了什么)

这是 ROUGE-1 比较:把两段文本都还原为词干,统计共有的单词数,返回精确率与召回率的调和结果(f-measure,见 final_response_match_v1.py 的_calculate_rouge_1_scores)。因此词序无关紧要,而"往回答里注水"和"漏掉要点"都会拉低分数;改写过的答案仍能得高分——这正是优先用它而非等价断言的全部理由。

它的误导场景在于语义:"the light is on" 与 "the light is off" 几乎共享所有单词。请把它当作"Agent 谈到了正确主题"的检查,当你要断言的确实是"正确性"时,改用final_response_match_v2——它会请一个 judge 模型来判断两个答案是否意思一致(LlmAsAJudgeCriterion,默认 judge 模型为gemini-2.5-flash,默认采样num_samples=5,见 eval_metrics.py)。

0.6 只是起点,不是推荐值

先对你已信任的 Agent跑一次 eval,看它产出的分数,再把每个阈值设到最低分数略低的位置,让测试只在"回归"时失败、而不是在正常波动时失败。

一行代码的测试

import pytest from google.adk.evaluation import AgentEvaluator @pytest.mark.asyncio async def test_home_automation_agent(): await AgentEvaluator.evaluate( agent_module="my_agents.home_automation", eval_dataset_file_path_or_dir="tests/eval/home_automation/simple.test.json", num_runs=2, )

这个调用里有两个决定成败的细节:

  • agent_module是可导入的点分模块路径,不是文件系统路径。指定包名后,加载器会导入它、寻找名为agent的成员,再从那个内层模块读取root_agent。这就是为什么约定俗成的包要在__init__.py里写from . import agent(见 home_automation_agent/init.py)。直接指定内层模块(如"my_agents.home_automation.agent")也可以;如果模块暴露的是异步的get_agent_async()而不是root_agent,加载器会 await 它并取其第一个返回值。这些逻辑对应 agent_evaluator.py 的_get_agent_for_eval:模块必须带有agent成员或以.agent结尾,否则抛ValueError
  • eval_dataset_file_path_or_dir是相对进程工作目录解析的,你写的是相对"启动 pytest 的目录"的路径,而不是相对测试文件的路径。如果传目录而非单文件,目录下所有以.test.json结尾的文件都会被运行,各自使用旁边的test_config.json

它是怎么工作的:五阶段执行流程

一次evaluate调用按顺序走完五个阶段(对应 agent_evaluator.py 的evaluate_get_eval_results_by_eval_id):

  1. 收集 eval 数据。目录参数会递归遍历*.test.json;文件参数原样使用。每个文件被解析成一个EvalSet(eval_set.py 定义了eval_set_idnamedescriptioneval_cases字段)。若文件是EvalSet之前的旧 schema,仍可加载,但会打出一条指向AgentEvaluator.migrate_eval_data_to_new_schema的警告。
  2. 寻找配置。对每个测试文件,find_config_for_test_file同目录下找test_config.json(agent_evaluator.py)。若缺失,评估不会报错,而是回退到内置默认值:tool_trajectory_avg_score阈值1.0response_match_score阈值0.8(见 eval_config.py 的_DEFAULT_EVAL_CONFIG)。这是很严苛的标准,首次运行常常会因为与 Agent 本身无关的原因失败。
  3. 解析 Agent。按上文方式导入模块并定位root_agent。若模块还暴露名为appApp实例,它也会被拾取,使其插件和上下文缓存配置参与运行。
  4. 真实运行 Agent。第一阶段是真实推理:LocalEvalService对 eval set 中的每个用户轮次实际执行你的 Agent,共执行num_runs次。即使你配置的所有指标都是确定性的,也需要模型凭据,因为"产出待评分的输出"本身就是一次模型调用。多次运行是串行的,所以墙上时间随num_runs线性增长——代码里通过[InferenceRequest(...)] * num_runs构造重复请求并逐个消费(agent_evaluator.py)。
  5. 评分并断言。第二阶段对录制输出评分:每个指标把每次运行的逐调用分数平均成一个数,均值 ≥ 阈值即通过(agent_evaluator.py 使用statistics.mean)。每个失败指标贡献一行输出:
response_match_score for my_agents.home_automation Failed. Expected 0.6, but got 0.41.

方法以assert not failures收尾,这些行于是成为 pytest 的失败消息。如果某次运行完全没有产出任何指标结果(比如推理本身抛异常),会被单独报告,保证"崩溃"不会伪装成"干净通过"(对应_get_failures_from_final_eval_status,agent_evaluator.py)。

print_detailed_results保持默认的True时,失败指标还会打印一张"期望 vs 实际"的逐调用对照表格,包含 prompt、期望/实际回复、期望/实际工具调用等列(_print_details,agent_evaluator.py)。该表格依赖pandastabulate,缺少时会抛出带安装提示的ModuleNotFoundError

配置选项全表

以下是evaluate的参数。evaluate_eval_seteval_dataset_file_path_or_dirinitial_session_file外参数相同——这两个被eval_seteval_config取代。

选项类型默认值说明
agent_modulestr必填Agent 包的点分模块路径。
eval_dataset_file_path_or_dirstr必填单个 eval 文件,或递归搜索*.test.json的目录。
num_runsint2整个 eval set 在被平均前运行多少次。
agent_namestr \| NoneNone评估命名子 Agent 而非根 Agent。
initial_session_filestr \| NoneNone初始会话状态,仅用于EvalSet之前的旧数据。
print_detailed_resultsboolTrue为失败指标打印"期望 vs 实际"表格。
artifact_serviceBaseArtifactService \| NoneNone运行读取的 Artifact 服务,默认用内存版。
output_filestr \| NoneNone以 CSV 把逐调用结果写入该路径。
app_namestr \| NoneNone持久化结果时使用的 App 名。
eval_set_results_managerEvalSetResultsManager \| NoneNone把结果持久化为*.evalset_result.json

各参数的源码级细节:

  • num_runs的存在是因为单次运行真实模型有噪声。平均两次是默认值(源码常量NUM_RUNS = 2);ADK 集成测试对已知易波动的用例用四次。测试"波动而非错误"时,提高它是第一件该试的事,代价是模型调用数线性增长。
  • agent_name从已加载根 Agent 的树中按名字选中一个子 Agent,从而可以单独给某个专家打分。名字不匹配会抛ValueError_get_agent_for_eval中的root_agent.find_agent(agent_name))。
  • initial_session_file只适用于旧 schema 数据。与新版EvalSet文件一起传会导致加载失败,并提示"初始会话应属于 eval set 文件内部"(agent_evaluator.py 的断言逻辑)。
  • artifact_service在用例依赖"运行开始前就必须存在"的 artifact 时(如 Agent 要总结的 PDF)有用:把 artifact 预载入服务并传入,再通过SessionInput.session_id把 eval case 固定到某个会话 id,查找即可命中——EvalCase.session_input的注释说明 artifact 按(app_name, user_id, session_id)键控(eval_case.py)。
  • eval_set_results_manager把运行产物持久化,从而把 CI 任务变成可对比的历史。它要求app_name:只传 manager 不传app_name会在任何运行之前抛ValueError(agent_evaluator.py)。LocalEvalSetResultsManager(agents_dir=...)会在<agents_dir>/<app_name>/.adk/eval_history/下为每个被评估的 eval set 写一个*.evalset_result.json,内含num_runs次运行各自的EvalCaseResult
  • output_file是扁平替代方案:每个指标每调用一行 CSV,携带阈值、分数、状态、prompt 以及期望/实际的回复与工具调用。行是追加的,所以多个测试文件可以累积进同一张表(_write_results_to_csvmode="a"且只在文件不存在时写表头,agent_evaluator.py)。

进阶应用:脱离文件驱动的两种场景

不用文件评估:evaluate_eval_set

当用例是代码生成而非检入的静态文件(例如覆盖输入矩阵、或从数据库读出的用例),直接用evaluate_eval_set传入EvalSetEvalConfig对象:

from google.adk.evaluation import AgentEvaluator from google.adk.evaluation.eval_case import EvalCase from google.adk.evaluation.eval_case import Invocation from google.adk.evaluation.eval_config import EvalConfig from google.adk.evaluation.eval_set import EvalSet from google.genai import types eval_set = EvalSet( eval_set_id="generated", eval_cases=[ EvalCase( eval_id="turn_off_bedroom_light", conversation=[ Invocation( user_content=types.Content( role="user", parts=[types.Part(text="Turn off the bedroom light.")], ), final_response=types.Content( role="model", parts=[types.Part(text="The bedroom light is off.")], ), ) ], ) ], ) await AgentEvaluator.evaluate_eval_set( agent_module="my_agents.home_automation", eval_set=eval_set, eval_config=EvalConfig(criteria={"response_match_score": 0.6}), num_runs=1, )

注意EvalCase校验"conversationconversation_scenario必须且只能提供一个"(eval_case.py 的ensure_conversation_xor_conversation_scenario)。

evaluate_eval_set也接受criteria字典,但已废弃。它不只是旧:当criteria非空时,它会替换你传入的eval_config,所以同时提供两者的调用会静默丢失配置(agent_evaluator.py)。请只用eval_config

迁移旧 eval 数据

AgentEvaluator.migrate_eval_data_to_new_schema(old_file, new_file)EvalSet之前的旧 JSON 文件重写为当前 schema,指标取自旧文件旁的test_config.json。它是一次性工具,不要在测试里调用(agent_evaluator.py)。

局限与注意点

  • 需要 evaluation extra,且失败信息几乎不提示原因。安装:pip install "google-adk[eval]"。没有它时,from google.adk.evaluation import AgentEvaluator会报ImportError: cannot import name 'AgentEvaluator' from 'google.adk.evaluation',仅此而已,不会点名真正缺失的依赖(init.py 捕获了ImportError)。想定位缺失依赖,直接导入google.adk.evaluation.agent_evaluator:基础安装下会报No module named 'vertexai',它来自google-cloud-aiplatform[evaluation]。底层统一抛出的MISSING_EVAL_DEPENDENCIES_MESSAGE也给出了同样的安装提示(constants.py)。
  • 评估不是离线的。每次运行都会执行 Agent,因此即使指标全是确定性的,也需要凭据与模型配额。
  • 失败是AssertionError。没有类型化异常,也没有返回给调用方的结果对象;要程序化查看分数,请传eval_set_results_manageroutput_file
  • 缺失配置是静默的。eval 文件旁没有test_config.json意味着采用严格的默认值,只打一条信息级日志说明。

相关示例

  • Evaluation samples:围绕同一个共享 Agent 的六种评估变体,附有对比各技术的 README。
  • Test file vs. eval set:.test.json.evalset.json的含义,以及为何两者以相同方式加载。
  • 共享的家居自动化 Agent:所有评估示例打分的确定性 Agent——所有工具由内存字典支撑,reset_data()adk eval拾取以在每个 eval case 之间重置状态。

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询