Prhub

#46025 fix(anthropic): auto-detect template support for mid-conversation system messages

原始 PR 作者 felix0080 合并时间 2026-06-19 04:19 文件变更 2 提交数 2 评论 8 代码增减 +120 / -6

执行摘要

自动检测模板是否支持中间 system 消息,兼容 Qwen 强制 system-first 模型

关联 Issue #41114 报告 Qwen3.6-27B 返回 "System message must be at the beginning." 错误。前驱 PR #44602 修复了内联 system 消息位置问题,但未兼容模板限制。本 PR 旨在自动检测模板能力,避免手动配置,同时维持前缀缓存优化。

该 PR 值得精读,尤其是自动检测策略和 Jinja 沙箱的使用。设计上避免了模板配置爆炸,使服务器自适应。代码改动量小,测试覆盖充分。可作为 vLLM 处理用户自定义模板兼容性的参考模式。

讨论亮点

Review 中 bbrowning 提出四项关键修改请求:

  1. 可变类状态:原始版本将合并标志存储在类属性上(type(self)._merge_inline_system),bbrowning 认为“在类上放置可变状态感觉不对”。作者改为实例属性,并通过关键字参数 merge_inline_system 传递给类方法。

  2. Jinja 沙箱安全:原始代码未使用沙箱环境,直接渲染模板存在安全风险。bbrowning 建议使用 ImmutableSandboxedEnvironment,正如 vllm/renderers/hf.py 中所做。作者采纳并在模块级别导入 jinja2.sandbox.ImmutableSandboxedEnvironment

  3. 异常捕获宽度:原始代码捕获裸 Exception,bbrowning 建议收窄到 jinja2.TemplateError。作者修改。

  4. 复用已有方法:bbrowning 建议合并 system 消息时复用 _extract_system_text 方法,避免重复计费头逻辑。作者采纳并调整实现。

作者逐一响应并修复,最终 bbrowning 批准并手动推送了 ruff 格式修复。

实现拆解

  1. 检测逻辑:在 AnthropicServingMessages.__init__ 中调用新类方法 _detect_merge_inline_system,注入 chat_template 参数。该方法使用 jinja2.sandbox.ImmutableSandboxedEnvironment 渲染一个 [system, user, system, user] 测试对话。若渲染抛出 jinja2.TemplateError(如 Qwen 模板中的 loop.first 守卫),则返回 True(需要合并);否则返回 False(保留原位)。当 chat_templateNone 时默认返回 True。检测结果存储在实例属性 self._merge_inline_system 中。

  2. 消息转换调整_convert_anthropic_to_openai_request 接受关键字参数 merge_inline_system,默认 False。此参数传递到 _convert_system_message_convert_messages。当 merge_inline_system=True 时,_convert_system_message 会扫描 anthropic_request.messages 中所有 role 为 system 的消息,将其 content 提取并追加到系统文本中(复用已有的 _extract_system_text 方法)。_convert_messages 则跳过所有 system 角色消息,避免重复。

  3. 测试覆盖:新增 TestDetectMergeInlineSystem 测试类,包含三个测试用例:test_qwen_template_requires_merge(Qwen 模板返回 True)、test_no_restriction_no_merge(无限制模板返回 False)、test_no_template_defaults_merge(无模板返回 True)。

  4. 安全性:使用 ImmutableSandboxedEnvironment 沙箱化 Jinja 渲染,防止模板注入恶意代码。异常捕获限定为 jinja2.TemplateError

文件 模块 状态 重要度
vllm/entrypoints/anthropic/serving.py Anthropic 服务 modified 7.48
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py 测试 modified 6.99

关键符号

_detect_merge_inline_system _convert_system_message _convert_messages

关键源码片段

vllm/entrypoints/anthropic/serving.py core-logic

核心文件,添加了自动检测系统合并的方法并修改了消息转换逻辑。

# vllm/entrypoints/anthropic/serving.py (head)@staticmethod
def _detect_merge_inline_system(chat_template: str | None) -> bool:
    """Auto-detect whether the chat template requires system-first ordering.    Renders a [system, user, system, user] conversation against the
    template; if it raises (e.g. Qwen's ``loop.first`` guard), the
    model needs inline system messages merged into the leading block.
    """
    if not chat_template:
        # No chat_template set → adopt safe default: merge
        return True
    try:
        # Use an immutable sandbox to prevent arbitrary code execution
        # from user-supplied templates. Same pattern as in
        # vllm/renderers/hf.py.
        env = jinja2.sandbox.ImmutableSandboxedEnvironment(
            trim_blocks=True,
            lstrip_blocks=True,
            extensions=[jinja2.ext.loopcontrols],
        )
        env.from_string(chat_template).render(
            messages=[
                {"role": "system", "content": "t"},
                {"role": "user", "content": "t"},
                {"role": "system", "content": "t"},
                {"role": "user", "content": "t"},
            ],
            add_generation_prompt=False,
        )
        # Rendering succeeded → template accepts mid-conversation systems
        return False
    except jinja2.TemplateError:
        # Exception raised (e.g. Qwen's ``loop.first`` guard) → merge needed
        return True# In __init__ the flag is stored:
# self._merge_inline_system = self._detect_merge_inline_system(chat_template)
# This flag is later passed as a keyword-only argument to
# _convert_anthropic_to_openai_request and then to _convert_system_message
# and _convert_messages.

评论区精华

可变类状态设计 设计

bbrowning 指出使用 `type(self)._merge_inline_system` 在类上放置可变状态感觉不对,应改为实例属性并通过参数传递。

结论:作者改为实例属性,并通过关键字参数 `merge_inline_system` 传递给类方法。 · 已解决

Jinja 渲染安全 安全

bbrowning 要求使用 `ImmutableSandboxedEnvironment` 沙箱化 Jinja 渲染,防止模板注入。

结论:作者采纳,在模块级别导入并使用 `ImmutableSandboxedEnvironment`。 · 已解决

异常捕获宽度 正确性

bbrowning 建议将裸 `Exception` 收窄为 `jinja2.TemplateError`,避免隐藏其他问题。

结论:作者修改捕获为 `jinja2.TemplateError`。 · 已解决

风险与影响

  1. Jinja 渲染安全风险:聊天模板由用户提供,可能包含恶意代码。本 PR 使用 ImmutableSandboxedEnvironment 沙箱渲染,与 vLLM 其他渲染逻辑一致,风险可控。
  2. 模板兼容性风险:检测机制基于是否抛出 TemplateError,但某些模板可能渲染成功却仍不接受中间 system 消息(例如静默忽略),导致错误合并或未合并。但保守默认设为合并,未检测到时不合并,误判影响有限。
  3. 性能风险:每个模型初始化时多一次简单模板渲染,影响可忽略。
  4. 回归风险:消息转换逻辑修改可能影响非 system 消息处理。测试覆盖了检测与合并路径,但未覆盖完整的端到端请求转换(需集成测试)。
  5. 前缀缓存性能:对于无限制模板,仍保持原位,前缀缓存效果不变。对于需合并的模板(如 Qwen),将破坏前缀缓存优化,但功能兼容性优先。

用户影响:使用 Anthropic API 的用户在部署 Qwen3.5/3.6 等模型时,之前因 #44602 而报错的请求现在正常。无需用户配置,升级即可。
系统影响:所有 Anthropic 路由(streaming + non-streaming)均受影响,因为 _convert_anthropic_to_openai_request 总是被调用。但检测仅在初始化时执行一次,无运行时开销。
团队影响:维护了模板兼容性的自动适配,减少了后续对类似问题的用户工单。但需注意,若未来有模板除 TemplateError 外还使用其他机制拒绝消息(如自定义异常),该检测可能失效。

Jinja 渲染安全 模板兼容性误判 回归风险(消息转换逻辑修改)

关联 Issue

#41114 [Bug]: Report "System message must be at the beginning." When using qwen3.6-27B

完整报告

参与讨论