Prhub

#43590 [Frontend][Responses API] Fold developer-role input messages into system instructions

原始 PR 作者 chaunceyjiang 合并时间 2026-06-03 22:52 文件变更 2 提交数 5 评论 10 代码增减 +391 / -1

执行摘要

将 Developer 角色消息转换为 System 以适配 Responses API

引用 issue #42407 和 #42475,用户使用 Codex CLI 集成 vLLM 时遇到 Unexpected message role 错误,因为 OpenAI Responses API 的 developer 角色在 vLLM 的 chat template 中未得到处理。PR 旨在兼容 Responses API 的 developer 消息,使得 Codex 等客户端能够正常工作。

建议合并。该 PR 解决了实际用户问题,设计上选择在渲染器瓶颈层统一处理,而非散落在 API 入口,是一个合理的架构决策。讨论中对 system 合并的权衡也值得关注。

讨论亮点

主要讨论点:

  • sfeng33 建议将逻辑从 responses/utils.py 移到 safe_apply_chat_template,因为后者是所有 HF 模板处理的共享瓶颈,这样同时修复 Chat Completion API 的类似问题。该建议被采纳。

  • cjackal 指出合并多个 system 消息可能不符合某些模型(如 Qwen 3.6)的预期,但 bbrowning 回应测试表明不合并反而会导致更差的效果,且合并只在模板不支持 developer 时才触发,因此作为安全默认可接受。

  • gemini-code-assist[bot] 对最初实现提出代码质量问题,但最终实现已改为在 hf.py 中处理,相关评论已过时。

  • DarkLight1337 指出 PR 标题曾误用 "Move",作者已更正。

实现拆解

实现分为以下步骤:

  1. 检测 chat template 对 developer 的原生支持:新增 _detect_developer_role_support 函数,通过简单的字符串匹配判断模板中是否包含 "developer"'developer'。该函数带有 @lru_cache,避免重复解析。

  2. 将 developer 消息转换为 system 消息:新增 _convert_developer_to_system 函数,遍历对话列表,将 roledeveloper 的消息改为 system,并移除 tools 键(因为 OpenAI system 消息不允许包含 tools)。该函数不修改原列表,返回新列表。

  3. 合并多个 system 消息到首位:新增 _consolidate_system_messages 函数,收集所有 system 消息的文本内容,合并为一个新的 system 消息放在列表开头,其余非 system 消息保持顺序。仅当有多个 system 消息或 system 不在首位时执行合并。

  4. 在 safe_apply_chat_template 中集成:在 safe_apply_chat_template 中,检查对话中是否有 developer 角色,且模板不支持 developer,则依次执行转换和合并,并打印一条 info_once 日志。此位置是所有 HF 模板处理的瓶颈,同时覆盖 Chat Completion API 和 Responses API。

  5. 测试:在 tests/renderers/test_hf.py 中添加了 TestConvertDeveloperToSystem(测试角色转换、tools 移除、不变性)和 TestDetectDeveloperRoleSupport(测试检测功能在 ChatML 模板和含 developer 模板上的表现)。

文件 模块 状态 重要度
vllm/renderers/hf.py 渲染器 modified 8.18
tests/renderers/test_hf.py 测试 modified 7.52

关键符号

_detect_developer_role_support _convert_developer_to_system _consolidate_system_messages safe_apply_chat_template

关键源码片段

vllm/renderers/hf.py core-logic

核心变更文件,添加了 developer 角色检测、转换和 system 合并逻辑,并集成到 safe_apply_chat_template 中,是所有 HF 模板处理的瓶颈。

@lru_cache(maxsize=32)
def _detect_developer_role_support(chat_template: str) -> bool:
    # 通过检查模板字符串中是否包含 "developer" 或 'developer'
    # 来判断模板是否原生支持 developer 角色
    return '"developer"' in chat_template or "'developer'" in chat_template
​
​
def _convert_developer_to_system(
    conversation: list[ConversationMessage],
) -> list[ConversationMessage]:
    """将 developer 消息转换为 system 消息,并移除 tools 键。"""
    converted: list[ConversationMessage] = []
    for msg in conversation:
        if msg["role"] == "developer":
            new_msg = dict(msg) # 创建副本,避免修改原始数据
            new_msg["role"] = "system"
            new_msg.pop("tools", None) # system 消息不允许包含 tools
            converted.append(new_msg) # type: ignore[arg-type]
        else:
            converted.append(msg)
    return converted
​
​
def _consolidate_system_messages(
    conversation: list[ConversationMessage],
) -> list[ConversationMessage]:
    """合并多个 system 消息为一个,放在列表开头。    有些 chat template(如 Qwen 3.6)要求 system 消息必须在首位。
    在 developer 转 system 后可能会出现 system 不在首位的情况,
    此函数将它们合并为一个消息。
    """
    system_contents: list[str] = []
    non_system: list[ConversationMessage] = []
    needs_consolidation = False
    for i, msg in enumerate(conversation):
        if msg["role"] == "system":
            if i > 0 or system_contents:
                needs_consolidation = True
            content = msg.get("content", "")
            # 处理 content 为列表的情况(多部分内容)
            if isinstance(content, list):
                parts = []
                for part in content:
                    if isinstance(part, dict) and "text" in part:
                        parts.append(part["text"])
                    elif isinstance(part, str):
                        parts.append(part)
                content = "\n".join(parts)
            if content:
                system_contents.append(content)
        else:
            non_system.append(msg)
​
    if not needs_consolidation:
        return conversation
​
    merged: ConversationMessage = {
        "role": "system",
        "content": "\n\n".join(system_contents),
    }
    return [merged, *non_system]
​
​
# 在 safe_apply_chat_template 中的调用(位于函数末尾,resolve_template 之后):
if any(
    msg["role"] == "developer" for msg in conversation
) and not _detect_developer_role_support(chat_template):
    conversation = _convert_developer_to_system(conversation)
    conversation = _consolidate_system_messages(conversation)
    logger.info_once(
        "Chat template does not support the 'developer' message role. "
        "Converting developer messages to 'system' role.",
    )
tests/renderers/test_hf.py test-coverage

添加了完整的单元测试,覆盖 developer 转换和检测功能,确保正常路径和边界情况。

class TestConvertDeveloperToSystem:
    # 测试角色转换:developer 变为 system
    def test_converts_role(self):
        conversation = [
            {"role": "developer", "content": "You are helpful."},
            {"role": "user", "content": "Hello"},
        ]
        result = _convert_developer_to_system(conversation)
        assert result[0]["role"] == "system"
        assert result[0]["content"] == "You are helpful."
        assert result[1]["role"] == "user"
​
    # 测试转换时会移除 tools 键(OpenAI system 不允许 tools)
    def test_removes_tools_key(self):
        conversation = [
            {
                "role": "developer",
                "content": "Instructions",
                "tools": [{"type": "function"}],
            },
        ]
        result = _convert_developer_to_system(conversation)
        assert "tools" not in result[0]
​
    # 测试没有 developer 消息时列表不变
    def test_no_developer_messages_unchanged(self):
        conversation = [
            {"role": "system", "content": "System prompt"},
            {"role": "user", "content": "Hello"},
        ]
        result = _convert_developer_to_system(conversation)
        assert result == conversation
​
    # 测试不修改原始传入列表
    def test_does_not_mutate_original(self):
        original = {
            "role": "developer",
            "content": "Instructions",
            "tools": [{"type": "function"}],
        }
        conversation = [original]
        _convert_developer_to_system(conversation)
        assert original["role"] == "developer"
        assert "tools" in original

评论区精华

逻辑应该放在 safe_apply_chat_template 共享瓶颈处 设计

sfeng33 建议将 developer 转换逻辑从 responses/utils.py 移到 safe_apply_chat_template,因为后者是所有 HF 模板处理的共享入口,同时修复 Chat Completion API 的类似问题。其他 reviewer 同意并协助完成迁移。

结论:逻辑最终被实现在 safe_apply_chat_template 中,而不是 responses/utils.py。 · 已解决

合并 system messages 可能影响某些模型 设计

cjackal 指出合并多个 system 消息可能不符合某些模型的训练预期,例如 Qwen 3.6。bbrowning 回应称不合并反而会导致更差的 agentic 行为,且合并仅在模板不支持 developer 时触发,作为安全默认可接受。

结论:当前合并策略被保留,未来可考虑通过配置控制是否合并。 · 已解决

PR 标题误用 "Move" other

DarkLight1337 指出 PR 标题称 "move developer-to-system conversion into HF renderer",但实际并未删除原始位置。作者承认并修正了标题。

结论:标题已更正为准确描述。 · 已解决

风险与影响

  1. 核心路径变更safe_apply_chat_template 是 Chat 和 Responses API 的必经之路,修改可能影响所有请求。但逻辑只在不支持 developer 的模板上触发,且检测有 LRU 缓存,性能开销小。
  2. system 消息合并:合并多个 system 消息可能改变模型行为,尤其是那些明确支持多条 system 的模型。但 PR 仅在模板不支持 developer 时合并(即模板可能是旧模板),且实际测试中合并反而提升效果。
  3. 兼容性:非 HF 渲染器(如 DeepSeek、Mistral、Harmony)有自己的 render_messages 实现,不受此变更影响。
  4. 回归风险:如果 chat template 字符串包含非角色引用(如变量名 "developer"),检测函数可能误报。但这种情况罕见,且误报只导致不必要的日志,不破坏功能。

用户:使用 OpenAI Responses API 的客户端(如 Codex CLI)不再因 developer 角色而报错。同时修复了 Chat Completion API 中可能出现的角色问题。
系统:所有 HF 渲染的请求都会通过新增的检查,但 LRU 缓存确保性能影响极小。
团队:代码集中在 vllm/renderers/hf.py,易于维护。新增的测试覆盖了主要逻辑,降低了回归风险。

核心路径变更 system 消息合并 兼容性检测缓存

关联 Issue

#42407 [Doc]: which version of vllm supports codex ?
#42475 [Doc]: Integration Issue: Qwen3.6-27B-FP8 with Codex via vLLM Server

完整报告

参与讨论