Agno ToolCallScorer 实战指南:用工具执行证据验证 Agent 行为,而非仅看回答文本
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
本篇技术指南聚焦 Agno 开源仓库中cookbook/environments/_05_tool_call_scorer/这一环境(Environment)示例,讲解如何用ToolCallScorer从运行记录中读取“成功的工具执行”来评分:流畅的回答并不足够,评分器只认证据。读完本文,你将掌握三种递进的校验姿势——仅按工具名匹配、同时校验参数子集、用严格模式拒绝意外工具调用——并能结合run_rollouts在多次尝试上量化通过率,为基于检索、查询或动作的任务建立可靠的可靠性基线。
ToolCallScorer 要解决什么问题
在 Agent 评测中,一个常见陷阱是“只看最终回答文本”。当任务的可靠性取决于**接地(grounding)、查询(lookup)或动作(action)**时,模型即使没有真正调用工具,也可能凭训练记忆编造出一段看起来合理的回答。
ToolCallScorer的核心设计是:只读取RunOutput.tools——即实际发生的执行记录(executions),而不是请求记录(requests)。为什么必须区分这两者?从 scorer/tools.py 的类注释可以看到关键原因:
Request-side matching counts a call that was refused, errored, or given junk arguments, which makes it satisfiable without the tool ever doing work.
即:如果按“请求”匹配,一次被拒绝、报错或携带垃圾参数的调用也会被计入,评分器在没有工具真正工作的情况下就能被打满分。而ToolCallScorer中的一次期望,只有满足以下条件的**干净执行(clean execution)**才能满足:
- 该调用的
tool_call_error不为真(None视为成功——恢复(rehydrated)的运行对成功调用携带None); - 该调用未被暂停(
is_paused不为真,暂停状态仅出现在等待确认/输入/外部执行时,恢复后被清除); - 被模型拒绝(refused)的调用根本不会进入
run.tools。
这套判定逻辑在 scorer/tools.py 的score()中实现:先从执行列表里过滤出clean列表,再据此构建clean_names集合用于后续匹配。
运行环境与示例文件
本示例位于 cookbook/environments/_05_tool_call_scorer/ 目录,包含三个文件,分别演示一种递增的校验策略:
| 文件 | 校验内容 | 关键参数 |
|---|---|---|
basic.py | 要求一次指定名称的工具执行 | expected_tools+allow_additional=False |
with_arguments.py | 同时要求工具名与精确的参数子集 | expected_tools+arguments |
strict_tools.py | 拒绝意外工具名的干净执行 | expected_tools+allow_additional=False |
运行方式(仓库根目录下):
python cookbook/environments/_05_tool_call_scorer/basic.py python cookbook/environments/_05_tool_call_scorer/with_arguments.py python cookbook/environments/_05_tool_call_scorer/strict_tools.py每个脚本都要求设置OPENAI_API_KEY,所有示例均通过OpenAIResponses使用gpt-5.5模型(reasoning_effort="low")。三个示例共享相同的运行骨架:用run_rollouts(env, k=6, concurrency=6)对每个任务跑 6 次尝试,打印task_result.n_passed / task_result.n_scored的统计结果。
说明:仓库 TEST_LOG.md 中的实测记录显示这些任务是针对 gpt-5.5 校准的——模型对“是否执行一次多余的备用工具调用”存在不稳定行为,这正是评测要暴露的“学习区(learning zone)”。换用其他模型或 API 时通过率会变化,请把重点放在评分语义而非具体数值上。
三级校验策略详解
原文档给出了明确的使用建议:先做名称匹配;当被调用的“资源”本身很重要时,加上参数校验;当意外工具类型不安全或代价昂贵时,启用严格模式。
第一级:仅校验工具名(basic.py)
basic.py 构造了一个同日发货(same-day dispatch)运营场景:Agent 需要调用lookup_shipping_cutoff查询官方发货截止时间来做决策;同时配了一个诱饵工具lookup_backup_carrier(备用承运商),并按路由规则只有当主承运商宕机时才需要调用它。
scorer=ToolCallScorer( expected_tools=["lookup_shipping_cutoff"], allow_additional=False, ),这里的校验目标是一石二鸟:既要求 Agent 的答案接地(必须调用lookup_shipping_cutoff),又要求它没有多余调用(allow_additional=False时,任何一次lookup_backup_carrier的干净执行都判失败)。三个任务精心构造:
direct-lookup:主承运商正常,只需查 North 截止时间;checksum-route-a/checksum-route-b:用一串确定性的校验和运算(大整数乘法 → 逐位求和 → 乘常数 → 取模)来决定主承运商是否宕机,从而“诱惑”模型在不需要时也去调用备用工具。
从 TEST_LOG.md 可以看到这组任务原本经历了一次修复:仅做“存在性检查”(presence-only)会让结果饱和——gpt-5.5 总是调用必需工具,而旧的奇偶路由又会让一次多余的调用被计为成功。加入诱饵工具和allow_additional=False之后,网格(k=6)变为:direct-lookup6/6、checksum-route-a0/6、checksum-route-b5/6(0.83,处于学习区)。干净的运行与被“注水”的运行,靠执行记录区分,而不是靠文本措辞——这正是该示例要传达的核心思想。
第二级:追加参数子集校验(with_arguments.py)
仅按名称匹配无法判断 Agent 是否查询了“正确的记录”。当区域(region)、服务(service)、生效日期(effective_date)本身属于待验证的行为时,就要用arguments参数指定期望的参数子集。with_arguments.py 中的quote_shipping_cutoff工具接收三个参数,评分器要求它们精确匹配:
scorer=ToolCallScorer( expected_tools=["quote_shipping_cutoff"], arguments={ "quote_shipping_cutoff": { "region": "north", "service": "priority", "effective_date": "2026-07-20", } }, ),任务同样用校验和运算制造歧义:让 Agent 在两条重复记录之间、或在priority/express、north/north-east之间做选择。从源码 scorer/tools.py 可知参数匹配是子集语义:期望的每个键都出现在ToolExecution.tool_args中且值相等即匹配,允许执行中携带额外键,且不做任何类型强转(no type coercion)。arguments中同一个工具名既可以给单个 spec 字典,也可以给 spec 字典列表(任一匹配即满足)。实测中,早期的常量字符串任务三行全部 6/6 饱和,改成“确定但困难”的校验和路由后暴露出了中间区间。
第三级:严格拒绝意外工具(strict_tools.py)
当意外的工具类型不安全或昂贵时,用allow_additional=False拒绝“多余工具名的干净执行”。strict_tools.py 提供了两个工具:必需的lookup_shipping_cutoff和可选的minutes_between。评分器要求lookup_shipping_cutoff必须被干净执行,同时任何一次minutes_between的干净执行都会导致失败:
scorer=ToolCallScorer( expected_tools=["lookup_shipping_cutoff"], allow_additional=False, ),lean-anchor任务明确要求“只用查询,减法自己算”;两个checksum-route-*任务则用校验和决定是否需要minutes_between。实测(k=6)为:lean-anchor6/6、checksum-route-a3/6(0.50)、checksum-route-b4/6(0.67),失败行正是多余的成功minutes_between执行——严格模式要抓的就是这个。
这里必须强调原文档中的一个精确区分,源码中也有对应说明(scorer/tools.py):严格模式是“工具名集合语义”(tool-name set semantics),并不检查某个期望工具的精确调用基数(exact call cardinality)。也就是说,同一个期望工具被重复调用两次,仍然只算满足一次期望;allow_additional=False只拒绝“不在期望集合里的名字”的干净执行。
ToolCallScorer 的完整参数与底层语义
构造签名位于 scorer/tools.py:
ToolCallScorer( expected_tools: Sequence[str], # 必填:期望的工具名序列 arguments: Optional[Dict[str, ...]] = None, # 可选:工具名 -> 参数 spec(dict 或 list) allow_additional: bool = True, # 可选:是否允许期望之外的工具被干净执行 )几个容易踩坑的构造约束(源码中会在构造期直接抛错):
expected_tools必须是序列,传入裸字符串会抛TypeError;- 重复的期望工具名会被去重(保留声明顺序),即重复期望名只算一个检查;
- 若
arguments中某个工具名对应的 spec 列表为空,会抛ValueError(空列表什么也检查不了); - 若
expected_tools为空且arguments贡献的检查数为 0,会抛ValueError——一个零检查的评分器会真空地给所有运行亮绿灯,因此被直接拒绝。
评分行为(score())总结:
- 匹配与顺序无关:期望由“该名字至少一次干净执行”满足,不考虑调用先后;
- value 计算:
value = n_satisfied / n_checks,名称期望与参数 spec 等权重; - passed 判定:所有检查满足即
passed=True;若allow_additional=False且存在额外干净调用,则passed=False并在reason中列出多余工具名; - 特殊边界:没有任何执行时,
reason会以 “run has no tool executions” 开头;expected_tools与arguments中的名字永远不会被判为 “additional”(否则一个纯参数严格评分器将永远无法满足); - 团队输出:对于
TeamRunOutput,只检查顶层(leader)的执行;当成员响应携带工具时,reason会注明“member tool executions were not inspected (member matching is out of scope for 2.8.0)”,如实声明边界。
此外,ToolCallScorer实现了digest()方法(scorer/tools.py):对expected_tools(排序)、arguments、allow_additional做 canonical JSON 后取 sha256,用于生成环境的env_fingerprint。这意味着修改评分器配置会改变环境指纹,从而让基线对比(diff)能够发现“环境漂移”。
与 Environment / run_rollouts 的配合
评分器不是独立使用的,它挂在Environment上由run_rollouts驱动(三个示例的用法完全一致):
env = Environment( name="tool-name-matching", agent=agent, tasks=(Task(id="direct-lookup", input=...), ...), scorer=ToolCallScorer(...), ) result = run_rollouts(env, k=6, concurrency=6) for task_result in result.task_results: print(f"{task_result.task.id}: {task_result.n_passed}/{task_result.n_scored} ...")从 environment.py 的源码可以看到Environment是一个frozen=True的 dataclass:name、tasks、scorer、agent以及可选timeout_seconds(默认 120)。frozen=True保证“连线不被重新绑定”,即结果确实来自这套任务集、评分器与策略对象。Task支持input、expected、id、metadata四个字段,也支持从 JSONL 加载(Task.from_jsonl,多余顶层键会抛错)。
runner.py 中的arun_rollouts/run_rollouts会:
- 解析任务 id(未声明的按
t1..tN位置补全),重复 id 抛错; - 对每个任务跑 K 次隔离的尝试:每次尝试都换上新内存数据库、新用户 id、关闭响应缓存,并切断所有写路径(memory capture、knowledge/learning 写、session-summary 写、
save_response_to_file),知识读取则保留——从而保证统计不被污染; - 对每次尝试调用
scorer.score(),产出TaskResult。每个TaskResult提供n_scored、n_passed、pass_rate、mean_value,以及in_learning_zone(部分尝试通过、部分失败,即“学习区”)。
何时使用:选择评分维度的决策路径
原文档给出了明确的决策框架,结合上面的三级示例可以整理为一张实用的选择表:
| 你的可靠性风险 | 推荐校验维度 | 示例 |
|---|---|---|
| 答案是否接地、是否执行了必需查询 | expected_tools名称匹配 | basic.py |
| 是否查到了正确的记录(资源很重要) | arguments参数子集匹配 | with_arguments.py |
| 意外工具类型是否安全/昂贵 | allow_additional=False严格模式 | strict_tools.py |
还可以组合:例如既校验expected_tools又校验arguments再叠加allow_additional=False,实现“必须调用 A 工具、参数必须匹配、且不允许任何其他工具”的三重约束。唯一需要注意的是严格模式的集合语义——它防“意外名字”,不防“期望工具的重复调用”。
本示例在环境评测系列中的位置
该目录位于cookbook/environments/环境评测系列中,承接两个相邻示例:
- 前一个示例
_04_judge_scorer/讲解基于评分规则(rubric)的裁判式检查,适合评估回答质量类任务; - 后一个示例
_06_learning_zone/则把混合结果(部分通过、部分失败的任务)转化为任务选择信号,用于进一步的数据筛选与训练集构建。
ToolCallScorer与之的定位差异在于:它不评估“说得好不好”,而是机械地、确定性地验证“做了没有”。这也让它可以和裁判式评分器组合使用——回答质量交给 rubric,行为合规交给工具执行证据。
小结
ToolCallScorer是 Agno 环境评测体系中“行为证据”维度的关键评分器:它只认可干净、成功、且未被暂停的工具执行,按名称与参数子集做与顺序无关的匹配,并通过allow_additional=False提供严格模式。结合run_rollouts的 K 次隔离尝试,你能得到每个任务真实的通过率与学习区判定,把“答案流畅但动作可疑”的情况暴露在数字之下。如果要继续深入,建议阅读 scorer/tools.py 的完整实现,以及 environment.py 中关于env_fingerprint/policy_fingerprint的环境漂移检测机制。
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考