news 2026/9/10 2:13:03

agno ToolCallScorer 实测校准日志解读:从饱和网格到学习区的工具调用评分

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
agno ToolCallScorer 实测校准日志解读:从饱和网格到学习区的工具调用评分

agno ToolCallScorer 实测校准日志解读:从饱和网格到学习区的工具调用评分

【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno

本文基于 agno 仓库cookbook/environments/_05_tool_call_scorer/目录下的测试日志(TEST_LOG.md)与配套示例,讲解ToolCallScorer如何把"Agent 是否真的执行了任务所依赖的工具"变成可判定的评分标准,并结合 libs/agno/agno/scorer/tools.py 的源码剖析其匹配语义,完整还原三个示例脚本从"全部饱和"到落入学习区(middle band)的校准过程。读完本文,你可以掌握ToolCallScorer的三级校验设计(名称匹配、参数子集匹配、严格名称集)、k=6网格测试的判读方法,以及如何为任务设计"确定性但困难"的校验和路由来构造有效评分区间。

为什么"回答正确"不够:工具调用评分器的定位

该目录的 README.md 开宗明义:流畅的回答并不够,ToolCallScorer从运行记录中读取成功的执行(executions)并为证据打分。当可靠性取决于接地(grounding)、查询(lookup)或动作(action)而非单纯的答案文本时,就应该使用这类评分器,并遵循渐进策略:

  • 先用名称匹配(name matching)确认期望工具被调用了;
  • 当"调用了哪个资源"也属于被验证行为时,叠加参数检查(argument checks);
  • 当意外工具类型不安全或代价高昂时,使用严格模式(strict mode)。

README 还特别澄清了严格模式的边界:它采用工具名称集合语义(tool-name set semantics),并不强制对期望工具检查精确的调用次数(call cardinality)。目录内三个示例文件分别对应这三个层级:

示例文件校验目标
basic.py要求至少一次命名工具执行,且拒绝多余的意外调用
with_arguments.py同时要求工具名与精确的参数子集匹配
strict_tools.pyallow_additional=False拒绝任何意外工具名的干净执行

该目录是 environments 教程序列的一环:它承接 cookbook/environments/_04_judge_scorer/ 的评分器主题,并为 cookbook/environments/_06_learning_zone/ 把"混合结果转化为任务选择信号"做准备。

ToolCallScorer 源码级评分语义

构造参数

ToolCallScorer定义在 libs/agno/agno/scorer/tools.py#L26,构造函数接受三个参数:

参数类型默认值说明
expected_toolsSequence[str]必填期望出现干净执行的工具名序列;内部去重且保序(重复名称视为同一检查项)。直接传裸字符串会抛TypeError
argumentsDict[str, Union[Dict[str, Any], List[Dict[str, Any]]]]None每个工具名对应一个参数字典或参数字典列表;子集匹配
allow_additionalboolTrueFalse时,任何不在期望集合内的干净工具执行都会使passed置为False

构造函数还做了两项防御性校验(见 tools.py#L55-L78):空的参数字典列表会被拒绝("an empty list checks nothing");如果expected_toolsarguments都不提供任何检查项,直接抛ValueError——避免一个"零检查"评分器对每次运行都空转地变绿。

评的是"执行",不是"请求"

这是该评分器最核心的设计决策。源码 docstring 指出:请求侧(request-side)匹配会把被拒绝、报错或参数垃圾的调用也计入,从而让评分在工具从未真正干活的情况下也能被满足。ToolCallScorer的期望只会被满足当存在一条tool_call_error不为真的执行。具体过滤逻辑在score()中:

# libs/agno/agno/scorer/tools.py L86 clean = [t for t in executions if not t.tool_call_error and not t.is_paused]

即"干净执行"同时排除了两类:报错/被拒的调用(tool_call_error),以及处于暂停态、尚未真正执行的调用(is_paused仅在等待确认/输入/外部执行时为真,恢复后会清除)。注意被模型完全拒绝的调用根本不会进入run.tools

匹配与打分规则

score()方法(tools.py#L80-L130)的规则可概括为:

  1. 名称检查:每个expected_tools项计一次检查,只要该名称出现在干净执行集合中即满足。匹配与顺序无关,且是集合语义——期望名重复时一次调用即可满足。
  2. 参数检查:对arguments中的每个 spec,采用子集匹配——spec 中的每个键都必须存在于ToolExecution.tool_args中且值相等,实际参数允许多出额外键,且不做类型强转"17:00"17.0不算相等)。一个工具可以挂多个 spec(传列表),任一候选执行命中即满足该 spec。
  3. 额外调用判定allowed_names = expected_tools ∪ arguments.keys(),即评分器自己期望的调用永远不算"额外"——这保证了只用arguments的严格评分器仍然可满足。
  4. 输出value为已满足检查项的占比(名称检查与参数检查等权),passed要求全部检查满足且在allow_additional=False时没有额外干净调用。

Score本身是一个强制value ∈ [0, 1]的 dataclass(libs/agno/agno/scorer/base.py),附带reason(失败原因列表拼接)与可选detail

两个值得注意的边界行为:

  • per-taskexpected不被参考score/ascoreexpected参数(来自Task.expectedCase.expected)对工具评分器是无效输入,工具期望只存在于构造器的expected_tools上。这意味着"每个任务期望值是答案文本"的任务套件仍可挂这个评分器,只是期望值到不了它。
  • Team 场景的已知限制:对TeamRunOutput只检查顶层tools(即 leader 的执行);成员工具匹配在 2.8.0 中不在范围内,若成员响应携带工具,Score.reason会明确标注这一限制。
  • 指纹digest()(tools.py#L135-L146)对expected_toolsargumentsallow_additional的规范化 JSON 求 sha256,参与env_fingerprint计算,这是后续基线回归、CI 门禁等章节比较运行结果合法性的基础。

三个官方示例的构造方式

三个脚本共享同一套骨架:Agent+Environment+Task+ToolCallScorer,通过run_rollouts(env, k=6, concurrency=6)做 6 次重复采样(run_rollouts是 libs/agno/agno/environments/runner.py#L1221 中异步门arun_rollouts的同步封装,默认k=8concurrency=4)。模型统一为gpt-5.5OpenAIResponsesreasoning_effort="low"

basic.py:名称匹配 + 拒绝多余调用

场景是同日发货规划:Agent 必须调用lookup_shipping_cutoff接地官方截止时间;另有一个诱饵工具lookup_backup_carrier(备用承运商查询),指令明确"仅当路由规则判定主承运商停运时才调用"。三个任务中两个(checksum-route-a/b)嵌入了大整数运算:

Multiply 2718281828459045 by 1618033988749895. Add every decimal digit of the product, multiply that sum by 131071, then subtract the product remainder modulo 65521. If the final integer is even, the primary carrier is down, so also look up the North backup carrier…

运算结果是确定性的,但计算难度足以让模型"不一致地"跟随或违背诱饵路由——这正是文件 docstring 所说的学习区来源:

scorer=ToolCallScorer( expected_tools=["lookup_shipping_cutoff"], allow_additional=False, )

评分器要求接地查询必须发生,同时任何顺带触发的备用查询都会判负。主程序打印每个任务的n_passed/n_scored

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} grounded with no unnecessary call")

with_arguments.py:精确参数子集

场景是运费截止时间询价工具quote_shipping_cutoff(region, service, effective_date="latest")。名称匹配无法区分"查对了记录"还是"查错了记录",于是加上参数子集:

scorer=ToolCallScorer( expected_tools=["quote_shipping_cutoff"], arguments={ "quote_shipping_cutoff": { "region": "north", "service": "priority", "effective_date": "2026-07-20", } }, )

三个任务(checksum-datechecksum-servicechecksum-region)各自用一个校验和运算决定日期、服务类型或区域的正确取值——比如checksum-date要求"最终路由码为奇数则引用 2026-07-20,偶数则引用 2026-07-21"。评分器验证的始终是那个固定参数子集,变化的是任务侧把正确值藏在了多深的计算后面。

strict_tools.py:严格名称集

工具集为lookup_shipping_cutoffminutes_between(两个 HH:MM 时刻的分钟差)。任务的时间计算刻意放在"模型可能心算,也可能顺手调用第二个工具"的边界上;lean-anchor明确说"只做减法,不要用工具",两个checksum-route-*则用校验和奇偶路由"奇数自己算,偶数才允许调用minutes_between"。评分器:

scorer=ToolCallScorer( expected_tools=["lookup_shipping_cutoff"], allow_additional=False, )

如 README 强调的,这是名称集的严格性minutes_between哪怕只被干净地执行一次也会判负;但对期望工具lookup_shipping_cutoff自身重复调用若干次,不会因次数问题被惩罚。

测试日志解读:一次完整的评分区校准

TEST_LOG.md 是该目录的测试记录,包含两轮:首轮在 Agno 2.7.4 上对gpt-5.5(经OpenAIResponses)测试(2026-07-20),随后在fix/cookbooks-claude分支(Agno 2.8.0 源码)上复测。它最有价值的部分是展示了"如何把一个全饱和的测试网格修成有区分度的网格"。

首轮测试(Agno 2.7.4):三个脚本全部 PASS

basic.py——"对三个需要当前运费策略查询才能正确路由的任务做仅名称的执行匹配"。在修正任务/评分器契约后的最终实跑:

任务通过率
direct-lookup6/6(1.00)
checksum-route-a4/6(0.67)
checksum-route-b5/6(0.83)

日志特别说明:两条路由行都是"真实的二值部分通过行";更早的模糊版本被丢弃,因为它在"提示词本就允许跳过查询"时仍给查询打分——即评分契约与任务契约不一致。

with_arguments.py——"带精确区域/服务/日期参数子集的工具执行匹配"。最终实跑(常量化字符串清理后):checksum-date5/6(0.83)、checksum-service4/6(0.67)、checksum-region6/6(1.00)。日志记录了两次失败的校准:"前两次校准都饱和了"——原始带日期行全部 6/6;第一次消歧修订的三个行也全部 6/6。最终方案是把含糊措辞替换为"确定性但困难"的校验和路由,在不改变评分器验证对象的前提下暴露出中间档

strict_tools.py——"严格执行匹配:要求查询名出现,同时拒绝意外的minutes_between"。最终实跑:lean-anchor6/6(1.00)、checksum-route-a3/6(0.50)、checksum-route-b4/6(0.67)。路由行的失败全部是"成功的多余minutes_between执行"——恰是严格模式要抓的东西。日志同样记载了初始两行校准是一次失败的全满网格(borderline-subtractiontempting-calculator均 6/6),随后被校验和条件工具路由取代。

2.8.0 源码复测:basic.py 的修复

复测记录(2026-07-20,fix/cookbooks-claude,Agno 2.8.0 源码)标记basic.py — FIXED,修复动机有两点:

  1. 仅存在性检查(presence-only check)饱和了gpt-5.5总会发起那个必需的工具调用,所以"是否调用了lookup_shipping_cutoff"这一单项检查失去区分度;
  2. 旧的奇偶路由让不必要的调用也算成功:评分契约与文件自身文案("without making an unnecessary lookup count as success")不一致。

修复方式是三管齐下:新增诱饵lookup_backup_carrier、增加引诱它的路由规则、设置allow_additional=False——这正是当前仓库中 basic.py 源码的形态。日志给出的复测网格(k=6):

任务结果
direct-lookup6/6
checksum-route-a0/6
checksum-route-b5/6(0.83,落在学习区)

"zone is now the inconsistently-slipped backup call"——学习区从"是否发起必需查询"转移到了"不一致地溜进来的备用查询"上。同批复测中,strict_tools.pychecksum-route-a0.50、checksum-route-b0.67)与with_arguments.pychecksum-date0.50)重跑干净、脚本无改动。

校准方法论的三点启示

  1. 饱和网格是最常见的失败模式:三个脚本在初版或早期修订中均出现过"所有任务 6/6"的全满网格,说明任务对当前模型太简单,评分器无法产生信号。
  2. 修复手段要落在任务侧而非评分器侧:with_arguments 的日志明确说最终方案"exposed the middle band without changing what the scorer verifies"——校验和运算只增加推理难度,评分器验证的参数子集保持不变。
  3. 评分契约必须与任务契约逐字对齐:basic.py 两次迭代(丢弃允许跳过查询的模糊版本、2.8.0 复测中用allow_additional=False对齐文案)都源于同一问题:评分器奖励了任务其实允许的行为。

复现与运行

运行前置条件:环境变量OPENAI_API_KEY,且全部示例使用gpt-5.5OpenAIResponses。官方给出的运行命令(见 README.md):

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

解读输出的要点:

  • 每次run_rollouts(env, k=6, concurrency=6)对每个任务采样 6 次,打印格式为<task_id>: <n_passed>/<n_scored>,即二值通过的次数/参与评分的次数;TEST_LOG 中"0.83""0.67"等数值就是这些比率的四舍五入形式。
  • Score.value是检查项满足占比(0~1 连续值),Score.passed才是进入n_passed统计的二值结论;Score.reason会给出形如 "expected tool 'x' has no clean execution" 或 "additional tool calls not allowed: [...]" 的失败原因,可直接用于错误分析(参见 cookbook/environments/_19_error_analysis/ 的做法)。
  • 结果对象带有env_fingerprintpolicy_fingerprint,其中包含ToolCallScorer.digest()的 sha256 摘要——更换期望工具或参数子集会改变指纹,基线比较(cookbook/environments/_13_saved_baselines/)会据此拒绝不可比的结果。

适用边界与限制

  • 名称匹配仍可用垃圾参数满足:源码 docstring 明确"Name-only matching remains satisfiable by a successful call with junk arguments. For a strict check, setarguments",即只做名称检查时,一次参数错误的成功调用同样通过;需要严格检查时必须设置arguments
  • Team 场景只校验 leader:对TeamRunOutput仅检查顶层(leader)的工具执行,成员执行不被检查(2.8.0 范围内),Score.reason会标注该限制。
  • expected_tools必须是序列:传裸字符串会抛TypeError;"零检查"构造会被ValueError拦截。
  • 模型与运行环境:文中所有通过率数据均来自gpt-5.5reasoning_effort="low",经OpenAIResponses)在 2026-07-20 的实测(Agno 2.7.4 与 2.8.0 源码两个版本),换用其他模型或提高推理强度后,学习区位置会移动,需要重新校准网格。

综合来看,这个目录的测试日志展示的不仅是一次功能验证,而是一套可复用的"评分区校准"工作流:先跑k=6网格定位饱和行,再把难度注入任务文本(校验和路由、诱饵工具、参数消歧),始终保持评分器的验证对象不变,直到任务落在"模型不一致地通过"的中间档——此时通过率才是可用于任务选择与回归门禁的有效信号。

【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:11:22

CC Switch与Codex协同实现LLM协议适配与本地代理调度

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 2:10:35

PDF/JSON解析报错garbage at the end:尾部垃圾数据定位与修复指南

你打开一个PDF&#xff0c;或者跑一条文档解析脚本&#xff0c;迎面来一句&#xff1a;garbage at the end of the document。翻译成人话就是&#xff1a;文档结尾有垃圾数据。我第一次看到这个提示是在用命令行工具处理一批标注过的PDF时&#xff0c;当时以为是工具坏了&#…

作者头像 李华
网站建设 2026/9/10 2:09:11

华为MetaERP总账模块核算场景与会计分录详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华