Prhub

#39217 [Mistral Grammar] Fix tool and reasoning parsing

vllm-project/vllm · 作者 juliendenize · 合并时间 2026-04-16 12:05

分析状态 已生成
文件变更 10提交数 7 · 评论 23
代码增减 +1601 / -266
bugfix tool-calling mistral frontend

执行摘要

修复 Mistral 模型在语法约束下的工具和推理解析失败问题。

PR body指出,PR #38150引入了语法注入以约束模型输出格式,但服务层仍回退到通用vLLM工具解析路径,这导致所有工具选择模式(auto、required、named、none)下的工具调用提取在流式和非流式中均失败。必须让服务层识别语法约束的Mistral输出并路由至MistralToolParser进行正确解析,同时确保tool_choice="none"时仍调用adjust_request以抑制特殊令牌泄漏。

建议精读此PR以理解Mistral语法约束解析的设计权衡,特别是全局状态问题的临时解决方案和测试覆盖的全面性。关注vllm/tool_parsers/mistral_tool_parser.py中的整合逻辑和服务层路由条件,可作为工具解析集成的参考案例。

讨论亮点
  1. 全局状态突变风险: gemini-code-assist[bot]指出在OpenAIServingChat.__init__中设置MistralToolParser.model_can_reason = True会导致全局类属性修改,存在线程安全问题,可能影响多模型服务。bbrowning建议使用实例级配置,但作者juliendenize回应技术限制,因模型推理能力不稳定且reasoning_effort参数不可靠。结论未解决,但作者接受了部分建议。
  2. 代码设计抽象: DarkLight1337评论称,未来应将整个_parse_tool_calls_from_content逻辑委托给工具解析器以避免增加条件分支,作者juliendenize同意但建议等待解析器稳定后再重构。结论为推迟改进。
  3. 细节优化: sfeng33提出小改进建议,如使用更清晰的条件变量和移动字段到请求协议中,作者部分采纳。

实现拆解

  1. 增强MistralToolParser以支持语法约束解析
    - 文件: vllm/tool_parsers/mistral_tool_parser.py
    - 关键符号: MistralStreamingResult(新增数据类)、extract_maybe_reasoning_and_tool_streaming(新增方法)、build_non_streaming_tool_calls(新增方法)
    - 变更: 添加流式推理和工具解析的整合逻辑,调整adjust_request使用adapt_inplace_to_mistral_tool适配工具定义,并设置request._grammar_from_tool_parser标志以指示语法路径。
    - 原因: 使解析器能处理Mistral语法工厂生成的约束输出,避免回退到通用解析。
    - 影响: 后续服务层可基于此标志路由解析逻辑。
  2. 重构tokenizer中的工具适配逻辑
    - 文件: vllm/tokenizers/mistral.py
    - 关键符号: _pop_unallowed_keys_and_warn(新增函数)、adapt_inplace_to_mistral_tool(新增函数)
    - 变更: 提取工具字段过滤和警告逻辑为独立函数,修复迭代时修改字典的bug(通过list(dictionary.keys())避免运行时错误)。
    - 原因: 提高代码复用性并确保工具定义兼容mistral-common格式。
    - 影响: 所有Mistral tokenizer调用将使用适配后的工具定义。
  3. 在服务层集成Mistral解析路由
    - 文件: vllm/entrypoints/openai/chat_completion/serving.pyvllm/entrypoints/openai/engine/serving.py
    - 关键符号: 无新增符号,但添加条件检查如request._grammar_from_tool_parseris_mistral_grammar_path
    - 变更: 在流式和非流式生成器中,当检测到语法路径时,调用MistralToolParser的专用方法进行解析,而非通用路径。
    - 原因: 确保服务层能正确识别和处理Mistral语法约束输出。
    - 影响: 直接修复工具调用提取失败问题,但增加服务层条件分支复杂度。
  4. 更新协议和采样参数以支持语法标志
    - 文件: vllm/sampling_params.pyvllm/entrypoints/openai/chat_completion/protocol.py
    - 关键符号: _grammar_from_tool_parser(新增字段)、移除set_include_reasoning_for_none_effort验证器。
    - 变更: 在StructuredOutputsParams中添加_from_tool_parser字段,并在请求协议中传播语法标志;移除可能导致错误行为的验证器。
    - 原因: 提供机制以跟踪语法注入来源,并避免不合理约束。
    - 影响: 确保请求处理能正确区分语法路径。
  5. 扩展测试套件以覆盖新逻辑
    - 文件: tests/tool_parsers/test_mistral_tool_parser.pytests/tool_use/mistral/test_mistral_tool_calls.pytests/tool_use/mistral/utils.py
    - 关键符号: 多个测试函数如test_extract_tool_calls_no_tools_collect_streamed_tool_call
    - 变更: 添加单元测试验证解析器行为,端到端测试覆盖各种工具使用场景(如流式、非流式、并行工具调用)。
    - 原因: 确保变更正确性并防止回归。
    - 影响: 提高代码置信度,但增加测试维护负担。
文件 模块 状态 重要度
vllm/tool_parsers/mistral_tool_parser.py 工具解析器 modified 8.75
vllm/tokenizers/mistral.py 分词器 modified 7.55
tests/tool_parsers/test_mistral_tool_parser.py 测试解析器 modified 7.24
vllm/entrypoints/openai/chat_completion/serving.py 服务入口 modified 7.3
vllm/tool_parsers/mistral_tool_parser.py core-logic

核心解析器增强,新增流式推理与工具解析整合方法,是语法路径处理的关键入口。

@dataclass
class MistralStreamingResult:
    r"""Encapsulates the mutable state returned from
    `MistralToolParser.extract_maybe_reasoning_and_tool_streaming`.
    """
    delta_message: DeltaMessage | None # 流式增量消息,可能包含工具调用或推理内容
    reasoning_ended: bool # 标志推理是否已结束
    tools_called: bool # 标志是否有工具被调用
    current_text: str # 当前累积的文本输出
    current_token_ids: list[int] # 当前累积的令牌ID列表class MistralToolParser(ToolParser):
    def extract_maybe_reasoning_and_tool_streaming(
        self,
        previous_text: str,
        current_text: str,
        delta_text: str,
        previous_token_ids: list[int],
        current_token_ids: list[int],
        delta_token_ids: list[int],
        request: ChatCompletionRequest,
    ) -> MistralStreamingResult:
        r"""解析流式输出,整合推理和工具调用提取。        当语法约束激活时(request._grammar_from_tool_parser为True),
        此方法处理Mistral格式的输出,提取可能的推理内容和工具调用。
        """
        # 实现逻辑:检查当前文本是否符合Mistral工具调用格式(如[TOOL_CALLS]前缀),
        # 使用状态机解析JSON工具定义,并同时检测推理标记。
        # 返回MistralStreamingResult实例,供服务层用于生成流式响应。
        pass
vllm/tokenizers/mistral.py core-logic

重构工具适配逻辑,提取通用函数并修复迭代修改字典的 bug,确保工具定义兼容 mistral-common。

def _pop_unallowed_keys_and_warn(
    dictionary: dict[str, Any], allowed_keys: set[str], err_dict_name: str
):
    """从字典中移除不在允许键集中的键,并记录警告。    避免在迭代时修改字典导致运行时错误,通过先复制键列表实现。
    """
    keys = list(dictionary.keys()) # 复制键列表以避免迭代时修改
    for key in keys:
        if key not in allowed_keys:
            dictionary.pop(key) # 移除不支持键
            logger.warning_once(
                f"'{key=}' is not supported by mistral-common "
                f"for {err_dict_name}. It has been popped from the "
                "object."
            )def adapt_inplace_to_mistral_tool(tool: dict[str, Any]) -> dict[str, Any]:
    """适配OpenAI工具格式为mistral-common兼容格式。    确保function工具包含必需的'parameters'和'description'字段,
    并使用_pop_unallowed_keys_and_warn过滤不支持字段。
    """
    tools_fields = set(Tool.model_fields.keys()) # 允许的工具字段集
    function_fields = set(Function.model_fields.keys()) # 允许的函数字段集
​
    if function := tool.get("function"):
        # 添加必需字段如果缺失
        if function.get("parameters") is None:
            function["parameters"] = {}
        if function.get("description") is None:
            function["description"] = ""
        # 过滤函数不支持字段
        _pop_unallowed_keys_and_warn(function, function_fields, "function")
    # 过滤工具不支持字段
    _pop_unallowed_keys_and_warn(tool, tools_fields, "tools")
    return tool
vllm/entrypoints/openai/chat_completion/serving.py dependency-wiring

服务层集成点,添加条件逻辑以路由 Mistral 语法路径至专用解析器。

# 在chat_completion_stream_generator方法中
is_mistral_grammar_path = request._grammar_from_tool_parser
if is_mistral_grammar_path or tool_choice_auto or reasoning_parser:
    # 当为Mistral语法路径时,使用专用解析逻辑
    assert tool_parser is not None
    assert isinstance(tool_parser, MistralToolParser)
    output_token_ids = as_list(output.token_ids)
    result = tool_parser.extract_maybe_reasoning_and_tool_streaming(
        previous_text=previous_text,
        current_text=current_text,
        delta_text=delta_text,
        previous_token_ids=previous_token_ids,
        current_token_ids=current_token_ids,
        delta_token_ids=delta_token_ids,
        request=request,
    )
    # 根据result更新流式响应和状态
    delta_message = result.delta_message
    reasoning_ended = result.reasoning_ended
    tools_called = result.tools_called

关键符号

MistralToolParser adapt_inplace_to_mistral_tool extract_maybe_reasoning_and_tool_streaming build_non_streaming_tool_calls _pop_unallowed_keys_and_warn

评论区精华

全局状态突变风险 设计

gemini-code-assist[bot] 指出在 OpenAIServingChat.__init__ 中设置 MistralToolParser.model_can_reason = True 会导致全局类属性修改,存在线程安全问题,可能影响多模型服务。bbrowning 建议使用实例级配置,但作者 juliendenize 回应因模型推理能力不稳定且 reasoning_effort 参数不可靠,技术限制使得当前方案必要。

结论:问题未完全解决,作者接受了风险但未重构;建议未来改进为实例级配置。 · 待处理

代码设计抽象改进 设计

DarkLight1337 评论称,未来应将整个 _parse_tool_calls_from_content 逻辑委托给工具解析器以避免增加条件分支,作者 juliendenize 同意但建议等待解析器稳定后再重构。

结论:建议被接受但推迟实施,视为未来重构方向。 · deferred

风险与影响

  1. 全局状态突变: 在vllm/entrypoints/openai/chat_completion/serving.py中修改MistralToolParser.model_can_reason类属性,可能导致多实例服务时行为不可预测,存在线程安全风险。
  2. 回归风险: 服务层新增多个Mistral特定条件分支(如检查_grammar_from_tool_parser),增加了代码复杂度,可能引入非Mistral路径的意外影响或未来修改错误。
  3. 兼容性风险: 依赖request._grammar_from_tool_parser标志,若其他模块未正确处理此标志,可能导致解析失败或行为不一致。
  4. 性能影响: 新增解析逻辑和测试覆盖可能轻微增加延迟,但主要影响可忽略。
  1. 用户影响: 使用Mistral模型和--tool-call-parser mistral的用户将恢复正确的工具调用提取功能,提升推理和工具使用体验,特别是对v11+ tokenizer和语法约束输出的支持。
  2. 系统影响: 服务层解析逻辑更细化,但通过测试确保稳定性;全局状态问题可能在未来多模型部署中引发问题,需监控。
  3. 团队影响: 代码中Mistral特定条件增多,凸显工具解析抽象缺失,为未来重构提供了明确方向;测试扩展有助于后续开发。
全局状态突变 服务层复杂度增加 向后兼容性风险

关联 Issue

未识别关联 Issue

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

完整报告

执行摘要

  • 一句话:修复Mistral模型在语法约束下的工具和推理解析失败问题。
  • 推荐动作:建议精读此PR以理解Mistral语法约束解析的设计权衡,特别是全局状态问题的临时解决方案和测试覆盖的全面性。关注vllm/tool_parsers/mistral_tool_parser.py中的整合逻辑和服务层路由条件,可作为工具解析集成的参考案例。

功能与动机

PR body指出,PR #38150引入了语法注入以约束模型输出格式,但服务层仍回退到通用vLLM工具解析路径,这导致所有工具选择模式(auto、required、named、none)下的工具调用提取在流式和非流式中均失败。必须让服务层识别语法约束的Mistral输出并路由至MistralToolParser进行正确解析,同时确保tool_choice="none"时仍调用adjust_request以抑制特殊令牌泄漏。

实现拆解

  1. 增强MistralToolParser以支持语法约束解析
    - 文件: vllm/tool_parsers/mistral_tool_parser.py
    - 关键符号: MistralStreamingResult(新增数据类)、extract_maybe_reasoning_and_tool_streaming(新增方法)、build_non_streaming_tool_calls(新增方法)
    - 变更: 添加流式推理和工具解析的整合逻辑,调整adjust_request使用adapt_inplace_to_mistral_tool适配工具定义,并设置request._grammar_from_tool_parser标志以指示语法路径。
    - 原因: 使解析器能处理Mistral语法工厂生成的约束输出,避免回退到通用解析。
    - 影响: 后续服务层可基于此标志路由解析逻辑。
  2. 重构tokenizer中的工具适配逻辑
    - 文件: vllm/tokenizers/mistral.py
    - 关键符号: _pop_unallowed_keys_and_warn(新增函数)、adapt_inplace_to_mistral_tool(新增函数)
    - 变更: 提取工具字段过滤和警告逻辑为独立函数,修复迭代时修改字典的bug(通过list(dictionary.keys())避免运行时错误)。
    - 原因: 提高代码复用性并确保工具定义兼容mistral-common格式。
    - 影响: 所有Mistral tokenizer调用将使用适配后的工具定义。
  3. 在服务层集成Mistral解析路由
    - 文件: vllm/entrypoints/openai/chat_completion/serving.pyvllm/entrypoints/openai/engine/serving.py
    - 关键符号: 无新增符号,但添加条件检查如request._grammar_from_tool_parseris_mistral_grammar_path
    - 变更: 在流式和非流式生成器中,当检测到语法路径时,调用MistralToolParser的专用方法进行解析,而非通用路径。
    - 原因: 确保服务层能正确识别和处理Mistral语法约束输出。
    - 影响: 直接修复工具调用提取失败问题,但增加服务层条件分支复杂度。
  4. 更新协议和采样参数以支持语法标志
    - 文件: vllm/sampling_params.pyvllm/entrypoints/openai/chat_completion/protocol.py
    - 关键符号: _grammar_from_tool_parser(新增字段)、移除set_include_reasoning_for_none_effort验证器。
    - 变更: 在StructuredOutputsParams中添加_from_tool_parser字段,并在请求协议中传播语法标志;移除可能导致错误行为的验证器。
    - 原因: 提供机制以跟踪语法注入来源,并避免不合理约束。
    - 影响: 确保请求处理能正确区分语法路径。
  5. 扩展测试套件以覆盖新逻辑
    - 文件: tests/tool_parsers/test_mistral_tool_parser.pytests/tool_use/mistral/test_mistral_tool_calls.pytests/tool_use/mistral/utils.py
    - 关键符号: 多个测试函数如test_extract_tool_calls_no_tools_collect_streamed_tool_call
    - 变更: 添加单元测试验证解析器行为,端到端测试覆盖各种工具使用场景(如流式、非流式、并行工具调用)。
    - 原因: 确保变更正确性并防止回归。
    - 影响: 提高代码置信度,但增加测试维护负担。

关键文件:

  • vllm/tool_parsers/mistral_tool_parser.py(模块 工具解析器;类别 source;类型 core-logic;符号 MistralStreamingResult, MistralToolParser, extract_maybe_reasoning_and_tool_streaming, build_non_streaming_tool_calls): 核心解析器增强,新增流式推理与工具解析整合方法,是语法路径处理的关键入口。
  • vllm/tokenizers/mistral.py(模块 分词器;类别 source;类型 core-logic;符号 _pop_unallowed_keys_and_warn, adapt_inplace_to_mistral_tool): 重构工具适配逻辑,提取通用函数并修复迭代修改字典的bug,确保工具定义兼容mistral-common。
  • tests/tool_parsers/test_mistral_tool_parser.py(模块 测试解析器;类别 test;类型 test-coverage;符号 test_extract_tool_calls_no_tools, test_extract_tool_calls_pre_v11_multiple_bot_tokens_raises, test_extract_tool_calls_streaming_one_chunk): 扩展单元测试覆盖解析器新功能,验证语法路径下的各种工具调用场景。
  • vllm/entrypoints/openai/chat_completion/serving.py(模块 服务入口;类别 source;类型 dependency-wiring): 服务层集成点,添加条件逻辑以路由Mistral语法路径至专用解析器。

关键符号:MistralToolParser, adapt_inplace_to_mistral_tool, extract_maybe_reasoning_and_tool_streaming, build_non_streaming_tool_calls, _pop_unallowed_keys_and_warn

关键源码片段

vllm/tool_parsers/mistral_tool_parser.py

核心解析器增强,新增流式推理与工具解析整合方法,是语法路径处理的关键入口。

@dataclass
class MistralStreamingResult:
    r"""Encapsulates the mutable state returned from
    `MistralToolParser.extract_maybe_reasoning_and_tool_streaming`.
    """
    delta_message: DeltaMessage | None # 流式增量消息,可能包含工具调用或推理内容
    reasoning_ended: bool # 标志推理是否已结束
    tools_called: bool # 标志是否有工具被调用
    current_text: str # 当前累积的文本输出
    current_token_ids: list[int] # 当前累积的令牌ID列表class MistralToolParser(ToolParser):
    def extract_maybe_reasoning_and_tool_streaming(
        self,
        previous_text: str,
        current_text: str,
        delta_text: str,
        previous_token_ids: list[int],
        current_token_ids: list[int],
        delta_token_ids: list[int],
        request: ChatCompletionRequest,
    ) -> MistralStreamingResult:
        r"""解析流式输出,整合推理和工具调用提取。        当语法约束激活时(request._grammar_from_tool_parser为True),
        此方法处理Mistral格式的输出,提取可能的推理内容和工具调用。
        """
        # 实现逻辑:检查当前文本是否符合Mistral工具调用格式(如[TOOL_CALLS]前缀),
        # 使用状态机解析JSON工具定义,并同时检测推理标记。
        # 返回MistralStreamingResult实例,供服务层用于生成流式响应。
        pass

vllm/tokenizers/mistral.py

重构工具适配逻辑,提取通用函数并修复迭代修改字典的bug,确保工具定义兼容mistral-common。

def _pop_unallowed_keys_and_warn(
    dictionary: dict[str, Any], allowed_keys: set[str], err_dict_name: str
):
    """从字典中移除不在允许键集中的键,并记录警告。    避免在迭代时修改字典导致运行时错误,通过先复制键列表实现。
    """
    keys = list(dictionary.keys()) # 复制键列表以避免迭代时修改
    for key in keys:
        if key not in allowed_keys:
            dictionary.pop(key) # 移除不支持键
            logger.warning_once(
                f"'{key=}' is not supported by mistral-common "
                f"for {err_dict_name}. It has been popped from the "
                "object."
            )def adapt_inplace_to_mistral_tool(tool: dict[str, Any]) -> dict[str, Any]:
    """适配OpenAI工具格式为mistral-common兼容格式。    确保function工具包含必需的'parameters'和'description'字段,
    并使用_pop_unallowed_keys_and_warn过滤不支持字段。
    """
    tools_fields = set(Tool.model_fields.keys()) # 允许的工具字段集
    function_fields = set(Function.model_fields.keys()) # 允许的函数字段集
​
    if function := tool.get("function"):
        # 添加必需字段如果缺失
        if function.get("parameters") is None:
            function["parameters"] = {}
        if function.get("description") is None:
            function["description"] = ""
        # 过滤函数不支持字段
        _pop_unallowed_keys_and_warn(function, function_fields, "function")
    # 过滤工具不支持字段
    _pop_unallowed_keys_and_warn(tool, tools_fields, "tools")
    return tool

vllm/entrypoints/openai/chat_completion/serving.py

服务层集成点,添加条件逻辑以路由Mistral语法路径至专用解析器。

# 在chat_completion_stream_generator方法中
is_mistral_grammar_path = request._grammar_from_tool_parser
if is_mistral_grammar_path or tool_choice_auto or reasoning_parser:
    # 当为Mistral语法路径时,使用专用解析逻辑
    assert tool_parser is not None
    assert isinstance(tool_parser, MistralToolParser)
    output_token_ids = as_list(output.token_ids)
    result = tool_parser.extract_maybe_reasoning_and_tool_streaming(
        previous_text=previous_text,
        current_text=current_text,
        delta_text=delta_text,
        previous_token_ids=previous_token_ids,
        current_token_ids=current_token_ids,
        delta_token_ids=delta_token_ids,
        request=request,
    )
    # 根据result更新流式响应和状态
    delta_message = result.delta_message
    reasoning_ended = result.reasoning_ended
    tools_called = result.tools_called

评论区精华

  1. 全局状态突变风险: gemini-code-assist[bot]指出在OpenAIServingChat.__init__中设置MistralToolParser.model_can_reason = True会导致全局类属性修改,存在线程安全问题,可能影响多模型服务。bbrowning建议使用实例级配置,但作者juliendenize回应技术限制,因模型推理能力不稳定且reasoning_effort参数不可靠。结论未解决,但作者接受了部分建议。
  2. 代码设计抽象: DarkLight1337评论称,未来应将整个_parse_tool_calls_from_content逻辑委托给工具解析器以避免增加条件分支,作者juliendenize同意但建议等待解析器稳定后再重构。结论为推迟改进。
  3. 细节优化: sfeng33提出小改进建议,如使用更清晰的条件变量和移动字段到请求协议中,作者部分采纳。
  • 全局状态突变风险 (design): 问题未完全解决,作者接受了风险但未重构;建议未来改进为实例级配置。
  • 代码设计抽象改进 (design): 建议被接受但推迟实施,视为未来重构方向。

风险与影响

  • 风险:1. 全局状态突变: 在vllm/entrypoints/openai/chat_completion/serving.py中修改MistralToolParser.model_can_reason类属性,可能导致多实例服务时行为不可预测,存在线程安全风险。
    2. 回归风险: 服务层新增多个Mistral特定条件分支(如检查_grammar_from_tool_parser),增加了代码复杂度,可能引入非Mistral路径的意外影响或未来修改错误。
    3. 兼容性风险: 依赖request._grammar_from_tool_parser标志,若其他模块未正确处理此标志,可能导致解析失败或行为不一致。
    4. 性能影响: 新增解析逻辑和测试覆盖可能轻微增加延迟,但主要影响可忽略。
  • 影响:1. 用户影响: 使用Mistral模型和--tool-call-parser mistral的用户将恢复正确的工具调用提取功能,提升推理和工具使用体验,特别是对v11+ tokenizer和语法约束输出的支持。
    2. 系统影响: 服务层解析逻辑更细化,但通过测试确保稳定性;全局状态问题可能在未来多模型部署中引发问题,需监控。
    3. 团队影响: 代码中Mistral特定条件增多,凸显工具解析抽象缺失,为未来重构提供了明确方向;测试扩展有助于后续开发。
  • 风险标记:全局状态突变, 服务层复杂度增加, 向后兼容性风险

关联脉络

  • PR #38150 [Mistral Grammar] Introduce grammar-based tool-call enforcement (假设标题,基于上下文推断): 引入了语法注入但未处理服务层解析,是本PR的直接前驱,导致解析失败问题。
  • PR #37081 First attempt to improve tool and reasoning parsing (假设标题,基于PR body提及): 第一次尝试改进工具解析,本PR是其第二次尝试,旨在完全修复问题。

参与讨论