# PR #46025 完整报告

- 仓库：`vllm-project/vllm`
- 标题：fix(anthropic): auto-detect template support for mid-conversation system messages
- 合并时间：2026-06-19 04:19
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/46025

---

# 执行摘要

- 一句话：自动检测模板是否支持中间 system 消息，兼容 Qwen 强制 system-first 模型
- 推荐动作：该 PR 值得精读，尤其是自动检测策略和 Jinja 沙箱的使用。设计上避免了模板配置爆炸，使服务器自适应。代码改动量小，测试覆盖充分。可作为 vLLM 处理用户自定义模板兼容性的参考模式。

# 功能与动机

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

# 实现拆解

1. **检测逻辑**：在 `AnthropicServingMessages.__init__` 中调用新类方法 `_detect_merge_inline_system`，注入 `chat_template` 参数。该方法使用 `jinja2.sandbox.ImmutableSandboxedEnvironment` 渲染一个 `[system, user, system, user]` 测试对话。若渲染抛出 `jinja2.TemplateError`（如 Qwen 模板中的 `loop.first` 守卫），则返回 `True`（需要合并）；否则返回 `False`（保留原位）。当 `chat_template` 为 `None` 时默认返回 `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 服务；类别 source；类型 core-logic；符号 _detect_merge_inline_system）: 核心文件，添加了自动检测系统合并的方法并修改了消息转换逻辑。
- `tests/entrypoints/anthropic/test_anthropic_messages_conversion.py`（模块 测试；类别 test；类型 test-coverage；符号 TestDetectMergeInlineSystem, test_qwen_template_requires_merge, test_no_restriction_no_merge, test_no_template_defaults_merge）: 新增测试类，覆盖三种模板场景，验证检测逻辑正确性。

关键符号：_detect_merge_inline_system, _convert_system_message, _convert_messages

## 关键源码片段

### `vllm/entrypoints/anthropic/serving.py`

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

```python
# 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.

```

# 评论区精华

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 格式修复。

- 可变类状态设计 (design): 作者改为实例属性，并通过关键字参数 `merge_inline_system` 传递给类方法。
- Jinja 渲染安全 (security): 作者采纳，在模块级别导入并使用 `ImmutableSandboxedEnvironment`。
- 异常捕获宽度 (correctness): 作者修改捕获为 `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 渲染安全 , 模板兼容性误判 , 回归风险（消息转换逻辑修改）

# 关联脉络

- PR #44602 [Bugfix] Preserve inline system messages for prefix caching: 前驱 PR，保留内联 system 消息位置，但未兼容模板限制，导致本 PR 的修复需求。
- PR #41114 [Bug]: Report "System message must be at the beginning." When using qwen3.6-27B: 关联 Issue，报告 Qwen 模型 system 消息位置错误，是本 PR 要解决的直接问题。