执行摘要
- 一句话:auto tool_choice 跳过结构标签除非 strict=True
- 推荐动作:值得精读,重点关注:
- 如何通过早期短路返回实现性能优化。
- 兼容性处理:增加新字段时确保默认行为不变。
- 测试设计:创建
sample_tools_strict fixture 分离严格与非严格场景。
功能与动机
防止结构标签在未明确要求严格函数调用时过度约束模型生成,与 OpenAI API 规范对齐。原始 PR #45003 引入的结构标签在 auto tool_choice 下会强制约束输出,即使客户端没有声明 strict。
实现拆解
- 扩展协议模型:在
FunctionDefinition(vllm/entrypoints/openai/engine/protocol.py)和 AnthropicTool(vllm/entrypoints/anthropic/protocol.py)中添加 strict: bool | None = None 字段,序列化时若为 None 则忽略。
- 添加
_any_tool_strict 检查:在 vllm/tool_parsers/structural_tag_registry.py 中新增辅助函数,遍历工具列表,检查是否存在任意工具的 strict == True。支持 ChatCompletionToolsParam 和 FunctionTool 两种类型。
- 修改
get_model_structural_tag:在返回结构标签之前增加早期返回检查:若 tool_choice == "auto" 且 _any_tool_strict(tools) 返回 False,则直接返回 None,跳过后续的 schema 转储和标签构建。
- Anthropic 服务传递 strict:在
vllm/entrypoints/anthropic/serving.py 中的工具转换逻辑中将 AnthropicTool.strict 传递给 FunctionTool.strict。
- 调整测试:更新核心测试 (
test_structural_tag_registry.py) 添加 sample_tools_strict fixture,并将需要结构标签的测试用例改为使用带 strict 的工具。为 Qwen3 Coder 和 DeepSeek V4 工具解析器测试添加 _with_strict 辅助函数。
- 文档同步:更新
docs/features/tool_calling.md,说明 strict 字段在 tool_choice="auto" 下的作用。
关键文件:
vllm/tool_parsers/structural_tag_registry.py(模块 结构标签;类别 source;类型 core-logic;符号 _any_tool_strict): 核心修改:新增 _any_tool_strict 函数,并在 get_model_structural_tag 中增加 tool_choice='auto' 时的跳过逻辑。
tests/tool_parsers/test_structural_tag_registry.py(模块 标签测试;类别 test;类型 test-coverage;符号 sample_tools_strict, test_auto_tool_choice_skips_structural_tag_without_strict): 核心测试:新增 sample_tools_strict fixture,并更新多个测试用例使用 strict 工具,确保新逻辑覆盖。
vllm/entrypoints/openai/engine/protocol.py(模块 协议模型;类别 source;类型 core-logic): 扩展 FunctionDefinition 模型,添加 strict 字段,并确保序列化时忽略 None 值。
tests/tool_parsers/test_qwen3coder_tool_parser.py(模块 Qwen3测试;类别 test;类型 test-coverage;符号 _with_strict): 为 Qwen3 Coder 测试添加 _with_strict 辅助函数,确保结构标签测试在 strict 场景下通过。
tests/tool_parsers/test_deepseekv4_tool_parser.py(模块 DeepSeek测试;类别 test;类型 test-coverage;符号 _with_strict): 为 DeepSeek V4 测试添加 _with_strict 辅助函数,结构标签相关测试适配 strict 要求。
vllm/entrypoints/anthropic/protocol.py(模块 Anthropic协议;类别 source;类型 core-logic): AnthropicTool 模型添加 strict 字段,支持 Anthropic 协议的 strict 工具定义。
vllm/entrypoints/anthropic/serving.py(模块 Anthropic服务;类别 source;类型 core-logic): 将 AnthropicTool.strict 传递给 FunctionTool.strict,确保 Anthropic 请求中的 strict 设置被正确处理。
docs/features/tool_calling.md(模块 文档;类别 docs;类型 documentation): 更新文档以反映新行为,指导用户如何使用 strict 字段。
关键符号:_any_tool_strict, get_model_structural_tag, _with_strict
关键源码片段
vllm/tool_parsers/structural_tag_registry.py
核心修改:新增 _any_tool_strict 函数,并在 get_model_structural_tag 中增加 tool_choice='auto' 时的跳过逻辑。
# vllm/tool_parsers/structural_tag_registry.py
def _any_tool_strict(
tools: Sequence[ChatCompletionToolsParam | ResponsesTool],
) -> bool:
"""检查工具列表中是否有任意一个工具设置了 strict=True。"""
for tool in tools:
# 处理 Responses API 的 FunctionTool 类型
if isinstance(tool, FunctionTool) and tool.strict is True:
return True
# 处理 Chat Completions API 的 ChatCompletionToolsParam 类型
if isinstance(tool, ChatCompletionToolsParam) and tool.function.strict is True:
return True
return False
def get_model_structural_tag(
model: str,
tools: Sequence[ChatCompletionToolsParam | ResponsesTool] | None,
tool_choice: ToolChoice,
reasoning: bool,
) -> StructuralTag | None:
"""Build a structural tag with xgrammar's builtin model templates."""
if not tools or tool_choice == "none":
return None
# 关键新增:当 tool_choice 为 "auto" 且没有任何工具声明 strict=True 时,
# 跳过结构标签,避免过度约束模型生成。
if tool_choice == "auto" and not _any_tool_strict(tools):
return None
dumped_tools = [_dump_tool_for_xgrammar(tool) for tool in tools]
dumped_tool_choice = _dump_tool_choice_for_xgrammar(tool_choice)
# 后续保持不变 ...
vllm/entrypoints/openai/engine/protocol.py
扩展 FunctionDefinition 模型,添加 strict 字段,并确保序列化时忽略 None 值。
# vllm/entrypoints/openai/engine/protocol.py
class FunctionDefinition(OpenAIBaseModel):
name: str
description: str | None = None
parameters: dict[str, Any] | None = None
strict: bool | None = None # 新增字段,匹配 OpenAI API 规范
defer_loading: bool | None = None
@model_serializer(mode="wrap")
def _serialize(self, handler):
data = handler(self)
# 序列化时如果 strict 为 None 则移除,避免发送不必要的字段
if self.strict is None:
data.pop("strict", None)
if self.defer_loading is None:
data.pop("defer_loading", None)
return data
评论区精华
风险与影响
关联脉络
- PR #45003 Implement structural tag for tool calling (assumed): 本 PR 直接修改了 #45003 引入的 get_model_structural_tag 行为,增加了 strict 门控。
- PR #44965 [Bugfix] Reject out-of-range temperature values in SamplingParams: 虽然不直接相关,但同为近期前端修复,体现工具调用参数的规范化趋势。
参与讨论