# PR #28906 完整报告

- 仓库：`sgl-project/sglang`
- 标题：fix(anthropic): detect-and-passthrough mid-conversation system messages
- 合并时间：2026-06-26 08:14
- 原文链接：http://prhub.com.cn/sgl-project/sglang/pull/28906

---

# 执行摘要

- 一句话：自动检测 chat template，修复 mid-conversation system 消息导致 prefix cache 分叉
- 推荐动作：该 PR 值得精读。它展示了如何在不增加用户配置开销的前提下，通过运行时的模板探针来适配不同模型的行为差异，是解决多模型兼容性问题的良好范例。同时移除了一个与架构不匹配的协议层 validator，使关注点分离更正确。建议关注 `detect_inline_system_support` 的检测逻辑和 `_convert_to_chat_completion_request` 的转换分支。

# 功能与动机

PR #26773 无条件将 mid-conversation system 消息提升到顶层，导致支持内联 system 的 template（如 GLM、Kimi、Qwen3）的 prefix cache 每轮分叉，TTFT 和吞吐量严重下降（Issue #28883）。需要根据 template 的实际能力决定是否合并 system 消息，而不是一刀切。本 PR 采用与 vLLM 一致的方案（vllm-project/vllm#46025），通过检测 template 对非首 system 消息的支持能力来动态决策。

# 实现拆解

1. **新增模板检测函数**：在 `template_detection.py` 中添加 `detect_inline_system_support(chat_template)` 函数。该函数使用 `ImmutableSandboxedEnvironment` 渲染一个 `[system, user, system, user]` 测试对话，通过检查第二个 system 的 sentinel 文本是否出现在渲染结果中，来判断 template 是否支持内联 system。如果 template 抛出异常（如 Qwen 的首 system 守卫）或 sentinel 被静默丢弃，则返回 `False`（需要合并）。

2. **服务层集成检测**：在 `serving.py` 的 `AnthropicServing.__init__` 中调用该检测，并将结果存入 `self._merge_inline_system` 标志。新增 `_chat_template()` 方法从 tokenizer 获取 chat template；新增 `_extract_system_text()` 辅助函数统一提取 system 内容文本。

3. **转换逻辑调整**：在 `_convert_to_chat_completion_request` 中，当 `_merge_inline_system` 为 `True` 时，遍历 `messages` 提取所有 `system` 角色的文本并与顶层 `system` 字段合并为一个 `system` 消息；同时跳过这些 system 消息避免重复。当为 `False` 时，保持 system 消息在原位不动。`handle_count_tokens` 复用同一转换路径。

4. **移除协议层 validator**：删除 `protocol.py` 中的 `move_mid_conversation_system_messages` `model_validator`，因为协议层无法感知 template，该决策已上移到服务层。

5. **更新测试**：重写原有测试用例，分别验证 `merge`（使用 Qwen-style 首 system 守卫模板）和 `passthrough`（使用任意位置 system 模板）两种场景。新增 `TestDetectInlineSystemSupport` 覆盖 guarded / inline / silent-drop / missing-template 四种情况。

关键文件：
- `python/sglang/srt/entrypoints/anthropic/serving.py`（模块 服务层；类别 source；类型 core-logic；符号 _extract_system_text, _chat_template）: 核心服务层，新增内联 system 检测、system 文本提取函数，以及根据检测标志决定是否合并 system 消息的逻辑。
- `python/sglang/srt/entrypoints/anthropic/protocol.py`（模块 协议层；类别 source；类型 core-logic；符号 move_mid_conversation_system_messages）: 移除了无条件提升 mid-conversation system 的 model_validator，这是本 PR 的核心清理。
- `python/sglang/srt/managers/template_detection.py`（模块 模板检测；类别 source；类型 dependency-wiring；符号 detect_inline_system_support）: 新增 detect_inline_system_support 函数，是智能决策的核心检测逻辑。
- `test/registered/unit/entrypoints/anthropic/test_serving.py`（模块 测试；类别 test；类型 test-coverage；符号 __init__, _serving, test_in_messages_system_role_folded_to_top_level, test_in_messages_system_merged_when_template_requires_first）: 测试覆盖 merge 和 passthrough 两种场景，新增模板探针测试用例，验证检测函数的正确性。

关键符号：detect_inline_system_support, _extract_system_text, _chat_template, _convert_to_chat_completion_request, __init__

## 关键源码片段

### `python/sglang/srt/entrypoints/anthropic/serving.py`

核心服务层，新增内联 system 检测、system 文本提取函数，以及根据检测标志决定是否合并 system 消息的逻辑。

```python
# python/sglang/srt/entrypoints/anthropic/serving.py

def _extract_system_text(
    content: Union[str, list[AnthropicContentBlock]],
) -> Optional[str]:
    """将 system 消息的 content 展平为字符串，返回 None 表示无有效文本。
    
    处理 str 和 struct 两种 Anthropic API 允许的格式。"""
    if isinstance(content, str):
        return content.strip() or None
    texts = []
    for block in content:
        # 支持 Pydantic model 和 dict 两种形态（HTTP / 编程两种路径）
        if isinstance(block, BaseModel) and getattr(block, "type", None) == "text":
            text = getattr(block, "text", "")
        elif isinstance(block, dict) and block.get("type") == "text":
            text = block.get("text", "")
        else:
            continue
        text = (text or "").strip()
        if text:
            texts.append(text)
    return "\n".join(texts) if texts else None


class AnthropicServing:
    def __init__(self, openai_serving_chat: OpenAIServingChat):
        self.openai_serving_chat = openai_serving_chat
        # 在初始化时检测 template 能力，一次检测，后续服用
        self._merge_inline_system = not detect_inline_system_support(
            self._chat_template()
        )

    def _chat_template(self) -> Optional[str]:
        """从 tokenizer 提取 chat template 字符串。"""
        tokenizer_manager = getattr(self.openai_serving_chat, "tokenizer_manager", None)
        if tokenizer_manager is None:
            return None
        tokenizer = getattr(tokenizer_manager, "tokenizer", None)
        if tokenizer is None:
            return None
        return getattr(tokenizer, "chat_template", None)

    # 在 _convert_to_chat_completion_request 中的应用：
    system_parts: list[str] = []
    if anthropic_request.system:
        # 处理顶层 system 字段
        if isinstance(anthropic_request.system, str):
            if anthropic_request.system.strip():
                system_parts.append(anthropic_request.system)
        else:
            for block in anthropic_request.system:
                if block.type == "text" and block.text:
                    system_parts.append(block.text)

    # 当需要合并时，遍历 messages 提取 system 角色
    if self._merge_inline_system:
        for msg in anthropic_request.messages:
            if msg.role != "system":
                continue
            text = _extract_system_text(msg.content)
            if text:
                system_parts.append(text)

    if system_parts:
        openai_messages.append(
            {"role": "system", "content": "\n".join(system_parts)}
        )

    # ... 然后当合并时，跳过 messages 中的 system 角色消息
    for msg in anthropic_request.messages:
        if msg.role == "system" and self._merge_inline_system:
            continue
        # ... 其余转换

```

### `python/sglang/srt/managers/template_detection.py`

新增 detect_inline_system_support 函数，是智能决策的核心检测逻辑。

```python
# python/sglang/srt/managers/template_detection.py

def detect_inline_system_support(chat_template: Optional[str]) -> bool:
    """检测 chat template 是否支持内联 system 消息。
    
    返回 True 表示模板可以将 system 消息渲染在任何位置（passthrough）；
    返回 False 表示必须合并到首位 system 块（merge）。
    检测方法：渲染一个 [system, user, system, user] 探针，
    如果第二个 system 的 sentinel 文本出现在输出中，说明模板处理了非首 system。
    如果模板抛出异常或静默丢弃第二个 system，返回 False。"""
    if not chat_template:
        return False  # 无模板时保守默认：合并
    sentinel = "__sglang_inline_system_sentinel__"
    try:
        # 使用 sandbox 环境避免模板注入风险
        env = jinja2.sandbox.ImmutableSandboxedEnvironment(
            trim_blocks=True,
            lstrip_blocks=True,
            extensions=[jinja2.ext.loopcontrols],
        )
        rendered = env.from_string(chat_template).render(
            messages=[
                {"role": "system", "content": "t"},
                {"role": "user", "content": "t"},
                {"role": "system", "content": sentinel},
                {"role": "user", "content": "t"},
            ],
            add_generation_prompt=False,
        )
        return sentinel in rendered
    except jinja2.TemplateError:
        return False  # 模板抛出异常（如 Qwen 的 loop.first 守卫）→ 合并
    except Exception:
        return False  # 任何其他异常 → 保守合并

```

# 评论区精华

review 中 `gemini-code-assist[bot]` 提出使用 tokenizer 的 `apply_chat_template` 方法进行检测，因为 tokenizer 已经注册了正确的自定义过滤器和全局变量（如 `raise_exception`），可以避免手动 Jinja 环境可能导致的假阳性。但最终实现未采纳该建议，仍使用 ImmutableSandboxedEnvironment，原因可能是保持检测过程轻量和独立。此外，该 bot 还建议注册一个 dummy `raise_exception` 来更好处理保险箱模板，但最终代码通过捕获 `jinja2.TemplateError` 来处理，也足够。这些建议未在最终代码中体现，但 PR 已被批准合并。

- 使用 tokenizer.apply_chat_template 替代手动 Jinja 渲染 (design): PR 作者未采纳该建议，最终代码仍使用 ImmutableSandboxedEnvironment 手动渲染。但通过捕获 TemplateError 也达到了类似效果。PR 被批准合并，说明 reviewer 认为当前方案足够。

# 风险与影响

- 风险：
 - **模板检测误判风险**：`detect_inline_system_support` 使用手动 Jinja 环境，可能缺失某些自定义全局变量或过滤器，导致模板渲染异常被当成不支持内联（返回 `False`）而合并 system 消息；相反，如果模板返回不符合预期但 sentinel 巧合出现，可能返回 `True` 导致 system 消息被放置在不适用的位置。当前通过 sentinel 检测和异常捕获，风险较低但存在边缘情况。
 - **API 兼容性回归**：对于原本需要合并的模板（如 Qwen），本 PR 的行为应与之前一致；但对于支持内联的模板，其行为从合并变为保留原位，可能打破依赖之前行为（如期望 system 消息总在首位）的客户端。不过这更符合 Anthropic API 规范。
 - **prefix cache 性能改善**：对于支持内联的模板，性能显著提升；但若误判为不支持，则性能下降（回到合并状态）。
 - **代码维护风险**：检测逻辑嵌入在 `AnthropicServing` 中，如果 tokenizer 的 chat template 在运行时动态变化（少见），会导致不一致。
- 影响：
 - **用户影响**：Claude Code 用户的多轮 agent 场景下，mid-conversation steer 消息能够正确生效，且 prefix cache 保持稳定，显著降低 TTFT 和提升吞吐量。对于只使用单轮或不支持内联模板的用户无影响。
 - **系统影响**：增加了初始化时的模板渲染开销（一次），但检测结果可缓存。转换路径增加分支，但开销可忽略。
 - **团队影响**：新增了一个可复用的模板检测函数，未来可用于其他需要判断模板能力的地方。代码结构更清晰（协议层不再负责语义决策）。
 - **影响程度**：中等到高，因为修复了一个真实性能回归，且未引入新的配置项，自动适配。
 - 风险标记：Anthropic API 核心路径 , 模板检测误判风险 , API 兼容性回归 , 初始化开销

# 关联脉络

- PR #26773 [fix] Add mid-conversation system message support for Anthropic API: 本 PR 的前置修复，首次添加了 system 角色支持但使用了无条件合并方案，导致了本 PR 解决的回归问题。
- PR #28883 [Anthropic] Mid-conversation system messages are hoisted to top-level, forking the prefix cache on inline-capable templates: 本 PR 修复的 Issue，详细描述了问题影响和期望行为。