Prhub

#45600 [Frontend] Skip structural tags for auto tool_choice without strict mode

原始 PR 作者 sfeng33 合并时间 2026-06-16 03:55 文件变更 8 提交数 5 评论 12 代码增减 +111 / -24

执行摘要

auto tool_choice 跳过结构标签除非 strict=True

防止结构标签在未明确要求严格函数调用时过度约束模型生成,与 OpenAI API 规范对齐。原始 PR #45003 引入的结构标签在 auto tool_choice 下会强制约束输出,即使客户端没有声明 strict。

值得精读,重点关注:

  • 如何通过早期短路返回实现性能优化。
  • 兼容性处理:增加新字段时确保默认行为不变。
  • 测试设计:创建 sample_tools_strict fixture 分离严格与非严格场景。
讨论亮点
  • @bbrowning 主张完全回退 #45003,认为结构标签应该仅通过环境变量 opt-in 启用。
  • @chaunceyjiang 提出对性能(遍历所有工具检查 strict)和兼容性(Claude Code 无 strict 字段)的担忧。
  • @sfeng33 回应:性能上是净收益(无 strict 时跳过 schema dump),兼容性上 strict 默认为 None,只有显式 True 才触发。
  • 最终结论:同意合并,要求显式 opt-in 解决了兼容性问题。

实现拆解

  1. 扩展协议模型:在 FunctionDefinition(vllm/entrypoints/openai/engine/protocol.py)和 AnthropicTool(vllm/entrypoints/anthropic/protocol.py)中添加 strict: bool | None = None 字段,序列化时若为 None 则忽略。
  2. 添加 _any_tool_strict 检查:在 vllm/tool_parsers/structural_tag_registry.py 中新增辅助函数,遍历工具列表,检查是否存在任意工具的 strict == True。支持 ChatCompletionToolsParamFunctionTool 两种类型。
  3. 修改 get_model_structural_tag:在返回结构标签之前增加早期返回检查:若 tool_choice == "auto"_any_tool_strict(tools) 返回 False,则直接返回 None,跳过后续的 schema 转储和标签构建。
  4. Anthropic 服务传递 strict:在 vllm/entrypoints/anthropic/serving.py 中的工具转换逻辑中将 AnthropicTool.strict 传递给 FunctionTool.strict
  5. 调整测试:更新核心测试 (test_structural_tag_registry.py) 添加 sample_tools_strict fixture,并将需要结构标签的测试用例改为使用带 strict 的工具。为 Qwen3 Coder 和 DeepSeek V4 工具解析器测试添加 _with_strict 辅助函数。
  6. 文档同步:更新 docs/features/tool_calling.md,说明 strict 字段在 tool_choice="auto" 下的作用。
文件 模块 状态 重要度
vllm/tool_parsers/structural_tag_registry.py 结构标签 modified 6.61
tests/tool_parsers/test_structural_tag_registry.py 标签测试 modified 6.41
vllm/entrypoints/openai/engine/protocol.py 协议模型 modified 5.61
tests/tool_parsers/test_qwen3coder_tool_parser.py Qwen3 测试 modified 5.59
tests/tool_parsers/test_deepseekv4_tool_parser.py DeepSeek 测试 modified 5.49
vllm/entrypoints/anthropic/protocol.py Anthropic 协议 modified 4.58
vllm/entrypoints/anthropic/serving.py Anthropic 服务 modified 4.58
docs/features/tool_calling.md 文档 modified 2.75

关键符号

_any_tool_strict get_model_structural_tag _with_strict

关键源码片段

vllm/tool_parsers/structural_tag_registry.py core-logic

核心修改:新增 _any_tool_strict 函数,并在 get_model_structural_tag 中增加 tool_choice='auto' 时的跳过逻辑。

# vllm/tool_parsers/structural_tag_registry.pydef _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 Falsedef 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 core-logic

扩展 FunctionDefinition 模型,添加 strict 字段,并确保序列化时忽略 None 值。

# vllm/entrypoints/openai/engine/protocol.pyclass 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

评论区精华

是否应该回退 #45003 设计

@bbrowning 认为完全回退 #45003 更安全,@sfeng33 坚持 #45003 是改进,且结构标签本身由 tool_choice 门控。

结论:不回退,接受本 PR 的 opt-in 方案作为改善。 · 已解决

遍历工具检查 strict 的性能开销 性能

@chaunceyjiang 担心 agent 框架发送大量工具时遍历开销,@sfeng33 解释 _any_tool_strict 短路且后续避免 schema dump,整体更优。

结论:接受性能论证。 · 已解决

Anthropic 协议是否支持 strict 字段 question

@chaunceyjiang 指出 Claude Code 的 Messages API 可能不包含 strict,@sfeng33 查证 Anthropic 文档确认支持 strict=True,vLLM 的协议之前遗漏该字段。

结论:需要添加 strict 字段以完全支持 Anthropic 协议。 · 已解决

风险与影响

  1. 兼容性风险:原依赖自动约束且未设置 strict 的客户端,在 auto 下不再获得结构标签约束。但作者声明 strict 默认 None,因此对未发送该字段的客户端无变化。
  2. 性能风险:每次请求需要遍历工具列表检查 strict,但 _any_tool_strict 短路返回且后续避免 schema dump,整体是优化。
  3. Anthropic 服务兼容性:新增字段已处理为 None 时忽略,不破坏现有请求。

用户影响:使用 tool_choice='auto' 且未设置 strict=True 的请求不再获得结构标签约束。需严格格式的用户需在工具定义中设置 strict=True
系统影响:代码库增加了对 OpenAI strict 字段的支持,提升协议兼容性。
团队影响:结构标签启用路径更清晰:auto 需要显式 opt-in,requirednamed 仍默认使用。

默认行为变更 兼容性忧虑 遍历开销隐患

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论