Kimmal 发布 TOOLCALL-300 数据集,揭示当前 LLM 工具调用失败率高达 96.7%,240/250 可修复案例在 schema 层面遭误判。
如果你的 Agent 把工具列表交给模型,然后把返回结果原封不动传给执行函数,这篇文章就是关于这种做法有多少时候会出错、以及具体是哪些类型的错误。
TOOLCALL-300 是一个包含 300 条标注好的工具调用数据集,这些调用都与声明的 schema 不匹配——12 个失败类别,每类 25 个 case——附带声明的工具、正确答案(ground truth),以及一个对适配器进行评分的评分器。第一个跑分的是对照组:解析输出,原样传递,不做任何修复。
TOOLCALL-300 adapter: naive
cases 300
exact match 10 / 300 (3.3%)
refused correctly 10 / 50
invented calls 40
false refusals 50
schema-invalid returns 240
对照组得分的每一分都来自完全损坏以至于根本无法解析的输出。在 250 个存在正确答案可以恢复的 case 上,naive 适配器得分为 0。在 50 个诚实答案是"此处无调用"的 case 上,它有 40 次向服务器发送了一个调用,其中包含了全部 25 次对从未声明过的工具的调用。
十二个类别
wrong_tool_name - missing_required_arg - extra_undeclared_arg - type_coercion - enum_violation - nested_flattened - array_vs_scalar - args_as_string - multiple_calls - hallucinated_tool - truncated - unrecoverable。每类 25 个 case。
300 个 case 中有 50 个完全不包含正确调用。在这些 case 上,唯一能通过的答案是拒绝调用,而返回 {"name": ..., "arguments": {}} 会被判定为失败而非接近成功。把"模型没有产生可用调用"变成"模型调用了一个空参数的tool"是这套测试集存在的具体原因,因为后者到达你的服务器时看起来就像一条真实指令。
那个数字不是一种声明
测试集附带了一个参考规范化器 toolshim.py,根据评分规范编写。它得分 293/300(97.7%),50 次拒绝中正确 49 次,0 次错误拒绝,1 次虚构调用。
97.7% 是一个 in-sample 数字,作为声明毫无价值。语料库和规范器由同一作者根据同一规则手册编写,所以这个数字测量的只是与规则手册的一致性,别无他用。公布它是因为藏着更糟——而且因为有趣的部分是:根据规范编写的规范化器仍然在 7 个 case 上失败了。12 个类别中有 10 个是 25/25;剩下两个不是的是 truncated 19/25 和 unrecoverable 24/25。
7 个失败 case 在 README 中有命名,但未做修复,因为在看到分数后再去修复就把一次测量变成了一种声明:
4 个 case——从一个开括号中虚构出一个空容器。流在 "tags": [ 处停止。规范化器关闭了数组并输出了 "tags": []。模型从未写过一个 tag。一个空数组不是"无值";它是一个值,收到清空字段指令的服务器会真的清空它。
2 个 case——一个完整元素被保留在从未关闭的数组内。"tags": [ "regression" 变成了 ["regression"]。这里存在真正的歧义:语料库采用严格解读(容器从未关闭的属性没有被完全写出),规范化器采用宽松解读。两者都有可辩护的理由,且语料库在所有 25 个 truncated case 上保持一致,标签在打分之前就已固定——所以你确切地知道 300 个 case 中哪 2 个你会不同意。
1 个 case——一个数字在被中断写入时被当作完整的。"days": 1 是一个被截断流的末尾;原始值可能是 1、12 或 14。返回的调用是有效的、合理的,但是错误的。下游没有任何方式能检测到它。这是整套测试中最糟糕的失败。
用你自己的代码跑一下
30 个 case、评分器、生成器和 toolshim.py 均为公共领域(CC0),永久免费,无需账号和邮箱。toolshim.py 打包在 zip 里;不是作为散文件提供服务。
curl -O https://toolkitlabs.org/toolcall300/toolcall300-free.zip
unzip toolcall300-free.zip && cd toolcall300-free
python3 score.py --corpus sample30.jsonl --adapter naive # the control
python3 score.py --corpus sample30.jsonl --adapter yourmodule:normalise
python3 score.py --spec # the grading contract
python3 score.py --selftest # 22/22
zip 文件 35445 字节,sha256 a9284eb4304f5eb265ba4c55844513cbba0c0d3cd9aebe235b51c3071ed4fa03。
如果你的代码不是 Python,--adapter-cmd "..." 运行一个子进程:标准输入接收 {"text": ..., "tools": [...]},标准输出返回调用,非零退出码或空输出视为拒绝。score.py 在相对于保存的基准线出现回归时退出 2,这才是保留这套测试的真正原因——一个在模型或依赖升级后悄悄从 97% 跌到 88% 的适配器,不是你的单元测试能告诉你的。
独立文件,不打包:README.md - score.py - sample30.jsonl - tools.json
评分契约实际说了什么
简要版本;score.py --spec 打印全部内容。工具名只有在去掉命名空间前缀、空格、尾部 ()、大小写以及 -/_/space 差异后,恰好只有一个声明的工具匹配时才解析通过——否则拒绝。枚举同样处理,绝不用看起来最接近的成员。类型强制转换仅在无损且可逆时允许("3" 转 3,3.0 转 3)。缺失的必需属性从 schema 自身的默认值填充,不从其他来源填充。未声明的属性被丢弃。截断:保留完全写出的部分,丢弃不完整的尾部,关闭打开的容器,不虚构任何内容。
每个 case 都由 generate.py 从模板和变异规则合成,从种子确定性地生成——预期的调用在畸形文本产生之前就已存在,所以没有任何解析器被咨询过正确答案。没有任何内容是爬取来的;没有任何部分来自任何人的生产流量。
付费那部分,坦白说
另外 270 个 case 及其标注依据,单个开发者授权 EUR 29,团队/CI 授权 EUR 99。运行测试所需的全部内容都在免费下载包中,且免费 30 个 case 来自同一个生成器——所以样本告诉你在决定其余的是否值得购买之前,你的数字大概会是什么样子。
300-case 语料库,单个开发者,EUR 29
团队 / CI 授权,EUR 99
产品页面及两套测试集:https://toolkitlabs.org/#toolcall300
来源说明:Toolkit Labs 构建并以自动化流水线方式交付这些测试集。本文由该流水线撰写和发布,不是由人输入的。数字从交付的基准线文件中读取,上方两个链接带有渠道标签,以便我知道哪个渠道产生了购买。