Agno Eval Suite 实战指南:基于 Case 声明式评测、CLI 与 CI 集成的完整测试记录解读
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
本篇技术指南以cookbook/09_evals/suite/TEST_LOG.md的测试记录为核心,围绕 Agno 内置的 Eval Suite 能力展开:如何用Case声明式定义评测用例(Agent 与 Team 两种被测对象),通过内置cli()完成用例列表、标签筛选、JSON 报告输出与 CI 退出码控制,并深度解读JudgeMode.NUMERIC数值评分、expected_tool_calls可靠性检查在 Team 场景下的真实行为。读完本文,你将掌握如何把多个评测用例打包成一套可被 CI 消费的 eval suite,并理解其底层实现(libs/agno/agno/eval/suite.py)的关键调用链与数据契约。
一、Eval Suite 是什么:从测试日志看功能全景
cookbook/09_evals/suite/TEST_LOG.md记录了 suite 目录下两个评测示例脚本的完整测试结论,是理解 Agno Eval Suite 最直接的入口。该日志覆盖了两个层面:
suite_basic.py:面向单个 Agent 的基础评测套件,测试了--list列表、--json-output全量运行、未知--tag选择器三种 CLI 场景,以及python -m agno.eval模块入口被移除这一行为变更。suite_team_scoring.py:面向 Team(团队)的数值评分评测,leader 将算术任务委派给 calculator 成员、将写作任务委派给 writer 成员,验证了run_cases/arun_cases程序化入口与 CLI 双路径。
日志呈现的核心事实包括:
| 观测点 | 结果 |
|---|---|
suite_basic.py全量运行 | 2/2 用例通过,退出码 0,JSON 载荷含预期 summary/cases 结构 |
未知--tag选择器 | 退出码 2,并列出可用用例名 |
| 可靠性检查 | expected_tool_calls=()在构造期即被拒绝(falsy 守卫) |
python -m agno.eval模块入口 | 已移除,CLI 统一走脚本内cli(CASES) |
| Team 用例 | 载荷携带team_id: "assistant-team"、agent_id: null |
| Team 成员真实工具 | 经team_response=可见tools_called: ["delegate_task_to_member", "multiply"] |
| 数值评分 | 两例均报告judge_score: 10 |
这些结论均有对应源码佐证,下文将逐一展开。
二、快速上手:声明一个最小 Eval Suite
测试日志提到的suite_basic.py(源码见 suite_basic.py)是理解整个机制的最佳起点。一个 suite 由三部分组成:被测 Agent/Team、一组Case、以及入口cli(CASES)。
import sys from agno.agent import Agent from agno.eval import Case, cli from agno.models.openai import OpenAIResponses from agno.tools.calculator import CalculatorTools # 1. 创建被测 Agent agent = Agent( id="math-tutor", model=OpenAIResponses(id="gpt-5.5"), tools=[CalculatorTools()], instructions="Use the calculator tools for any arithmetic.", ) # 2. 声明评测用例 CASES = ( Case( name="factorial_uses_calculator", agent=agent, input="What is 10! (ten factorial)?", tags=("smoke",), criteria="States that 10! equals 3628800.", expected_tool_calls=("factorial",), ), Case( name="explains_compound_interest", agent=agent, input="Explain compound interest in one short paragraph.", criteria="Explains that interest is earned on both the principal and previously earned interest.", ), ) # 3. 以 CLI 方式运行 if __name__ == "__main__": sys.exit(cli(CASES))运行方式由脚本内置 CLI 提供(详见 suite/README.md):
python cookbook/09_evals/suite/suite_basic.py # 运行全部用例 python cookbook/09_evals/suite/suite_basic.py --list # 仅列出用例,不运行 python cookbook/09_evals/suite/suite_basic.py --tag smoke # 只运行带 smoke 标签的子集 python cookbook/09_evals/suite/suite_basic.py --name factorial_uses_calculator # 按名称筛选 python cookbook/09_evals/suite/suite_basic.py --json-output tmp/evals.json # 输出机器可读 JSON python cookbook/09_evals/suite/suite_basic.py -v # 每个用例渲染完整运行面板这段示例覆盖了日志中的两个核心场景:第一个用例同时启用 Agent-as-Judge 检查(criteria)与可靠性检查(expected_tool_calls),第二个用例仅启用 judge 检查。值得注意的是,Case的name、input为必填,agent/team必须二选一,且至少配置一种检查(criteria、expected_tool_calls 或 scorer 之一),否则在构造期直接抛出ValueError(见 suite.py 第 110-126 行的__post_init__校验)。
三、Case 数据结构详解:配置参数与取值边界
Case是 suite 的原子单元,定义于 suite.py。其字段分为四组,理解每一组才能在声明用例时不踩坑。
3.1 基础字段:谁被测试、如何筛选
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | str | 必填 | 用例名,也是--name筛选与 JSON 载荷中的标识 |
input | str | 必填 | 送入 Agent/Team 的输入文本 |
agent | Optional[Agent] | None | 被测 Agent,与team二选一 |
team | Optional[Team] | None | 被测 Team,与agent二选一 |
tags | Tuple[str, ...] | () | 标签元组,供--tag子集筛选 |
timeout_seconds | Optional[int] | None | 单用例超时(秒),缺省回落到 runner 的default_timeout |
agent与team分离为两个字段是有意设计:源码注释明确说明这是为了镜像AccuracyEval的约定,避免 Team 被塞进名为agent的参数里造成语义混淆。构造时若两者均为空或同时非空,会分别抛出 "provide one of 'agent' or 'team'" 与 "provide only one of 'agent' or 'team'" 的错误。
3.2 Judge 检查:criteria、judge_mode、judge_threshold
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
criteria | Optional[str] | None | 判分标准描述;设置后启用 AgentAsJudgeEval |
judge_model | Optional[Model] | None | 单用例判官模型覆盖,缺省回落到 runner 的judge_model= |
judge_mode | JudgeMode | JudgeMode.BINARY | 二值通过/失败,或 1-10 数值评分 |
judge_threshold | int | 7 | 数值模式的及格线(1-10),仅 NUMERIC 模式生效 |
JudgeMode是定义在 suite.py 的str枚举:BINARY = "binary"(二值判定)与NUMERIC = "numeric"(1-10 打分,达到judge_threshold即通过)。其字符串值直接对应对应AgentAsJudgeEval.scoring_strategy的取值(见 agent_as_judge.py),因此向judge_mode传入等值字符串"numeric"同样被接受。
值得注意的构造期校验:judge_threshold必须落在 1-10 区间,否则抛出 "judge_threshold must be 1-10" 的ValueError。数值模式的底层行为是score >= threshold判定通过(agent_as_judge.py),而NumericJudgeResponse的 schema 用ge=1, le=10约束了评分范围(agent_as_judge.py)。
3.3 可靠性检查:expected_tool_calls 与 allow_additional_tool_calls
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
expected_tool_calls | Optional[Tuple[str, ...]] | None | 期望触发的工具名序列;设置后启用 ReliabilityEval |
allow_additional_tool_calls | bool | True | 为 True 时允许出现期望之外的额外工具调用(子集匹配) |
日志特别记录了 2026-07-05 外部评审修复轮之后的行为:expected_tool_calls=()会在构造期被 falsy 守卫拒绝。其逻辑位于 suite.py:校验采用 truthiness 而非is None,因为criteria=""或expected_tool_calls=()会构造出一个检查"真空通过"的用例——即一个什么也没验证的绿色 CI 门禁。同理scorer is None用is None判断,因为 scorer 实例恒为真值。
3.4 生命周期与扩展:setup/teardown、scorer/expected
Case还提供两组高阶能力:
setup/teardown钩子:setup在运行前执行(不计入超时),其返回值作为 "context" 传给teardown;teardown只要 setup 已完成就必然执行(无论 pass/fail/error/timeout),接收(context, result)以便检查result.error/result.timed_out。同步可调用对象经asyncio.to_thread执行,异步可调用对象被 await。scorer/expected字段:设置scorer后在进程内评分(agno.scorer协议,即任何拥有async ascore(run, expected)的对象),运行于用例超时窗口内,接收(result.response, case.expected);Team 用例的 response 是TeamRunOutput。
源码注释特别强调字段顺序是承重设计:这些 dataclass 并非kw_only,新字段必须追加在末尾,否则会静默重排位置参数调用者。
四、CLI 参数全解析:退出码、JSON 载荷与筛选逻辑
cli()(定义于 suite.py)是对公共 runner API 的纯消费者,其参数解析逻辑在acli()中(suite.py):
| 参数 | 说明 |
|---|---|
--name | 只运行指定名称的用例 |
--tag | 只运行带指定标签的用例 |
--timeout | 默认单用例超时(秒),缺省 120 |
--json-output | 将机器可读 JSON 结果写入指定路径 |
--list | 仅列出被选中的用例而不运行 |
-v/--verbose | 每个用例后渲染完整运行面板(Message、Tool Calls、Response) |
4.1 退出码契约
- 0:所有被选中的用例全部通过;
- 1:任一失败(含
--json-output写入失败); - 2:没有用例匹配选择器(如日志中测试的未知
--tag)。
acli()在无匹配时会打印no cases selected与可用用例名列表并返回 2(suite.py),这与日志记录的行为一致。
4.2 JSON 载荷结构:CI 消费方的稳定契约
SuiteResult.to_dict()(suite.py)生成的载荷是日志反复验证的核心对象,其结构如下:
{ "summary": { "total": 2, "passed": 2, "failed": 0, "status": "PASS" }, "cases": [ { "name": "factorial_uses_calculator", "agent_id": "math-tutor", "team_id": null, "tags": ["smoke"], "session_id": "eval-factorial_uses_calculator-1a2b3c4d", "duration_seconds": 12.345, "judge_passed": true, "judge_reason": "...", "judge_score": null, "reliability_passed": true, "output": "...", "tools_called": ["factorial"], "timed_out": false, "skipped": false, "passed": true, "error": null, "score_value": null, "score_passed": null, "score_reason": null } ] }几个关键设计点:
- 空 suite 的 status 为
FAIL:SuiteResult.status在results为空时直接返回"FAIL"(suite.py)。源码注释点明原因——CI 门禁比较== "PASS",拼错标签绝不能"什么都没运行却绿灯放行发布"。 judge_score仅在数值模式非空:二值模式下为None,数值模式下保存 1-10 分数,以便载荷跟踪质量漂移(而非只有通过/失败)。tools_called为运行期间按序触发的工具名。对 Team 用例,_tool_names()(suite.py)会下沉一层收集member_responses中的工具调用——这正是日志中 Team 用例显示tools_called: ["delegate_task_to_member", "multiply"]的原因:如果不收集成员层,只能看到 leader 的委派调用,看不到成员的真实工具。score_*三字段为 2.8.0 起追加(suite.py 注释),未配置 scorer 时全部为null,对 CI 消费者纯增补、向后兼容。
五、程序化调用:run_cases 与 arun_cases(无控制台 I/O)
测试日志明确记录了run_cases与arun_cases两个程序化入口均被覆盖测试。它们适合 CI 工作流或嵌入式场景,因为runner 本身不做任何控制台 I/O——所有呈现都通过on_case_start/on_run_event/on_case_end三个钩子流出(suite.py)。
import asyncio from agno.eval import Case, run_cases, arun_cases # 同步入口:整个 suite 运行在单一事件循环上 suite_result = run_cases( CASES, tag="smoke", # 可选:标签筛选 name=None, # 可选:名称筛选 default_timeout=120, # 单用例默认超时 judge_model=None, # suite 级判官模型默认值 # db=my_db, # 传入则评测结果写入存储 ) # 异步入口:在已有事件循环内使用 async def main(): suite_result = await arun_cases(CASES) payload = suite_result.to_dict() # 稳定契约,供 CI 消费 print(suite_result.passed, "/", suite_result.total)run_cases是arun_cases的同步包装(asyncio.run),两者共享全部参数。钩子的设计约束值得注意:
- 呈现钩子仅支持同步可调用对象,且直接在事件循环上执行,应保持轻量;异步钩子会被
_call_presentation_hook显式拒绝并抛出TypeError("presentation hooks must be sync callables"),避免"异步钩子返回协程后从未执行"的静默失败。 - 钩子抛异常(或异步钩子被拒)会被记录到该用例的
error字段(前缀hook: ...),不会中止整个 suite。 - 取消行为:某用例以
cancelled状态结束时(服务端cancel_run,或 agno 将 KeyboardInterrupt 转换成的取消),suite 中止并将未运行的剩余用例记为skipped=True、error="skipped: suite aborted after cancelled run",保证载荷与on_case_end钩子看到的用例数一致。
六、Team 数值评分评测:suite_team_scoring 深度解读
日志第二个测试对象是 suite_team_scoring.py:将一个含 calculator 与 writer 两个成员的 Team 作为被测对象,leader 委派任务,且每个答案都用 1-10 数值判官评分。
import sys from agno.agent import Agent from agno.eval import Case, JudgeMode, cli from agno.models.openai import OpenAIResponses from agno.team.team import Team from agno.tools.calculator import CalculatorTools calculator = Agent( id="calculator", model=OpenAIResponses(id="gpt-5.5"), tools=[CalculatorTools()], instructions="Use the calculator tools for every arithmetic operation. Never compute arithmetic yourself.", ) writer = Agent( id="writer", model=OpenAIResponses(id="gpt-5.5"), instructions="Answer in one clear paragraph.", ) assistant_team = Team( id="assistant-team", model=OpenAIResponses(id="gpt-5.5"), members=[calculator, writer], instructions="Delegate arithmetic to the calculator member and writing to the writer member, then report the member's result.", ) CASES = ( Case( name="team_uses_calculator", team=assistant_team, input="What is 4891 multiplied by 7238?", tags=("smoke",), criteria="States that the product is 35,401,058.", judge_mode=JudgeMode.NUMERIC, judge_threshold=7, expected_tool_calls=("multiply",), ), Case( name="team_explains_clearly", team=assistant_team, input="Explain compound interest in one paragraph.", criteria="Explains that interest is earned on both the principal and previously earned interest.", judge_mode=JudgeMode.NUMERIC, judge_threshold=7, ), ) if __name__ == "__main__": sys.exit(cli(CASES))日志记录的两项 Team 关键事实在源码中均有对应实现:
- 载荷中
team_id: "assistant-team"、agent_id: null:CaseResult仿照AccuracyEval拆分,Agent 用例填agent_id,Team 用例填team_id,另一侧保持None(suite.py),由_component_id()统一取值(team.id or case.name)。 tools_called: ["delegate_task_to_member", "multiply"]:如 4.2 节所述,_tool_names()递归收集member_responses中的工具调用。同时,可靠性检查通过team_response=注入(suite.py):ReliabilityEval收到agent_response还是team_response取决于响应类型——agent 的RunOutput走前者,Team 的TeamRunOutput走后者;reliability.py中的_collect_member_evidence()(reliability.py)会递归收集每层成员响应的工具执行与消息,因此 leader 的delegate_task_to_member与成员的真实multiply都能被统计。
数值评分模式下的judge_score: 10来自AgentAsJudgeEval的NumericJudgeResponse(结构化输出{score: int 1-10, reason: str}),scoring_strategy="numeric"时以score >= threshold判过(agent_as_judge.py),且 suite 层的JudgeMode.NUMERIC字符串值与之一致,judge_threshold=7直接透传为判官的threshold。
七、底层原理:一次 Case 的完整执行流程
把日志中的行为映射到源码,一次 Case 的运行流程如下(对应_arun_case与_run_case_body,suite.py):
- 生成独立会话:每个用例分配
session_id=f"eval-{case.name}-{uuid4().hex[:8]}",保证评测流量不污染 Agent/Team 的历史记录;db=设置时该会话会关联存储的 trace。 - 执行 setup 钩子:在超时窗口之外运行,失败则跳过运行阶段。
- 流式运行被测对象:以
arun(input=..., stream=True, stream_events=True, yield_run_output=True)迭代事件。RunOutput/TeamRunOutput在流中到达时立即提交响应与证据字段——即使后续流停滞(如持久化挂起)触发超时,已产出的结果也不会丢失。 - 错误事件捕获:Agent、Team、Workflow 三种错误事件类(
_RUN_ERROR_EVENTS元组)都会被识别并记录为agent:/team:前缀的错误,而非静默失败。 - 可评分性判定:仅当无错误、存在响应且
status == RunStatus.completed时才进入评分。paused/cancelled 等状态携带占位内容(如 HITL 样板文本),不视为真实答案;未完成状态按_STATUS_ERRORS映射为可读错误。 - Judge 检查:配置了
criteria则构造AgentAsJudgeEval(沿用judge_mode字符串值作为scoring_strategy,judge_threshold作为threshold,关闭 spinner 与遥测)对(input, output)评分。 - 可靠性检查:配置了
expected_tool_calls则构造ReliabilityEval,Agent 走agent_response=、Team 走team_response=。 - Scorer 检查:配置了
scorer则调用ascore(response, expected)。 - teardown 钩子:在
finally中确保即使超时/错误也会执行,失败以cleanup:前缀记录。 - 汇总:
duration_seconds精确到毫秒,结果汇入SuiteResult。
这一流程解释了日志中所有行为断言:为何未知 tag 退出码为 2、为何空 suite 为 FAIL、为何 Team 的成员工具可见、为何expected_tool_calls=()在构造期就被拦截。
八、在 CI 中落地 Eval Suite
综合测试日志与 README 的指引,将 suite 接入 CI 的标准模式是:
- 以 JSON 载荷为门禁输入:运行
python cookbook/09_evals/suite/suite_basic.py --json-output tmp/evals.json,退出码 0 即通过;同时解析summary.status == "PASS"作为双保险。 - 分层筛选:日常开发用
--tag smoke跑冒烟子集,发版前跑全量。 - 程序化集成:CI 编排器中直接
run_cases(CASES, tag=...)并读取to_dict(),runner 无控制台 I/O 的特性保证输出纯净。 - 关注载荷中的证据字段:
tools_called、judge_reason、duration_seconds足够在单条 JSON 里定位失败根因(日志中 "enough evidence to debug a failure from the payload alone" 正是CaseResult的设计目标)。
九、与既有评测体系的关系
Eval Suite 并非孤立功能,而是 Agno 评测体系(libs/agno/agno/eval/包)的上层编排器。其内部复用了两个成熟组件:
- AgentAsJudgeEval(agent_as_judge.py):以 LLM 为判官,
BinaryJudgeResponse/NumericJudgeResponse两种结构化输出 schema 支撑二值/数值评分;prompt 通过fence_untrusted对被评测输出做不可信数据隔离。 - ReliabilityEval(reliability.py):核对实际工具调用与期望集,
ReliabilityResult区分 failed/passed/additional/missing 工具调用与参数检查结果。
agno.eval包通过懒加载__getattr__导出(init.py)规避与Agent的循环导入。同目录的 accuracy.py、performance.py 则提供准确率与性能评测,suite 负责将它们组合成可调度的用例集。
结语
cookbook/09_evals/suite/TEST_LOG.md不仅是一份测试记录,更是一份浓缩的 Eval Suite 行为规范。透过它可以看到:声明式的Case、稳定可消费的to_dict()契约、严谨的空检查守卫、Team 场景下对成员工具调用的深度可见性,以及"runner 与呈现解耦"的架构取舍。按本文的 CLI 参数与程序化入口,你可以立即将评测套件接入自己的 CI 门禁,让每次 Agent/Team 变更都有可追踪、可断言的自动化质量反馈。
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考