Prhub

#41199 [Bugfix] Pass reasoning parser kwargs to structured output

原始 PR 作者 BugenZhao 合并时间 2026-05-01 14:38 文件变更 9 提交数 6 评论 9 代码增减 +132 / -56

执行摘要

传递 reasoning parser kwargs 至结构化输出引擎

修复 #41132 和 #33215:当启用 thinking 模式且使用 structured output 时,DeepSeek 模型的工具调用内容被错误地放入 reasoning 字段,原因是引擎侧未获取前端使用的 chat_template_kwargs(如 enable_thinking)。

该 PR 值得精读,特别是 _get_reasoner 方法和 request-scoped 设计的引入过程。讨论中 chaunceyjiang 对 DeepSeek 与 Qwen3 设计差异的分析具有参考价值。建议关注 gemini-code-assist 指出的类型注解问题,并在后续提交中修复。

讨论亮点

BugenZhao 在评论中指出,当前设计将前端请求参数泄漏到核心引擎并不理想,但修复 bug 优先,未来可重构。chaunceyjiang 认为 DeepSeek 系列的推理解析器设计有缺陷(根据请求切换解析器),而 Qwen3 系列无需切换,但鉴于 DeepSeek V4 的流行度,先合并此修补。gemini-code-assist 在 review 中指出 type[ReasoningParser] 类型注解在运行时会导致 NameError,因为 ReasoningParser 仅在 TYPE_CHECKING 块中导入,但 PR 最终合并时未采纳该建议,可能成为隐藏风险。

实现拆解

  1. 数据结构扩展:在 StructuredOutputRequest (vllm/v1/structured_output/request.py) 中新增 reasoning_parser_kwargsreasoner 字段,用于存储每个请求的 kwargs 和缓存的 reasoning parser 实例。
  2. Manager 重构:在 StructuredOutputManager.__init__ 中,将原本直接实例化的 self.reasoner 改为仅存储 reasoner 类 self.reasoner_cls,不再 manager 级实例化。
  3. 按需获取 reasoning parser:新增 _get_reasoner 方法,在每次请求调用 should_fill_bitmaskshould_advance 时,按需实例化(带 kwargs 缓存),替代直接引用 self.reasoner
  4. 引擎传播:在 AsyncLLM.add_requestgenerate 方法中增加 reasoning_parser_kwargs 参数,通过请求对象传递至 StructuredOutputRequest。对 streaming input 分支增加非空检查,暂不实现。
  5. 前端入口:在 chat_completion/serving.pyresponses/serving.py 中,从 chat_template_kwargs 构造 reasoning_parser_kwargs 并传递给引擎。
  6. 测试适配:更新 test_reasoning_structured_output.py,引入 MockReasonermanager_with_reasoner fixture,新增 test_should_fill_bitmask_uses_request_reasoning_parser_kwargs 验证 per-request kwargs 生效。
文件 模块 状态 重要度
vllm/v1/structured_output/__init__.py 结构化输出 modified 7.47
vllm/v1/structured_output/request.py 请求模型 modified 6.34
vllm/v1/engine/async_llm.py 异步引擎 modified 6.3
vllm/entrypoints/openai/chat_completion/serving.py 聊天补全 modified 5.7
vllm/entrypoints/openai/responses/serving.py 响应 API modified 5.94
vllm/v1/request.py 请求模型 modified 5.13
vllm/engine/protocol.py 引擎协议 modified 4.56
vllm/v1/engine/__init__.py 引擎核心 modified 4.56
tests/v1/structured_output/test_reasoning_structured_output.py 测试 modified 7.26

关键符号

StructuredOutputManager._get_reasoner StructuredOutputManager.__init__ StructuredOutputManager.should_fill_bitmask StructuredOutputManager.should_advance AsyncLLM.add_request AsyncLLM.generate LLMEngine.add_request openai_chat_completion.create_chat_completion openai_responses.create_responses

关键源码片段

vllm/v1/structured_output/__init__.py core-logic

核心变更文件:修改 Manager 初始化,不再直接实例化 reasoner,改为存储类;新增 `_get_reasoner` 方法实现请求级实例化;修改 `should_fill_bitmask` 和 `should_advance` 使用 `_get_reasoner` 替代原来的 `self.reasoner` 直接引用。

from vllm.reasoning import ReasoningParserManagerclass StructuredOutputManager:
    def __init__(self, vllm_config: VllmConfig):
        self.backend: StructuredOutputBackend | None = None
        # 仅存储 reasoner 类,实例化延迟到请求级
        self.reasoner_cls: type[ReasoningParser] | None = None # FIXME: 运行时 NameError(见 risk_analysis)
        self.vllm_config = vllm_config
        # ...
        reasoning_parser = self.vllm_config.structured_outputs_config.reasoning_parser
        if reasoning_parser:
            # 仅获取类,不实例化
            self.reasoner_cls = ReasoningParserManager.get_reasoning_parser(
                reasoning_parser
            )
​
    def _get_reasoner(self, request: "Request") -> "ReasoningParser | None":
        """
        延迟创建每个请求的 reasoning parser,并使用请求级别的 kwargs。
        """
        structured_req = request.structured_output_request
        if structured_req is None or self.reasoner_cls is None:
            return None
        if structured_req.reasoner is None:
            parser_kwargs = structured_req.reasoning_parser_kwargs or {}
            # 使用传入的 kwargs 实例化(例如 chat_template_kwargs)
            structured_req.reasoner = self.reasoner_cls(
                tokenizer=self.tokenizer,
                **parser_kwargs,
            )
        return structured_req.reasoner
vllm/v1/structured_output/request.py dependency-wiring

定义 `StructuredOutputRequest` 数据结构,新增 `reasoning_parser_kwargs` 字段和 `reasoner` 字段,为 per-request kwargs 存储提供基础。

@dataclasses.dataclass
class StructuredOutputRequest:
    params: StructuredOutputsParams
    _grammar: Future[StructuredOutputGrammar] | StructuredOutputGrammar | None = None
    reasoning_ended: bool | None = None
    reasoning_parser_kwargs: dict[str, Any] | None = None # 请求级 kwargs
    reasoner: "ReasoningParser | None" = None # 缓存的 per-request 解析器实例
tests/v1/structured_output/test_reasoning_structured_output.py test-coverage

测试文件:重构测试,引入 `MockReasoner` 和 `manager_with_reasoner` 夹具,新增 `test_should_fill_bitmask_uses_request_reasoning_parser_kwargs` 测试,验证 per-request kwargs 生效。

from unittest.mock import Mock
import pytestclass MockReasoner:
    def __init__(self, tokenizer):
        self.is_reasoning_end = Mock(return_value=False)
        self.is_reasoning_end_streaming = Mock(return_value=False)@pytest.fixture
def manager_with_reasoner(self, mock_vllm_config):
    manager = StructuredOutputManager(mock_vllm_config)
    manager.reasoner_cls = MockReasoner # 注入 mock 类
    manager.tokenizer = Mock()
    return managerdef test_should_fill_bitmask_uses_request_reasoning_parser_kwargs(
    self, manager_with_reasoner, mock_request_with_structured_output
):
    mock_request_with_structured_output.structured_output_request.reasoning_parser_kwargs = {"chat_template_kwargs": {"thinking": True}}
    result = manager_with_reasoner.should_fill_bitmask(
        mock_request_with_structured_output
    )
    # 确保 kwargs 被正确传递并影响结果
    assert result is False

评论区精华

设计决策:request-scoped vs manager-scoped reasoning parser 设计

BugenZhao 指出将前端参数泄漏到核心引擎不理想,但为了修复 bug 可暂时接受;chaunceyjiang 认为 DeepSeek 系列设计有缺陷,Qwen3 无需切换 parser。

结论:同意采用 request-scoped 设计作为修复,后续会考虑重构。 · 已解决

类型注解 NameError 风险 正确性

gemini-code-assist 指出 `type[ReasoningParser]` 在运行时因 `ReasoningParser` 仅在 `TYPE_CHECKING` 块导入而引发 NameError,建议使用字符串前向引用。

结论:未采纳,PR 合并后该问题仍存在。 · unresolved

DeepSeek vs Qwen3 设计差异 设计

chaunceyjiang 解释 DeepSeek 的 reasoning parser 根据请求切换,而 Qwen3 无需切换,当前 PR 弥补了 DeepSeek 的缺陷。

结论:认可此 PR 作为临时修复。 · 已解决

风险与影响

  1. 类型注解风险vllm/v1/structured_output/__init__.py 第 43 行 self.reasoner_cls: type[ReasoningParser] | None = None 在运行时因 ReasoningParser 未导入而引发 NameError,需使用字符串前向引用或添加 from __future__ import annotations
  2. streaming input 兼容性async_llm.py 中增加 reasoning_parser_kwargs 非空检查,当 promptAsyncGeneratorreasoning_parser_kwargs 非空时抛 NotImplementedError,可能影响正在使用 streaming input 与 reasoning 的用户。
  3. 核心路径变更:修改了 StructuredOutputManager.should_fill_bitmaskshould_advance,可能影响所有使用 reasoning parser 的模型(如 Qwen3)。
  4. 测试覆盖不全:新增测试仅覆盖基础路径,未涵盖 streaming input、多卡并行等场景。

用户:修复 DeepSeek V3.2/V4 用户在 thinking 模式下使用 structured output 的 bug。系统:额外传递少量参数,无性能回退。团队:后续需要重构 reasoning parser 设计以彻底解决架构问题。

核心路径变更 类型注解运行时错误 streaming input 未支持 测试覆盖不完整

关联 Issue

#33215 [Bug]: DeepSeek V3.2 `tool_choice==required` in thinking mode gives internal server error.
#41132 [Bug]: DeepSeek V3.2 & V4 incorrect structured output when thinking enabled
#41178 Fa/fix json schema + tool calls

完整报告

参与讨论