OpenRouter 教程:如何测试 AI Agent 的工具调用准确性
How to Test Tool-Calling Accuracy in AI Agents
OpenRouter 发布教程,讲解如何测试 AI Agent 的工具调用准确性,将失败拆分为工具选择错误和参数错误两类分别测试。
原文把工具调用失败拆成选错工具和传错参数两类,给出三种评测方法及跨模型测试代码,方法可直接迁移到自己的 Agent 评测。
当智能体使用工具时,它可能在两个地方出错。它可能选错工具,也可能选对了工具却传错了参数。
这些失败会告诉你不同的信息。如果智能体调用了 lookup_order 而不是 refund_order,问题出在工具选择上。如果它调用 refund_order 时传错了 order_id,那说明它选对了工具,但传错了参数。
本指南涵盖三种测试工具调用行为的方法。第一种是无参考的大语言模型(LLM)评判器。第二种是确定性的参数检查。第三种是轨迹比较。随后会展示如何通过 OpenRouter 对多个具备工具能力的模型运行相同的测试用例。
简而言之
- 分别测试工具选择和参数正确性。智能体可能选错工具、不必要地调用工具,或者选对了工具却传错了参数。
- 当你知道预期的工具或参数值时,使用确定性检查。当正确性取决于上下文,或者多个选择都可能有效时,使用无参考的 LLM 评判器。
- 使用 JSON Schema 来捕获格式错误或结构无效的参数,并单独检查参数值。符合 schema 的值仍然可能是错的。
- 当工具调用的顺序很重要时,使用轨迹比较。如果多条路径都能达到相同的有效结果,就不要要求某个确切的顺序。
- 当你比较模型时,对每个候选模型保持测试用例、评分规则、模型设置和路由配置一致。
两种需要不同测试的失败模式
先从工具决策本身入手,然后检查模型生成的调用。
工具选择
假设一个内部支持智能体可以使用 lookup_order、issue_refund 和 search_docs。
如果用户询问退款政策是怎么规定的,search_docs 是合适的工具。如果他们要求为订单 ord_7281 退款,智能体可能需要先查询订单,然后再发起退款。你的测试集还应包含模型已有足够信息可以回答、不应调用工具的情况。
不调用工具的情况很重要,因为仅检查响应是否包含 tool_calls 是不够的。一个调用了不必要或错误函数的模型仍然会产生工具调用。
当明确预期某个工具时,在代码中将返回的工具名称与预期的工具名称进行比较。如果多个工具都能合理地解决该请求,精确匹配可能会拒绝一个有效的选择。这时 LLM 评判器就更有用。
参数正确性
在模型选择工具后,检查它生成的参数。这项检查包含两部分:结构和值。
结构验证可捕获格式错误的 JSON、缺少必填字段、类型不正确、无效的枚举值以及工具不接受的参数。
结构上有效的调用仍然可能包含错误的值。
{
"order_id": "ord_7282"
}如果 order_id 被定义为字符串,该负载满足 schema。但如果用户询问的是 ord_7281,它仍然是错的。
现有的评估框架也做了同样的区分。DeepEval 有单独的 Tool Correctness 和 Argument Correctness 指标,Phoenix 则有一个单独的 tool selection 评估器。
方法 1. 无参考的 LLM 评判器
无参考评判器在没有固定预期答案的情况下对工具调用进行评分。当正确性取决于上下文,或者多个选择都可能有效、因而没有单一值可以在代码中与之比较时,这种方法很有用。
对于工具选择,把用户的请求、agent 可用的工具以及模型的输出交给评判器。然后让它判断所选工具是否合适,包括模型是否本就不该使用任何工具。
设想一个同时拥有 web_search 和 search_internal_docs 的研究 agent。可能并不存在唯一正确的选择。更好的工具取决于用户问了什么,以及对话中已经有哪些信息。
同样的问题也出现在参数上。一个搜索查询、描述或日期范围可能符合 schema,却仍然无法表达用户的真实意图。如果没有固定的值可供比对,评判器可以改为评估其含义。
无参考评判器仍然需要明确的指令来界定什么算正确。在比较候选模型时,保持这些指令和评判模型固定不变,并在将其用于整个数据集之前,先用一小批你自己审查过的案例来检查评判器的判断。
如果等值检查、schema 校验器或业务规则能够可靠地回答同一个问题,就改用它们。
方法 2. 对参数进行确定性 schema 检查
并非每个参数错误都需要再调用一次模型。如果工具 schema 能够证明该失败,就用代码来校验。
看这个工具定义。
tools = [
{
"type": "function",
"function": {
"name": "lookup_order",
"description": "Look up an order by its ID.",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"include_items": {"type": "boolean"},
},
"required": ["order_id"],
"additionalProperties": False,
},
},
}
]你发送给模型的同一份 schema 也可以用来校验它返回的参数。它能捕获缺失的 order_id、本应是 include_items 所期望的布尔值却传了字符串,或诸如 customer_email 这样的未声明字段。
方法 3. 轨迹比较
工具选择和参数检查覆盖的是单次调用。多步 agent 也可能在调用序列上出错。当这个序列本身就是需求的一部分时,轨迹比较可以直接测试它。
一个退款工作流可能要求按顺序进行三次调用。
lookup_order
↓
verify_refund_eligibility
↓
issue_refund严格匹配只在顺序有要求时才有意义。LangSmith 的 轨迹评估器 因此支持严格、无序、子集和超集匹配。严格检查强制要求一种序列。其他模式接受不同的顺序,或只要求出现某一组特定的调用。
Agent 基准测试也采用同样的方式。在 τ²-bench 中,记录的动作列表是一条参考轨迹,通过重放它来推导出目标数据库的最终状态。任何能产生等价最终状态的工具调用序列都能通过数据库检查。
如果 lookup_customer 和 lookup_subscription 可以以任意顺序发生,就不要因为你的参考用了其中一种顺序而判定另一种顺序失败。改为对必需的调用或最终状态进行评分。
| 评估方法 | 检查内容 | 最适用场景 |
|---|---|---|
| 无参考 LLM 评判器 | 某个工具选择或参数值在上下文中是否合适 | 无法机械检查的决策 |
| JSON Schema 校验 | JSON 结构、必填字段、类型、枚举和未声明字段 | 对返回的调用进行结构校验 |
| 轨迹比较 | 调用了哪些工具,以及在需要时以什么顺序调用 | 具有已知预期路径的工作流 |
一个测试用例可以使用不止一种检查。例如,你可以比较工具名称,用 JSON Schema 校验其参数,然后将已知的参数值与预期 payload 进行比较。
在多个模型上运行同一套评估
一旦定义了测试用例和评分器,你就可以对每个候选模型运行同一套测试框架。
我们在所支持的模型上暴露同一个 工具调用接口,因此你无需为想要比较的每个模型单独做提供商集成。
示例将 tool_choice 设置为 "auto"。这是你提供工具时的默认值,显式设置它可以让无工具测试更容易理解。
此示例使用 OpenAI Python SDK 和我们兼容 OpenAI 的端点。请先安装依赖项。
pip install openai jsonschema在你的环境中设置 OPENROUTER_API_KEY,然后对每个候选模型运行相同的测试用例。
import json
import 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,
},
},
}
]
tool_schemas = {
tool["function"]["name"]: tool["function"]["parameters"]
for tool 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": [],
},
]
models = [
"anthropic/claude-opus-5",
"openai/gpt-5.6-sol",
"moonshotai/kimi-k3",
]
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_calls = case["expected_calls"]
actual_names = [call.function.name for call in calls]
expected_names = [call["name"] for call in expected_calls]
tool_selection = actual_names == expected_names
schema_results = []
parsed_calls = []
for call in calls:
name = call.function.name
schema = tool_schemas.get(name)
if schema is None:
schema_results.append(False)
continue
try:
arguments = json.loads(call.function.arguments)
Draft7Validator(schema).validate(arguments)
except (json.JSONDecodeError, ValidationError):
schema_results.append(False)
continue
schema_results.append(True)
parsed_calls.append(
{
"name": name,
"arguments": arguments,
}
)
schema_valid = all(schema_results) if calls else None
argument_values = None
if expected_calls:
argument_values = (
schema_valid is True
and parsed_calls == expected_calls
)
if expected_calls:
passed = (
tool_selection
and schema_valid is True
and argument_values is True
)
else:
passed = tool_selection
return {
"tool_selection": tool_selection,
"schema_valid": schema_valid,
"argument_values": argument_values,
"passed": passed,
}
for model in models:
results = [
grade_case(model, case)
for case in test_cases
]
passed = sum(result["passed"] for result in results)
print(f"{model}: {passed}/{len(results)} cases passed")
for case, result in zip(test_cases, results):
print(f" {case['name']}: {result}")正确的无工具用例会计入工具选择和总体结果,并且没有需要评分的 schema 或参数负载。对于返回工具的用例,测试框架会在将返回的值与预期负载进行比较之前验证每一次调用。
测试框架为每个候选模型将 reasoning.effort 设置为 low,这样默认推理强度的差异就不会表现为工具调用准确率的差异。上述三个模型都在 models 端点的 reasoning 对象的 supported_efforts 数组中列出了 low。它还将 provider.require_parameters 设置为 true。一个模型的 supported_parameters 列表可能包含只有该模型的部分提供商端点接受的参数,而在默认路由下,不支持某个参数的提供商仍会收到请求并忽略它。设置 require_parameters 后,我们只会将请求路由到支持其中每个参数的提供商,因此每个被评分的响应都以测试框架要求的强度运行。该字段请参见 提供商路由。测试框架不设置 temperature,因为 openai/gpt-5.6-sol 没有在 supported_parameters 中列出 temperature。如果你列表中的每个候选模型都接受 temperature,也请显式设置它。
上面的模型 ID 只是示例。models 端点中的每个条目都有一个 supported_parameters 数组。当该数组包含 tools 和 tool_choice 时,模型就支持此测试框架。在长期存在的评估套件中固定候选列表之前,请查看当前的 工具调用模型集合。
要进行真正的比较,请对每个测试用例运行多次,这样模型的得分就不会基于单次响应。更大的套件还应包含你的应用遇到的更难用例,例如缺少参数、相似的工具有描述、多次调用,以及不应使用任何工具的请求。
此测试框架评估单次工具调用轮次。对于多步智能体,请在整个 trace 中收集调用,并根据工作流的要求比较序列或最终状态。
提供商路由也会影响你的比较所衡量的内容。Auto Exacto 默认对每个包含工具的请求运行,并为你选择的模型重新排序提供商,因此它可能会改变由哪个提供商端点来服务工具调用请求。它的输入之一是工具调用错误率。对于每个包含工具的请求,我们会检查模型返回的每一次工具调用,并将结构性失败分类为 InvalidJson、UnknownName 或 SchemaMismatch,同时根据你在 JSON Schema Draft 7 下提供的 parameters schema 验证 arguments。该指标衡量的是提供商行为。它不能替代你自己测试框架中的本地 schema 验证,这就是上面的示例自行验证参数的原因。
如果你想测试你的应用在生产中会使用的路由设置,请为每个候选模型保持启用 Auto Exacto。如果你想进行端点级别的比较,请使用我们的 提供商路由控制来固定提供商。将 provider 对象中的 order 字段设置为该提供商的 slug,并将 allow_fallbacks 设置为 false,这样每个请求都会发往同一个端点。
在多次运行之间保持提示词、工具、测试用例、评判模型和评估标准一致。显式设置采样和推理参数,例如 temperature 和 reasoning,而不是依赖默认值,并检查每个候选模型是否支持你设置的参数。不要在测试框架中设置 max_tokens。被截断的响应可能会切断工具调用的 JSON,并表现为与模型工具选择无关的 InvalidJson 失败。
如果你不想自己维护跨模型运行器,Ori Eval 可以让你的 agent 针对候选模型运行,断言它调用了哪些工具、避免了哪些工具,并用 LLM 评判器对开放式回答进行评分。
常见错误
如果测试用例或评分规则过于狭窄,工具调用评估可能会给出误导性的结果。
- 只测试干净的请求。要包含缺失信息、相似工具、不应调用任何工具的情况,以及模型应询问某个值而不是自行编造值的提示词。
- 只检查
tool_calls[0]。一个响应可能包含多个调用,因此要对整个数组进行评分。 - 把有效的载荷当作正确的载荷。Schema 验证无法告诉你一个有效值是否对应正确的客户、订单、日期或金额。
- 在不同模型之间更改评估。如果工具、提示词、评判器、模型设置或路由策略在多次运行之间发生变化,你就不再是在进行同样的比较。
来源:OpenRouter:Announcements(RSS) · openrouter.ai