当智能体使用工具时,它可能在两个地方出错。它可能选错工具,也可能选对了工具但传错了参数。

这些失败告诉你不同的信息。如果智能体调用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 有单独的工具正确性和参数正确性指标,而 Phoenix 有一个单独的评估器用于工具选择。

方法 1. 无参考 LLM 评判器

无参考评判器在没有固定预期答案的情况下对工具调用进行评分。当正确性取决于上下文,或者多个选择都可能有效时,这种方法很有用,因为此时没有单一值可以在代码中与之比较。

对于工具选择,将用户的请求、智能体可用的工具以及模型的输出提供给评判器。然后让它判断所选的工具是否合适,包括模型是否本应完全避免使用工具。

考虑一个同时拥有web_search和search_internal_docs的研究智能体。可能并不存在唯一正确的选择。更好的工具可能取决于用户询问的内容以及对话中已有的信息。

同样的问题也出现在参数中。搜索查询、描述或日期范围可能符合 schema,但仍无法表达用户的意图。如果没有固定的值可以比较,评判器可以改为评估其含义。

无参考评判器仍然需要关于什么算作正确的清晰指令。在比较候选模型时,保持这些指令和评判器模型固定不变,并在将其用于整个数据集之前,用一小部分你自己审查过的案例来检查评判器的判断。

如果相等性检查、模式验证器或业务规则能可靠地回答同一个问题,就改用它们。

方法 2. 对参数进行确定性模式检查

并非每个参数错误都需要再调用一次模型。如果工具模式能证明失败,就在代码中验证它。

考虑这个工具定义。

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,
},
},
}
]

你发送给模型的同一个模式可以验证它返回的参数。它能捕获缺失的order_id,一个本应为include_items期望布尔值的位置却传入了字符串,或一个未声明的字段,例如customer_email。

方法 3. 轨迹比较

工具选择和参数检查覆盖单次调用。多步智能体也可能在它们发起的调用序列中失败。当该序列是需求的一部分时,轨迹比较会直接测试它。

一个退款工作流可能要求按顺序进行三次调用。

lookup_order
↓
verify_refund_eligibility
↓
issue_refund

严格匹配只有在顺序被要求时才有意义。LangSmith 的轨迹评估器因此支持严格、无序、子集和超集匹配。严格检查强制一种序列。其他模式接受不同的顺序,或只要求一组特定的调用。

智能体基准测试以同样的方式处理这一点。在τ²-bench中,记录的动作列表是一条参考轨迹,它被重放以推导出目标数据库的最终状态。任何能产生等价最终状态的工具调用序列都能通过数据库检查。

如果lookup_customer和lookup_subscription可以以任意顺序发生,不要因为你的参考使用了另一种顺序就让某个序列失败。改为对必需的调用或结果状态进行评分。

评估方法它检查什么最适合
无参考 LLM 评判某个工具选择或参数值在上下文中是否合适无法机械检查的决策
JSON Schema 验证JSON 结构、必需字段、类型、枚举和未声明字段对返回调用的结构验证
轨迹比较调用了哪些工具,以及在需要时以什么顺序具有已知预期路径的工作流

一个测试用例可以使用多个检查。例如,你可以比较工具名称,根据 JSON Schema 验证其参数,然后将已知参数值与预期负载进行比较。

在多个模型上运行相同的评估

一旦你定义了测试用例和评分器,就可以对每个候选模型运行相同的测试框架。

我们暴露一个工具调用接口跨受支持的模型,因此你无需为想要比较的每个模型单独进行提供商集成。

该示例将tool_choice设置为"auto"。这是你提供工具时的默认值,显式设置它可以让无工具测试更容易理解。

此示例使用 OpenAI Python SDK 与我们兼容 OpenAI 的端点。请先安装依赖项。

pipinstallopenaijsonschema

设置OPENROUTER_API_KEY在你的环境中,然后对每个候选模型运行相同的测试用例。

importjson
importos

fromjsonschemaimportDraft7Validator, ValidationError
fromopenaiimportOpenAI

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"]
fortoolintools
}

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",
]


defgrade_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_callsor[]
expected_calls=case["expected_calls"]

actual_names=[call.function.nameforcallincalls]
expected_names=[call["name"]forcallinexpected_calls]
tool_selection=actual_names==expected_names

schema_results=[]
parsed_calls=[]

forcallincalls:
name=call.function.name
schema=tool_schemas.get(name)

ifschemaisNone:
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)ifcallselseNone

argument_values=None
ifexpected_calls:
argument_values=(
schema_validisTrue
andparsed_calls==expected_calls
)

ifexpected_calls:
passed=(
tool_selection
andschema_validisTrue
andargument_valuesisTrue
)
else:
passed=tool_selection

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


formodelinmodels:
results=[
grade_case(model, case)
forcaseintest_cases
]

passed=sum(result["passed"]forresultinresults)

print(f"{model}:{passed}/{len(results)}cases passed")

forcase, resultinzip(test_cases, results):
print(f"{case['name']}:{result}")

一个正确的无工具用例会计入工具选择和整体结果,且没有 schema 或参数负载需要评分。对于返回工具的用例,测试框架会在将返回值与预期负载比较之前验证每一次调用。

测试框架将reasoning.effort设为low,对每个候选模型都如此,这样默认推理努力程度的差异就不会表现为工具调用准确率的差异。上述三个模型都在 models 端点的low对象的supported_efforts数组中列出了reasoning。它还将provider.require_parameters设为true。一个模型的supported_parameters列表可能包含只有该模型的部分提供商端点接受的参数,而在默认路由下,不支持某参数的提供商仍会收到请求并忽略它。设置require_parameters后,我们只会将请求路由到支持其中每个参数的提供商,因此每个被评分的响应都按测试框架要求的努力程度运行。关于该字段,请参见提供商路由。测试框架不设置temperature,因为openai/gpt-5.6-sol没有在temperature中列出supported_parameters。如果你列表中的每个候选模型都接受temperature,也请显式设置它。

上面的模型 ID 只是示例。models 端点中的每个条目都有一个models 端点都有一个supported_parameters数组。当该数组包含以下内容时,模型支持此测试框架:tools以及tool_choice。在将候选列表固定到长期运行的评估套件之前,请检查当前的工具调用模型集合。

要进行真正的比较,请对每个测试用例运行多次,这样模型的得分就不会基于单次响应。更大的套件还应包含你的应用所遇到的更难用例,例如缺少参数、相似的工具有描述、多次调用,以及不应使用任何工具的请求。

此测试框架评估单轮工具调用。对于多步智能体,请在整个轨迹中收集调用,并根据工作流的要求比较序列或最终状态。

提供商路由也会影响你的比较所衡量的内容。Auto Exacto默认会在每个包含工具的请求上运行,并为你所选的模型重新排序提供商,因此它可能改变由哪个提供商端点来服务工具调用请求。它的输入之一是工具调用错误率。对于每个包含工具的请求,我们会检查模型返回的每一次工具调用,并将结构性失败分类为InvalidJson、UnknownName,或SchemaMismatch,并根据你提供的arguments在parametersJSON Schema Draft 7 下进行验证。该指标衡量的是提供商行为。它不能替代你自己测试框架中的本地 schema 验证,这就是为什么上面的示例会自行验证参数。

如果你想测试你的应用在生产中将使用的路由设置,请为每个候选保持启用 Auto Exacto。如果你想进行端点级别的比较,请使用我们的提供商路由控制来固定提供商。将order字段设置为该提供商的 slug,并将provider对象中的allow_fallbacks设置为false,这样每个请求都会发往同一个端点。

在多次运行之间保持提示、工具、测试用例、评判模型和评估标准一致。显式设置采样和推理参数,例如temperature和reasoning,而不是依赖默认值,并检查每个候选是否支持你设置的参数。不要在测试框架中设置max_tokens。被截断的响应可能会截断工具调用的 JSON,并表现为与模型工具选择无关的InvalidJson失败。

如果你不想自己维护跨模型运行器,Ori Eval让你的智能体针对候选模型运行,断言它调用了哪些工具、避免了哪些工具,并用 LLM 评判器对开放式回答打分。

常见错误

如果测试用例或评分规则过于狭窄,工具调用评估可能会给出误导性的结果。

  • 只测试干净的请求。加入缺失信息、相似工具、不应调用任何工具的情况,以及模型应主动询问某个值而不是凭空编造的提示。
  • 只检查tool_calls[0]。一个响应可能包含多个调用,因此要对整个数组进行评分。
  • 把有效的载荷当作正确的载荷。模式验证无法告诉你一个有效值是否对应正确的客户、订单、日期或金额。
  • 在不同模型之间更改评估。如果工具、提示、评判器、模型设置或路由策略在多次运行之间发生变化,你就不再是在做同样的比较。