执行摘要
- 一句话:将 Developer 角色消息转换为 System 以适配 Responses API
- 推荐动作:建议合并。该 PR 解决了实际用户问题,设计上选择在渲染器瓶颈层统一处理,而非散落在 API 入口,是一个合理的架构决策。讨论中对 system 合并的权衡也值得关注。
功能与动机
引用 issue #42407 和 #42475,用户使用 Codex CLI 集成 vLLM 时遇到 Unexpected message role 错误,因为 OpenAI Responses API 的 developer 角色在 vLLM 的 chat template 中未得到处理。PR 旨在兼容 Responses API 的 developer 消息,使得 Codex 等客户端能够正常工作。
实现拆解
实现分为以下步骤:
-
检测 chat template 对 developer 的原生支持:新增 _detect_developer_role_support 函数,通过简单的字符串匹配判断模板中是否包含 "developer" 或 'developer'。该函数带有 @lru_cache,避免重复解析。
-
将 developer 消息转换为 system 消息:新增 _convert_developer_to_system 函数,遍历对话列表,将 role 为 developer 的消息改为 system,并移除 tools 键(因为 OpenAI system 消息不允许包含 tools)。该函数不修改原列表,返回新列表。
-
合并多个 system 消息到首位:新增 _consolidate_system_messages 函数,收集所有 system 消息的文本内容,合并为一个新的 system 消息放在列表开头,其余非 system 消息保持顺序。仅当有多个 system 消息或 system 不在首位时执行合并。
-
在 safe_apply_chat_template 中集成:在 safe_apply_chat_template 中,检查对话中是否有 developer 角色,且模板不支持 developer,则依次执行转换和合并,并打印一条 info_once 日志。此位置是所有 HF 模板处理的瓶颈,同时覆盖 Chat Completion API 和 Responses API。
-
测试:在 tests/renderers/test_hf.py 中添加了 TestConvertDeveloperToSystem(测试角色转换、tools 移除、不变性)和 TestDetectDeveloperRoleSupport(测试检测功能在 ChatML 模板和含 developer 模板上的表现)。
关键文件:
vllm/renderers/hf.py(模块 渲染器;类别 source;类型 core-logic;符号 _detect_developer_role_support, _convert_developer_to_system, _consolidate_system_messages, safe_apply_chat_template): 核心变更文件,添加了 developer 角色检测、转换和 system 合并逻辑,并集成到 safe_apply_chat_template 中,是所有 HF 模板处理的瓶颈。
tests/renderers/test_hf.py(模块 测试;类别 test;类型 test-coverage;符号 TestConvertDeveloperToSystem, test_converts_role, test_removes_tools_key, test_no_developer_messages_unchanged): 添加了完整的单元测试,覆盖 developer 转换和检测功能,确保正常路径和边界情况。
关键符号:_detect_developer_role_support, _convert_developer_to_system, _consolidate_system_messages, safe_apply_chat_template
关键源码片段
vllm/renderers/hf.py
核心变更文件,添加了 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": "
".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
添加了完整的单元测试,覆盖 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
评论区精华
主要讨论点:
-
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",作者已更正。
-
逻辑应该放在 safe_apply_chat_template 共享瓶颈处 (design): 逻辑最终被实现在 safe_apply_chat_template 中,而不是 responses/utils.py。
- 合并 system messages 可能影响某些模型 (design): 当前合并策略被保留,未来可考虑通过配置控制是否合并。
- PR 标题误用 "Move" (other): 标题已更正为准确描述。
风险与影响
关联脉络
参与讨论