Prhub

#42660 [Bugfix] Fix incorrect chat template format for Qwen3.5

原始 PR 作者 DarkLight1337 合并时间 2026-05-15 11:52 文件变更 2 提交数 2 评论 1 代码增减 +6 / -0

执行摘要

修复 Qwen3.5 聊天模板格式检测错误

PR body 指出 Qwen3.5 的聊天模板内容格式被错误检测为 string 而非 openai,导致多模态输入与文本间多出换行,且交错显示默认不生效(除非手动设置 --interleave-mm-strings)。作者在调试 speculators 仓库的 token 不匹配问题时才发现此问题。

建议精读。该 PR 虽小,但展示了 Jinja2 AST 解析的边界情况处理,对理解 vLLM 的聊天模板自动检测机制有参考价值。

讨论亮点

review 过程中,gemini-code-assist[bot] 指出了一个潜在问题:新增分支中的 yield loop_ast, loop_iter.name 本意是将循环迭代器的名称(即 "content")作为第二个元素返回,这与函数定义“应返回内容项变量名”的意图不一致,也与已有分支的 loop_target.name 不统一。但该评论未得到作者回复,且该 PR 已经合并。实际上,观察最终合并的代码发现,提交的第二版已将 loop_iter.name 改为 loop_target.name,解决了该问题。

实现拆解

  1. 修复 _iter_nodes_assign_content_item 函数(vllm/renderers/hf.py:在原有的 for content in message['content'] 检测逻辑之后,新增一条分支:当循环迭代器是一个 jinja2.nodes.Name 节点且其名称恰好为 "content" 时,也将其识别为内容项循环,并 yield 对应的循环目标变量名。这样就能捕获类似 {%- for item in content -%} 的写法。
  2. 添加测试用例(tests/renderers/test_hf.py:在 test_resolve_content_format_hf_defined 的参数化列表中增加 ("Qwen/Qwen3.5-4B", "openai"),确保模板格式检测对该模型返回 "openai"
文件 模块 状态 重要度
vllm/renderers/hf.py 渲染器 modified 5.82
tests/renderers/test_hf.py 测试 modified 3.11

关键符号

_iter_nodes_assign_content_item

关键源码片段

vllm/renderers/hf.py core-logic

核心修复文件,新增对 `for item in content` 形式的 AST 检测分支。

# vllm/renderers/hf.py
def _iter_nodes_assign_content_item(root: jinja2.nodes.Node):
    message_varnames = [
        varname for _, varname in _iter_nodes_assign_messages_item(root)
    ]
​
    # Search for {%- for content in message['content'] -%} loops
    # or {%- for item in content -%} loops
    for loop_ast in root.find_all(jinja2.nodes.For):
        loop_iter = loop_ast.iter
        loop_target = loop_ast.target
​
        for varname in message_varnames:
            if _is_var_or_elems_access(loop_iter, varname, "content"):
                assert isinstance(loop_target, jinja2.nodes.Name)
                yield loop_ast, loop_target.name
                break
​
        # 新增:处理直接遍历 content 变量本身的循环
        # 例如 Qwen3.5 模板中的 {%- for item in content -%}
        if isinstance(loop_iter, jinja2.nodes.Name) and loop_iter.name == "content":
            assert isinstance(loop_target, jinja2.nodes.Name)
            yield loop_ast, loop_target.name
tests/renderers/test_hf.py test-coverage

增加 Qwen3.5-4B 的测试用例,确保回归覆盖。

# tests/renderers/test_hf.py
# 在 test_resolve_content_format_hf_defined 的参数列表中添加一行
@pytest.mark.parametrize(
    ("model", "expected_format"),
    [
        ("microsoft/Phi-3.5-vision-instruct", "string"),
        ("Qwen/Qwen2-VL-2B-Instruct", "openai"),
        ("Qwen/Qwen2.5-VL-3B-Instruct", "openai"),
        ("Qwen/Qwen3.5-4B", "openai"), # 新增:确保 Qwen3.5 检测为 openai
        ("fixie-ai/ultravox-v0_5-llama-3_2-1b", "string"),
        ("Qwen/Qwen2-Audio-7B-Instruct", "openai"),
        ("meta-llama/Llama-Guard-3-1B", "openai"),
    ],
)
def test_resolve_content_format_hf_defined(model, expected_format):
    # ... 测试逻辑不变

评论区精华

新分支 yield 的值应为 loop_target.name 而非 loop_iter.name 正确性

gemini-code-assist[bot] 指出新增分支中 `yield loop_ast, loop_iter.name` 返回的是集合名 `"content"`,而非单个内容项的变量名,与已有分支不一致。

结论:作者在最终提交中已将 `loop_iter.name` 修正为 `loop_target.name`,与函数意图一致。 · 已解决

风险与影响

变更范围极小(仅 6 行源码),且逻辑只影响 _detect_content_format 函数的 AST 解析路径。风险较低,主要风险是新分支可能误匹配某些不规范的模板,但错误匹配为 openai 比误判为 string 更安全(因为 string 格式会丢失多模态交错功能)。

直接影响 Qwen3.5 系列模型及使用类似 for item in content 模板的其他模型,使其聊天模板格式被正确识别为 openai,从而修复多模态输入换行和交错问题。不影响其他模型。

极小变更 仅影响模板检测路径

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论