状态:草稿 (2026-10-27),实测。 测试环境为一张 8GB 级笔记本 GPU(Ada 代)、vLLM 0.31.0、Qwen3-Reranker-0.6B(针对重排微调的 Qwen3 0.6B 因果语言模型)。官方文档以 0.32.0(2026-10-18 发布)为准于 2026-10-27 打开进行了确认,与 0.31.0 文档有差异的地方已单独记录。 2026-10-27 补充。 使用对话专用的 Qwen3-0.6B、Qwen3-0.6B-GPTQ-Int8、Qwen3-4B-AWQ 以及 vLLM 0.32.0 重新测得的结果已补充至第 3.1 节。用 0.31.0 测得的前述数值保持不变。 编写规则:以实测数据为基础,未测出的标记为“未实测”,未能确认的标记为“未确认”。
目录
1. 工具调用是固定格式的文本
让我们想象一下代理(Agent)调用天气 API 的场景。模型内部并没有名为“函数调用”的特殊输出装置。模型依然是一次生成一个 token,只是在训练时学到了“调用工具时要写成这种形状”。Qwen 系列采用 Hermes 格式。
<tool_call>
{"name": "get_weather", "arguments": {"city": "Seoul", "unit": "c"}}
</tool_call>
服务器要做的事有三件:
- 将工具列表放入提示词中。 请求中的
tools会由模型的聊天模板以文本形式写入系统提示词中。 - 挑选模型写出的文本。 从生成的文本中找出上述形状并将其转换为 OpenAI 格式的
message.tool_calls的正是 工具解析器。由于不同模型的形状各异,解析器也有多种,例如hermes,llama3_json,mistral,qwen3_xml。 - 必要时强制规定形状。
tool_choice若required是
或特定的函数名,第 4 篇中的结构化输出便会在 token 层面强制规定调用的外观。
相关的启用选项如下。如果没有 --enable-auto-tool-choice ,则不接受 tool_choice="auto" 请求。
vllm serve <모델> --enable-auto-tool-choice --tool-call-parser hermes
2. 四种 tool_choice
| tool_choice | 调用义务 | 什么来保证外观 |
|---|---|---|
auto |
无,由模型决定 | 仅靠解析器。如果模型写错形状,则会作为文本残留或丢失 |
required |
一个或多个,必须调用 | 结构化输出强制规定外观 |
指定名称({"type":"function","function":{"name":...}}) |
仅限该函数 | 结构化输出强制规定外观 |
none |
不调用 | 工具定义默认保留在提示词中(可通过--exclude-tools-when-tool-choice-none 移除) |
官方文档针对指定名称的调用写道:“保证可解析的调用,但不保证高质量的调用。”这句话完美总结了本文的实测结果。
外观与参数被分别约束
从 0.21.0 开始,xgrammar 的 结构标签(structural tag) 被引入用于强制工具调用,而当前的文档将强制分成了两层。
- 外层信封(envelope):
<tool_call>:诸如标记与函数名称等内容。在required和指定名称时始终强制执行。 - 参数架构:仅当为该工具添加了
"strict": true或以--tool-strict-level parameter启动服务器时才会强制执行。否则,参数仅被约束为“看起来像 JSON”的程度。
服务端的底层默认值 --tool-strict-level 有三个: auto(默认,遵循请求)、 function(在所有请求中强制外层信封)、 parameter(将所有工具视作 strict)。由于大多数代理框架并未添加 strict ,因此这是为了让运维人员能够提高底层默认值而设计的。环境变量 VLLM_ENFORCE_STRICT_TOOL_CALLING=false 会完全关闭结构标签。
3. 亲自测试
环境与方法
- GPU:单张 8GB 级笔记本 GPU(Ada 代),驱动 595.99,CUDA 13.0 构建
- vLLM 0.31.0 官方容器,PyTorch 2.13.0,xgrammar 0.2.7,llguidance 1.7.6
- 模型:Qwen3-Reranker-0.6B(bf16)。由于是针对重排微调的模型,几乎不具备对话能力。通过聊天提问通常会回答“No.”。请将此测试理解为:不是考察“擅长使用工具的模型”,而是考察 “服务器能把不会用工具的小模型扶持到什么程度”的实验。
- 服务器:
--enable-auto-tool-choice --tool-call-parser hermes,其余为默认值 - 请求:“What is the weather in Seoul right now?”,单个工具
get_weather(city: string, unit: "c"|"f"),两个字段均为必填,temperature 0.7,max_tokens 200,随机种子 0~4 进行 5 次测试。发送相同的请求,分别在不使用 strict 以及附带strict: true和additionalProperties: false的情况下进行。 - “遵守架构”是指客户端将
arguments作为 JSON 读取,并确认city为字符串、unit为 c 或 f 的测试结果。
结果
| tool_choice | strict | 产生 tool_calls 的次数 | 遵守 Schema 的参数 | 响应时间(中位数) |
|---|---|---|---|---|
| auto | 无 | 0/5 | 0/5 | 24.5ms |
| auto | 有 | 0/5 | 0/5 | 24.6ms |
| required | 无 | 4/5 | 0/5 | 216.6ms |
| 指定名称 | 无 | 4/5 | 0/5 | 216.2ms |
| required | 有 | 5/5 | 5/5 | 263.1ms |
| 指定名称 | 有 | 5/5 | 5/5 | 261.7ms |
直接复制了几条原始数据的响应。
auto:五次的正文全都是「No.」,且都没有调用工具。auto中,如果模型决定不调用,那就这样了。required,无 strict:第一次请求在<tool_call>内像"location": "1801, 401, 181, 182, …"那样,一直输出 Schema 中没有的字段和数字序列直到 200 个 Token 结束并被截断,导致解析器未能将其转换为 tool_calls(1,180ms)。其余四次中,{}三次,{"city": "Hawaii"}一次。函数名称虽然正确,但必填字段unit一次也没有出现过。required,有 strict:{"city": "Baltic", "unit": "c"},{"city": "Berlin", "unit": "c"},{"city": "Hawaii", "unit": "f"},{"city": "none", "unit": "f"},{"city": "New York", "unit": "f"}.
在相同的种子下, required 与指定名称的输出连字符都完全相同。因为如果只有一个工具,这两种方式会生成相同的语法。
服务器也会对此进行统计。测试结束后, /metrics 的 vllm:tool_call_parser_invocations_total 在非流式聊天请求中是 outcome="tool_call" 18, outcome="no_tool_call" 12。这与客户端统计的值(产生 tool_calls 的响应 18 个,未产生的响应 12 个)一致。
阅读方法
- 无 strict 的 required 仅保证了「存在调用」。 函数名称虽然对了,但参数是个空对象,或者缺少了必填字段。0.31.0 和 0.32.0 的文档中都提到「没有 strict 的工具在 required 和指定名称时也只接受宽松的参数语法」,实际测量结果与该描述相符。
- 开启 strict 后虽然遵守了 Schema,但语义并不正确。 虽然所有参数都能被解析为 JSON 并且遵守了必填字段和枚举值,但提问中的 Seoul 一次也没有出现过,还出现了
"none"这样的值。语法仅过滤出「允许的 Token」,而在剩余的 Token 中究竟选择哪一个,依然由模型决定。 - 时间差异主要来自于 Token 数量。 strict 这一侧虽然长了大约 50ms,但其中夹杂了因为
{}完整写出两个字段而导致 Token 增加的部分,因此不能将其简单视为语法检查本身的开销。仅单独查看语法开销的数据在第四篇中。
3-1. 2026-10-27 重新测量:熟悉工具的模型与 0.32.0
前面的测试使用的是不熟悉工具的模型,观察的是「服务器能够约束的范围」。这一次我们使用训练过工具调用的模型来观察内容本身。
- 环境保持不变,仅将 vLLM 更换为 0.32.0 官方容器。模型包括用于对话的 Qwen3-0.6B (bf16)、同一模型的 GPTQ 8-bit 版本,以及 Qwen3-4B 的 AWQ 4-bit 版本。思考过程通过
chat_template_kwargs: {"enable_thinking": false}关闭。 - 请求:「What is the weather in {城市} right now? Answer in celsius.」城市共六个:Seoul、Paris、Tokyo、Cairo、Lima、Oslo。「正确」指参数 city 中包含了该城市且 unit 为 c。
| 模型 | tool_choice | strict | 产生调用的次数 | 正确的城市与单位 | 响应时间中位数 |
|---|---|---|---|---|---|
| Qwen3-0.6B | auto | 无 | 6/6 | 6/6 | 162.1ms |
| Qwen3-0.6B | required | 无 | 6/6 | 6/6 | 178.0ms |
| Qwen3-0.6B | 指定名称 | 无 | 6/6 | 6/6 | 178.1ms |
| Qwen3-0.6B | auto·required·指定名称 | 有 | 18/18 | 18/18 | 184.1~185.4ms |
| Qwen3-0.6B GPTQ 8-bit | 所有六种情况 | 无·有 | 36/36 | 36/36 | 115.7~140.6ms |
| Qwen3-4B AWQ 4-bit | 所有六种情况 | 无·有 | 36/36 | 36/36 | 338.7~362.4ms |
- 熟悉工具的模型即使在 auto 下也每次都会调用,并且在没有 strict 的情况下参数也是正确的。 这与重排模型必须开启 strict 才能勉强遵守 Schema 却依然搞错城市的情况形成了鲜明对比。
- 在 0.6B 中开启 strict 后,required 和指定名称增加了约 7ms,auto 增加了 22ms。由于 auto 在没有 strict 时完全不使用语法限制、开启后才启用,因此差异更大。由于生成的参数文本与是否有 strict 无关,因此可以将该差异视为语法掩码(Mask)的开销。
- 在相同的 0.32.0 容器中使用重排模型重新运行上述测试的结果,其响应文本甚至与 0.31.0 完全一致(无 strict 的 required 对 Schema 的遵守率为 0/5,strict 为 5/5,城市分别为 Baltic、Berlin、Hawaii、none、New York)。
由于该测试只包含一个工具和一个简短的问题,属于比较简单的情况。当拥有多个工具或参数较为复杂时,准确率仍有待实际测量。
4. 运维实践建议
- 如果是 Agent 服务器,应提高下限。 与其期待客户端加上 strict,不如通过
--tool-strict-level parameter启动,从而确保所有工具的参数都遵守 Schema。相反,如果 Schema 较深或者存在大量大型工具,则会产生构建语法(第四篇中的首次请求延迟)的开销,因此需要连同首次进入请求的延迟一同进行观察。 - 参数永远是不可信的输入。 即使是遵守 Schema 的参数也
"none"这样荒谬的值。调用工具的一方必须重新检查值的范围和权限,诸如文件路径、SQL、Shell 命令等危险参数只能通过白名单来接收。设计时应当认为,进入提示词的外部文本(网页、文档)诱导模型调用工具的提示词注入,也是出于同样的原因而无法阻止的。 - 将 auto 静默失败的情况作为指标来看待。
auto中如果模型格式错误,则不会报错,而是返回普通的正文。上面的vllm:tool_call_parser_invocations_total中no_tool_call的比例如果在仪表盘中展示,当模型、解析器或聊天模板发生变化时就能立即发现。不过,该指标无法区分“需要工具却没调用”和“不需要工具”,因此需要结合应用侧的期望来观察。 - 解析器与模型是一对。 Qwen2.5、Qwen3 使用
hermes,Qwen3-Coder 使用qwen3_xml,Llama 3.1 使用llama3_json等模型文档指定的解析器。0.32.0 中引入了读取检查点的response_template并无需特定模型解析器即可进行解释的hf解析器,但文档中记载该解析器不接收 strict、required 和名称指定(未实际测试)。
5. 未确认的事项
- 学习了工具调用的模型的准确率仅在 3-1 节中通过单个工具和简短提问进行了测量。当存在多个工具或对话较长时,则未进行实际测试。
- 一次调用多个工具的并行调用、流式传输中的 tool_calls 片段、
--tool-strict-level服务器选项未进行实际测试。 - 0.32.0 的
hf解析器和parallel_tool_calls=false强制执行未进行实际测试。
参考
- vLLM 文档,Tool Calling (0.32.0, 0.31.0): https://docs.vllm.ai/en/latest/features/tool_calling.html
- vLLM 0.21.0 发行说明(通过结构标签实现 strict 工具调用):https://github.com/vllm-project/vllm/releases/tag/v0.21.0
- Qwen 文档,Function Calling 与 vLLM: https://qwen.readthedocs.io/en/latest/framework/function_call.html
- 本系列第 4 篇 结构化输出
- 原始数据:本系列文件夹 README 的“原始数据”部分