执行摘要
- 一句话:传递 reasoning parser kwargs 至结构化输出引擎
- 推荐动作:该 PR 值得精读,特别是
_get_reasoner 方法和 request-scoped 设计的引入过程。讨论中 chaunceyjiang 对 DeepSeek 与 Qwen3 设计差异的分析具有参考价值。建议关注 gemini-code-assist 指出的类型注解问题,并在后续提交中修复。
功能与动机
修复 #41132 和 #33215:当启用 thinking 模式且使用 structured output 时,DeepSeek 模型的工具调用内容被错误地放入 reasoning 字段,原因是引擎侧未获取前端使用的 chat_template_kwargs(如 enable_thinking)。
实现拆解
- 数据结构扩展:在
StructuredOutputRequest (vllm/v1/structured_output/request.py) 中新增 reasoning_parser_kwargs 和 reasoner 字段,用于存储每个请求的 kwargs 和缓存的 reasoning parser 实例。
- Manager 重构:在
StructuredOutputManager.__init__ 中,将原本直接实例化的 self.reasoner 改为仅存储 reasoner 类 self.reasoner_cls,不再 manager 级实例化。
- 按需获取 reasoning parser:新增
_get_reasoner 方法,在每次请求调用 should_fill_bitmask 和 should_advance 时,按需实例化(带 kwargs 缓存),替代直接引用 self.reasoner。
- 引擎传播:在
AsyncLLM.add_request 和 generate 方法中增加 reasoning_parser_kwargs 参数,通过请求对象传递至 StructuredOutputRequest。对 streaming input 分支增加非空检查,暂不实现。
- 前端入口:在
chat_completion/serving.py 和 responses/serving.py 中,从 chat_template_kwargs 构造 reasoning_parser_kwargs 并传递给引擎。
- 测试适配:更新
test_reasoning_structured_output.py,引入 MockReasoner 和 manager_with_reasoner fixture,新增 test_should_fill_bitmask_uses_request_reasoning_parser_kwargs 验证 per-request kwargs 生效。
关键文件:
vllm/v1/structured_output/__init__.py(模块 结构化输出;类别 source;类型 core-logic;符号 _get_reasoner): 核心变更文件:修改 Manager 初始化,不再直接实例化 reasoner,改为存储类;新增 _get_reasoner 方法实现请求级实例化;修改 should_fill_bitmask 和 should_advance 使用 _get_reasoner 替代原来的 self.reasoner 直接引用。
vllm/v1/structured_output/request.py(模块 请求模型;类别 source;类型 dependency-wiring): 定义 StructuredOutputRequest 数据结构,新增 reasoning_parser_kwargs 字段和 reasoner 字段,为 per-request kwargs 存储提供基础。
vllm/v1/engine/async_llm.py(模块 异步引擎;类别 source;类型 core-logic): 引擎核心:add_request 和 generate 方法新增 reasoning_parser_kwargs 参数,在请求对象上赋值,传递至 StructuredOutputRequest;在 streaming input 分支增加非空检查。
vllm/entrypoints/openai/chat_completion/serving.py(模块 聊天补全;类别 source;类型 core-logic): 聊天补全入口:从 chat_template_kwargs 构造 reasoning_parser_kwargs 并传递给引擎的 generate 方法。
vllm/entrypoints/openai/responses/serving.py(模块 响应 API;类别 source;类型 core-logic): 响应 API 入口:类似 chat_completion,从 chat_template_kwargs 构造 reasoning_parser_kwargs 并传递,同时修改了 _generate_with_builtin_tools 签名。
vllm/v1/request.py(模块 请求模型;类别 source;类型 core-logic): 请求模型:在 Request 类中添加 reasoning_parser_kwargs 字段。
vllm/engine/protocol.py(模块 引擎协议;类别 source;类型 core-logic): 引擎协议:在 LLMEngine 抽象类中增加 reasoning_parser_kwargs 参数。
vllm/v1/engine/__init__.py(模块 引擎核心;类别 source;类型 core-logic): 导出变更:重新导出 reasoning_parser_kwargs 相关符号(若有)。
tests/v1/structured_output/test_reasoning_structured_output.py(模块 测试;类别 test;类型 test-coverage;符号 MockReasoner, init, mock_reasoning_parser, manager_with_reasoner): 测试文件:重构测试,引入 MockReasoner 和 manager_with_reasoner 夹具,新增 test_should_fill_bitmask_uses_request_reasoning_parser_kwargs 测试,验证 per-request kwargs 生效。
关键符号: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
核心变更文件:修改 Manager 初始化,不再直接实例化 reasoner,改为存储类;新增 _get_reasoner 方法实现请求级实例化;修改 should_fill_bitmask 和 should_advance 使用 _get_reasoner 替代原来的 self.reasoner 直接引用。
from vllm.reasoning import ReasoningParserManager
class 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
定义 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
测试文件:重构测试,引入 MockReasoner 和 manager_with_reasoner 夹具,新增 test_should_fill_bitmask_uses_request_reasoning_parser_kwargs 测试,验证 per-request kwargs 生效。
from unittest.mock import Mock
import pytest
class 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 manager
def 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
评论区精华
BugenZhao 在评论中指出,当前设计将前端请求参数泄漏到核心引擎并不理想,但修复 bug 优先,未来可重构。chaunceyjiang 认为 DeepSeek 系列的推理解析器设计有缺陷(根据请求切换解析器),而 Qwen3 系列无需切换,但鉴于 DeepSeek V4 的流行度,先合并此修补。gemini-code-assist 在 review 中指出 type[ReasoningParser] 类型注解在运行时会导致 NameError,因为 ReasoningParser 仅在 TYPE_CHECKING 块中导入,但 PR 最终合并时未采纳该建议,可能成为隐藏风险。
- 设计决策:request-scoped vs manager-scoped reasoning parser (design): 同意采用 request-scoped 设计作为修复,后续会考虑重构。
- 类型注解 NameError 风险 (correctness): 未采纳,PR 合并后该问题仍存在。
- DeepSeek vs Qwen3 设计差异 (design): 认可此 PR 作为临时修复。
风险与影响
- 风险:
- 类型注解风险:
vllm/v1/structured_output/__init__.py 第 43 行 self.reasoner_cls: type[ReasoningParser] | None = None 在运行时因 ReasoningParser 未导入而引发 NameError,需使用字符串前向引用或添加 from __future__ import annotations。
- streaming input 兼容性:
async_llm.py 中增加 reasoning_parser_kwargs 非空检查,当 prompt 为 AsyncGenerator 且 reasoning_parser_kwargs 非空时抛 NotImplementedError,可能影响正在使用 streaming input 与 reasoning 的用户。
- 核心路径变更:修改了
StructuredOutputManager.should_fill_bitmask 和 should_advance,可能影响所有使用 reasoning parser 的模型(如 Qwen3)。
- 测试覆盖不全:新增测试仅覆盖基础路径,未涵盖 streaming input、多卡并行等场景。
- 影响:用户:修复 DeepSeek V3.2/V4 用户在 thinking 模式下使用 structured output 的 bug。系统:额外传递少量参数,无性能回退。团队:后续需要重构 reasoning parser 设计以彻底解决架构问题。
- 风险标记:核心路径变更, 类型注解运行时错误, streaming input 未支持, 测试覆盖不完整
关联脉络
- PR #41178 Fa/fix json schema + tool calls: 该 PR 的替代修复方案,尝试通过修改 DeepSeek V3 reasoning parser 实现,但本 PR 更通用且避免 hacking parser。
- PR #33215 [Bug]: DeepSeek V3.2
tool_choice==required in thinking mode gives internal server error.: 本 PR 修复的原始 issue 之一,描述 tool_choice=required 时 internal server error。
- PR #41132 [Bug]: DeepSeek V3.2 & V4 incorrect structured output when thinking enabled: 本 PR 修复的主要 issue,描述 structured output 在 thinking 模式下输出错误。
参与讨论