Prhub

#44330 [Bugfix] GPT-OSS instruction rendering

原始 PR 作者 yzong-rh 合并时间 2026-06-06 01:52 文件变更 9 提交数 2 评论 2 代码增减 +275 / -111

执行摘要

修复 GPT-OSS 指令渲染与 HF 模板不一致

根据 issue #33210,GPT-OSS 的指令渲染与 HF 的 apply_chat_template 不一致:vLLM 总是插入一个默认的 developer 消息,且用户的 system/developer 消息作为单独的消息放在后面。本 PR 修复此问题,将第一个 system/developer 指令消息折叠到 preamble 的 developer 块中,并确保在无指令无工具时跳过 developer 块。

  • 值得精读:特别是 build_harmony_preamble 的设计和如何通过 extract_instructions_from_messages 抽象消息预处理逻辑。
  • 关注测试:四个 case 的渲染对比示例是验证行为的好材料。
  • 留意环境变量VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS 的行为需在部署时确认。
讨论亮点

reviewer bbrowning 在简化 review 中表示“同意与官方 chat template 对齐,因为模型供应商的 apply_chat_template 是事实标准”,并感谢 PR 描述中的 side-by-side 对比帮助理解变更影响。没有其他争议讨论。

实现拆解

  1. 新增 extract_instructions_from_messages 函数(位于 harmony_utils.py):从消息列表中分离出首条 system/developer 消息的文本内容,并返回剩余消息列表。

  2. 新增 build_harmony_preamble 函数(位于 harmony_utils.py):构建包含 system 消息和 developer 消息的 preamble,将指令、工具描述、浏览器/代码解释器等内置工具描述合并到 developer 块中。

  3. 改造 Chat Completions 入口render/serving.py_make_request_with_harmony):原逻辑固定创建 system 消息 + 可选的 developer 消息(仅当有工具时)。现改为调用 extract_instructions_from_messages 并传入 build_harmony_preamble,使首条 system/developer 指令自然融入 preamble。

  4. 改造 Responses API 入口responses/serving.py_construct_input_messages_with_harmony):类似地,先尝试从 request.instructions 获取指令,否则从 request.input 中提取首条指令消息,再调用 build_harmony_preamble。原冗长的 _construct_harmony_system_input_message 被拆分为更轻量的 _get_harmony_builtin_tool_descriptions

  5. 统一消息转换逻辑responses/harmony.py_parse_chat_format_messageresponse_input_to_harmony):将 system/developer 消息统一转换为带有 DeveloperContent 的消息,通过 get_system_or_developer_message 根据环境变量 VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS 决定使用 system 还是 developer 角色。

  6. 配套测试:新增 test_system_message_without_toolstest_system_message_with_tools 验证 Chat Completions 路径;更新 test_response_input_to_harmony.py 中 developer 消息的预期输出;添加 test_developer_message 到渲染测试中。

文件 模块 状态 重要度
vllm/entrypoints/openai/parser/harmony_utils.py 入口解析 modified 8.58
vllm/entrypoints/openai/responses/serving.py 入口层 modified 8.17
vllm/entrypoints/openai/responses/harmony.py 入口层 modified 6.44
vllm/entrypoints/serve/render/serving.py 入口层 modified 6.44
tests/entrypoints/openai/chat_completion/test_serving_chat.py 对话服务 modified 6.35
tests/entrypoints/openai/responses/test_response_input_to_harmony.py 响应解析 modified 5.99
tests/entrypoints/openai/parser/test_harmony_render_parity.py 测试对齐 modified 5.61
tests/entrypoints/openai/parser/test_harmony_utils.py 测试工具 modified 4.59
tests/entrypoints/openai/utils.py 测试公用 modified 3.7

关键符号

get_system_or_developer_message flatten_input_text_content extract_instructions_from_messages build_harmony_preamble _construct_input_messages_with_harmony _make_request_with_harmony _parse_chat_format_message response_input_to_harmony

关键源码片段

vllm/entrypoints/openai/parser/harmony_utils.py dependency-wiring

核心 utils,新增 extract_instructions_from_messages、build_harmony_preamble 等关键函数,重构消息预处理逻辑

# harmony_utils.py — 新增的核心函数def extract_instructions_from_messages(
    messages: Sequence[Any],
) -> tuple[str | None, list[Any]]:
    """
    从消息列表中分离出首条 system/developer 消息的文本内容。
    返回(指令文本,剩余消息列表)。如果首条不是 system/developer,返回 (None, messages)。
    """
    remaining_messages = list(messages)
    if not remaining_messages:
        return None, remaining_messages
​
    first_message = remaining_messages[0]
    if not isinstance(first_message, dict):
        if hasattr(first_message, "to_dict"):
            first_message = first_message.to_dict()
        elif hasattr(first_message, "model_dump"):
            first_message = first_message.model_dump(exclude_none=True)
        else:
            raise ValueError(f"Unknown message type: {type(first_message)}")
​
    if first_message.get("role") not in ("system", "developer"):
        return None, remaining_messages
​
    instructions = flatten_input_text_content(first_message.get("content"))
    return instructions, remaining_messages[1:]
​
​
def build_harmony_preamble(
    *,
    instructions: str | None = None,
    tools: list[Tool | ChatCompletionToolsParam] | None = None,
    reasoning_effort: str | None = None,
    browser_description: str | None = None,
    python_description: str | None = None,
    container_description: str | None = None,
    with_custom_tools: bool = False,
) -> list[Message]:
    """
    构建 Harmony 请求的 preamble:system 消息 + 可选的 developer 消息(包含指令和工具描述)。
    如果既没有 instructions 也没有 tools,且无内置工具,则只返回 system 消息。
    """
    sys_msg = get_system_message(
        reasoning_effort=reasoning_effort,
        browser_description=browser_description,
        python_description=python_description,
        container_description=container_description,
        with_custom_tools=with_custom_tools,
    )
    # 只有当有指令或工具时才添加 developer 消息
    if instructions or tools or browser_description or python_description or container_description:
        dev_msg = get_developer_message(
            instructions=instructions,
            tools=tools,
        )
        return [sys_msg, dev_msg]
    return [sys_msg]
vllm/entrypoints/openai/responses/serving.py core-logic

Responses API 入口重构,提取指令和工具描述逻辑,使用新的 build_harmony_preamble

# responses/serving.py — _construct_input_messages_with_harmony 关键重构部分def _construct_input_messages_with_harmony(
    self,
    request: ResponsesRequest,
    prev_response: ResponsesResponse | None,
) -> list[OpenAIHarmonyMessage]:
    messages: list[OpenAIHarmonyMessage] = []
    request_input = request.input
​
    if prev_response is None:
        # 新的对话:提取指令,构建 preamble
        tool_types = extract_tool_types(request.tools)
        with_custom_tools = has_custom_tools(tool_types)
​
        instructions = request.instructions
        if instructions is None and isinstance(request_input, list):
            # 从 input 中提取首条 system/developer 消息作为指令
            instructions, request_input = extract_instructions_from_messages(request_input)
​
        tool_descriptions = self._get_harmony_builtin_tool_descriptions(request, tool_types)
​
        # 构建 Harmony preamble(system + developer 消息)
        preamble = build_harmony_preamble(
            instructions=instructions,
            tools=request.tools if with_custom_tools else None,
            browser_description=tool_descriptions["browser_description"],
            python_description=tool_descriptions["python_description"],
            container_description=tool_descriptions["container_description"],
            with_custom_tools=with_custom_tools,
        )
        messages.extend(preamble)
​
        # 转换剩余的输入消息(已去除首条指令消息)
        messages.extend(response_input_to_harmony_list(request_input))
    else:
        # 已有对话:直接转换所有消息
        messages = response_input_to_harmony_list(request_input)
        messages = construct_harmony_previous_input_messages(prev_response, messages)
    return messages

评论区精华

Review 批准和对齐 HF 的讨论 other

reviewer bbrowning 快速审核后批准,强调与官方 chat template 对齐是正确的方向,并感谢 PR 描述中的对比示例。

结论:无争议,直接合并。 · 已解决

风险与影响

  • 渲染行为变化:现有依赖原始渲染格式的客户端可能受到影响,但新行为更接近 HF 预期,属于修复性变更。
  • 环境变量依赖VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS 控制是否使用 system 角色,如果用户依赖该变量但未更新部署,可能导致意外行为。测试已覆盖两种模式。
  • 工具折叠逻辑:工具描述与指令合并到同一 developer 块,若工具列表为空则无 developer 块,可能改变已有工具调用的输出格式。但该逻辑与 HF 保持一致。
  • Responses API 路径_construct_input_messages_with_harmony 重构后,request.instructions 优先级高于从 input 提取,可能影响使用指令的客户端。但这是明确的设计选择,文档已更新。
  • 用户影响:所有使用 GPT-OSS 模型的 Chat Completions 和 Responses API 用户。生成的消息序列改变,但对齐 HF 后模型输出质量有望提升。
  • 系统影响:无性能影响;改动集中在入口解析层,不涉及核心引擎。
  • 团队影响:维护者需注意未来新增模型时遵循此模式。
渲染逻辑变更 环境变量依赖 工具折叠逻辑 Responses API 重构

关联 Issue

#33210 [Bug]: gpt-oss chat format mismatch with HF apply_chat_template

完整报告

参与讨论