AI Agent 工具调用准确性评测:选择错误与参数错误分开测

AI Agent 工具调用准确性评测:选择错误与参数错误分开测

原文:OpenRouter Blog - 《How to Test Tool-Calling Accuracy in AI Agents》(https://openrouter.ai/blog/tutorials/how-to-test-tool-calling-accuracy-in-ai-agents/)

Agent 上线以后最常听到的一句反馈是"它有时候不调工具"。这句话没法直接拿来优化,因为"不调"背后其实是两类完全不同的失败。OpenRouter 在 2026 年 9 月 30 日发了一篇教程,把这个含糊的问题拆成了两个可测的维度,并给了一套能直接跑的评测脚手架。这篇按它的思路整理成一份可落地的测试方案。

一、先分清两类失败

Agent 用工具时只有两个地方会出错:选错工具,或者选对了工具但参数传错。

  • 工具选择错误:该调 refund_order 却调了 lookup_order;
  • 参数错误:工具选对了,但 order_id 传成了另一单。

这两类失败的修法完全不同。前者要改工具描述和工具数量,后者要改参数 schema 和示例。混在一个"准确率"里算,等于把两个 bug 平均成一个数字。原文提到,DeepEval 把工具正确性和参数正确性做成两个独立指标,Phoenix 也单独提供工具选择评估器,思路是一致的。

工具选择这一侧还有个很容易漏的用例:不需要调工具的请求同样要测。如果只检查回复里有没有工具调用,一个多调了无关工具的模型照样能通过。所以用例集里必须包含"模型已经有足够信息、应该直接回答"的样本,以及需要两步才能完成的样本------比如处理 ord_7281 的退款,正常路径是先查单再发起退款。

参数这一侧要分两步看:结构对不对,以及值对不对。结构层面要拦住的是非法 JSON、缺必填字段、类型写错、枚举值越界、多传了工具不认识的参数。值层面则是另一回事,原文举的例子很典型:一个只带 order_id 的调用,值写成 ord_7282 完全符合 schema,因为 order_id 本来就是字符串,但用户问的是 ord_7281------结构合法不等于值正确。

二、三种评测方法,按能不能机械判断来选

方法 检查什么 适合什么场景
无参考答案的 LLM 评委 在具体语境下,这个工具选择或参数值是否合理 无法机械判断、多种选择都成立的决策
JSON Schema 校验 JSON 结构、必填字段、类型、枚举、未声明字段 返回调用的结构合法性
轨迹比对 调了哪些工具、必要时是否按顺序 有已知标准路径的工作流

LLM 评委要喂三样东西:用户的请求、可用的工具列表、模型的实际输出,然后问"选这个工具合不合适",包括"是否本就不该调工具"。它适合搜索类场景,比如一个研究 Agent 同时有 web_search 和 search_internal_docs,哪个更合适取决于用户到底想问什么。用它的时候要固定两样东西:判断标准和评委模型,否则跨模型比较就没意义。原文还补了一条原则:能靠等值比较、schema 校验或业务规则判定的,就别再花一次模型调用。

Schema 校验的关键技巧是复用同一份 schema。发给模型的那份工具定义,直接拿来校验它返回的参数,不用另写一套。工具定义里要显式写 additionalProperties: false,否则多传字段不会被拦下来。

轨迹比对针对多步流程。像查单、校验退款资格、发起退款这种有强制顺序的链路,只看单次调用没用,得看整体序列。原文提到 LangSmith 的轨迹评估器支持严格、无序、子集、超集四种匹配方式;另一套基准的做法更宽松,它把参考动作列表重放一遍得到目标数据库终态,只要某个序列能推出等价终态就算通过。这个判据很实用:如果两个工具谁先谁后都行,就别因为参考轨迹用了另一种顺序而判失败。

三类检查也可以叠加在同一个用例里:比工具名、用 JSON Schema 校参数结构、再比已知参数值。

三、一份可以直接跑的评测脚本

原文给了一套跨模型评测的最小脚手架,先装两个包:

bash 复制代码
pip install openai jsonschema

主流程完整保留如下:

python 复制代码
import json, os
from jsonschema import Draft7Validator, ValidationError
from openai import OpenAI

client = OpenAI(base_url="https://openrouter.ai/api/v1",
                api_key=os.environ["OPENROUTER_API_KEY"])

tools = [{
    "type": "function",
    "function": {
        "name": "lookup_order",
        "description": "Look up an order by its ID.",
        "parameters": {
            "type": "object",
            "properties": {"order_id": {"type": "string"}},
            "required": ["order_id"],
            "additionalProperties": False,
        },
    },
}]

# 直接复用发给模型的 schema,不再另写一套
tool_schemas = {t["function"]["name"]: t["function"]["parameters"] for t in tools}

test_cases = [
    {
        "name": "known order",
        "messages": [{"role": "user", "content": "Check the status of order ord_7281."}],
        "expected_calls": [{"name": "lookup_order", "arguments": {"order_id": "ord_7281"}}],
    },
    {
        "name": "no tool needed",
        "messages": [{"role": "user", "content": "What does an order status of 'shipped' mean?"}],
        "expected_calls": [],
    },
]

def grade_case(model, case):
    response = client.chat.completions.create(
        model=model,
        messages=case["messages"],
        tools=tools,
        tool_choice="auto",
        extra_body={
            "reasoning": {"effort": "low"},
            "provider": {"require_parameters": True},
        },
    )

    calls = response.choices[0].message.tool_calls or []
    expected = case["expected_calls"]

    # 1) 工具选择:整数组比对,而不是只看第一个
    tool_selection = [c.function.name for c in calls] == [e["name"] for e in expected]

    # 2) 结构:用同一份 schema 校验参数
    schema_ok, parsed = [], []
    for call in calls:
        schema = tool_schemas.get(call.function.name)
        if schema is None:
            schema_ok.append(False)
            continue
        try:
            args = json.loads(call.function.arguments)
            Draft7Validator(schema).validate(args)
        except (json.JSONDecodeError, ValidationError):
            schema_ok.append(False)
            continue
        schema_ok.append(True)
        parsed.append({"name": call.function.name, "arguments": args})

    schema_valid = all(schema_ok) if calls else None

    # 3) 取值:结构合法之后再比对具体参数值
    values_ok = None
    if expected:
        values_ok = schema_valid is True and parsed == expected

    passed = tool_selection if not expected else (
        tool_selection and schema_valid is True and values_ok is True
    )

    return {"tool_selection": tool_selection, "schema_valid": schema_valid,
            "argument_values": values_ok, "passed": passed}

几个参数值得单独说清楚:

  • tool_choice 设为 auto,这是配了工具之后的默认行为,保持默认才能测出真实的自主选择能力;
  • extra_body 里的 reasoning.effort 统一设成 low,避免候选模型默认推理档位不同带来的干扰;
  • provider.require_parameters 设为 true,保证请求只路由到支持全部参数的供应商,否则被测的就不是你写的那份参数了;
  • 刻意不设 temperature,因为部分模型不在 supported_parameters 里声明它;同理不设 max_tokens,截断会切掉工具调用返回的 JSON,制造出假的 JSON 解析失败。

原文还提醒,每个用例都要跑多次,用例集里要补上难例:缺参数、工具描述高度相似、一次要调多个工具、以及根本不该调工具的请求。这套脚手架评估的是单轮工具调用,多步流程要在完整 trace 上收集调用再评分。

四、跨模型比较时,别把变量也一起换了

脚本最后会打印每个模型通过多少条用例,但只有在用例、评分逻辑、模型设置、路由配置四样都保持一致时,这个数字才有可比性。原文在常见错误里列了四条,都是踩出来的:

  • 只测干净请求:真实用户会缺信息、会问两个相似工具该用哪个、会问一句根本不需要工具的话,这些都必须进用例集;
  • 只看第一个工具调用:一次回复可能带多个工具调用,要整数组比对,否则漏判;
  • 把合法调用当成正确调用:schema 只管结构,值对不对------哪个客户、哪一单、什么日期、多少钱------它管不了;
  • 在不同模型之间改评测:工具、提示词、评委、设置、路由改任何一样,比较就作废。

还有一条路由细节容易被忽略:在带工具的请求上,平台默认会启用按工具调用错误率重排供应商的机制。想测真实生产链路就保持默认;想测某一个具体端点,就用 order 字段钉住供应商并关掉回退。另外,即使平台侧已经有工具调用错误率的统计,把结构失败分成非法 JSON、未知工具名、schema 不匹配三类,本地 harness 里的 schema 校验仍然要留着,因为两者测的层级不同。

五、和同系列另外两篇的配合

OpenRouter 同期还发了另外两篇教程,讲的正好是这套脚手架的前后两步。一篇是从生产流量构建 golden 评测集,建议先抽 20 到 50 条真实请求人工复核,再扩到 100 到 1000 条完整回归集,流程是抽样、去重聚类、补预期输出、首轮评估修正评分标准、提交 Git 接入 CI,核心观点是用真实流量而不是合成数据,才能保住请求的分布和失败模式。另一篇讲提示词、模型、工具定义或检索设置变更之后,重跑锁定的用例集对照书面行为契约做回归。

三篇串起来就是一条完整链路:用真实流量建集,把工具调用拆成两个维度评,每次变更后回归重跑。

回到最开始那句"它有时候不调工具",现在可以拆成三个能出数字的问题:工具选择错了多少、参数结构错了几条、参数值错了几条。数字分开之后,该改描述、该改 schema 还是该改模型,一眼就能看出来。

相关推荐
昨日之日20061 小时前
yovoice:本地配音工具箱,支持音色克隆与情绪控制,专为旁白、有声书和视频配音打造
人工智能·音视频
DP DPharness1 小时前
选型时怎么比:dsh-knowledge 与三类 RAG 方案的维度对照
人工智能·dpharness
数智顾问1 小时前
(90页PPT)IBM集团管理驾驶舱项目蓝图规划(附下载方式)
大数据·人工智能·物联网
二川bro1 小时前
Windows下Claude Code从安装到落地完整踩坑记录
人工智能
ZzT1 小时前
Claude Code Mods 是什么:给 Claude 加工具、在终端画界面
人工智能·ai编程·claude
上位机妹子1 小时前
C 语言 数组删除指定元素(快慢指针法)
c语言·数据结构·算法
心中有你02141 小时前
【路径规划】A*寻路算法最通俗易懂讲解(C语言完整实现+详细注释)
c语言·开发语言·算法
I'mChloe2 小时前
Windows部署BiliNote:Docker安装、AI视频转写、Markdown笔记与cpolar远程访问
人工智能·windows·docker
喜欢打篮球的普通人2 小时前
MiniMind 学习笔记(十二):Pretrain 实操——从版本梳理到 8GB 显卡上的真实训练
人工智能·笔记·学习