Qwen3.8-27B 的图像输入怎么传:示例字段与预处理配置的对应

2026-08-16

想把一张图片喂给 Qwen3.8-27B,你会发现「图像」这件事在这个仓库里被写在了三个互不相邻的地方:model card 的示例代码里是一个 JSON 字段名,chat_template.jinja 里是一段 Jinja 判定条件,preprocessor_config.json 里则是另一组参数。三层用的词不一样,也不都能互相对上。

这篇就沿着「一条带图请求从字段名走到占位符」的路径,把三层逐一贴出来。全部内容来自 Hugging Face Qwen/Qwen3.8-27B 的 model card 与随仓配置文件,核对日 2026-08-16,快照 1d4bf0f。我们没有下载权重、没有部署、没有推理过一个 token,所以下文只有文本与行号,没有任何「跑起来会怎样」。

第一层:model card 的 Image Input 示例

README.md:343 的小节标题是 ##### Image Input,代码块在 README.md:345-373,原样照抄:

from openai import OpenAI
# Configured by environment variables
client = OpenAI()

messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "image_url",
                "image_url": {
                    "url": "https://qianwen-res.oss-accelerate.aliyuncs.com/Qwen3.5/demo/CI_Demo/mathv-1327.jpg"
                }
            },
            {
                "type": "text",
                "text": "The centres of the four illustrated circles are in the corners of the square. ..."
            }
        ]
    }
]

chat_response = client.chat.completions.create(
    model="Qwen/Qwen3.8-27B",
    messages=messages,
)
print("Chat response:", chat_response)

text 那一长串题干原文很长,上面用省略号代替,其余逐字照抄;完整题面见 model card 该代码块原文。)

从这段能读到三件事,都只是 README 文本本身:

  1. 图像走的是 OpenAI 兼容的 "type": "image_url""image_url": {"url": ...} 两层结构(README.md:355-358)。
  2. 示例给的是一个 .jpg 的 https URL(README.md:357)。README 未在此处说明是否支持 base64 或本地文件路径。
  3. 这个请求没有传任何 extra_body、没有传采样参数、也没有传 reasoning_effortREADME.md:368-371)。对比同一份 model card 里的 Text-Only 示例(README.md:284-340),那一段把 enable_thinking / preserve_thinking 放进了 extra_body.chat_template_kwargs,并把 reasoning_effort="xhigh" 作为顶层参数传了进去。两个示例的写法差异就摆在那儿,我们只记录,不推断哪种写法是「推荐的」。

示例运行前的环境准备,model card 写在 README.md:272-280pip install -U openai,然后 export OPENAI_BASE_URL='your-base-url'export OPENAI_API_KEY='your-api-key'——your-base-urlyour-api-key 是 README 原文的占位符,实际值请自行按所用服务填写。(补一句:密钥不要写进会被提交的文件,这是通用运维做法,不是 model card 的内容。)

第二层:chat_template.jinja 靠什么认出这是一张图

真正决定「这一项算不算图像」的是模板。chat_template.jinja 全文 170 行,开头两行先建了两个计数器:{%- set image_count = namespace(value=0) %}{%- set video_count = namespace(value=0) %}chat_template.jinja:1-2)。

图像分支的判定条件在第 8 行:

{%- if 'image' in item or 'image_url' in item or item.type == 'image' %}

三个条件任一成立即进入图像分支。README 示例传的对象里既有 image_url 这个键、type 又是 "image_url",其中 'image_url' in item 这一条是对得上的。

进了这个分支之后,模板做四件事:

  • 如果当前正在渲染的是 system 消息(is_system_content),直接抛 System message cannot contain images.chat_template.jinja:10)。图片不能放进 system 消息,这一条在模板里是硬校验。
  • do_vision_count 为真时 image_count 自增(:12-14)。
  • add_vision_id 为真时,先输出 'Picture ' ~ image_count.value ~ ': ' 前缀(:15-17)。
  • 最后输出占位符 <|vision_start|><|image_pad|><|vision_end|>:18)。

计数这件事有个容易被忽略的细节:模板里 render_content 被调用了两次,用的 do_vision_count 不一样。主循环里是 render_content(message.content, true)chat_template.jinja:103),而计算 ns.last_query_index 的那次倒序扫描用的是 render_content(message.content, false):92)。也就是说图片编号只在主循环里累加。

视频侧的判定条件与图像不对称

同一个宏里,视频分支的条件在第 19 行:

{%- elif 'video' in item or item.type == 'video' %}

只有两个条件,不含 video_url。而 README 的 Video Input 示例传的正是 "type": "video_url""video_url": {...}README.md:387-390)。

两处位置写清楚:chat_template.jinja:8 的图像分支有三个条件、含 image_urlchat_template.jinja:19 的视频分支有两个条件、不含 video_url。这两处文本不一致,就这样。请求在到达模板之前是否会被推理框架改写,我们无法从这个仓库里核实,因此不推断运行时会发生什么,也不据此评价什么。

视频分支的其余动作与图像对称:system 内出现视频抛 System message cannot contain videos.:21),add_vision_id 为真时输出 'Video N: ':26-28),最终渲染成 <|vision_start|><|video_pad|><|vision_end|>:29)。既不是图也不是视频、又没有 text 键的项,落到 Unexpected item type in content.:33)。

中间一步:占位符对应哪几个 token id

模板输出的三个占位符都能在 tokenizer_config.jsonadded_tokens_decoder 里找到(该字典共 33 个条目,id 从 248044 连续到 248076):248053 <|vision_start|>248054 <|vision_end|>248055 <|vision_pad|>248056 <|image_pad|>248057 <|video_pad|>。这五个的 special 都是 true,且都列在 additional_special_tokenstokenizer_config.json:269,共 13 个)里。

config.json 侧的对应字段与上面一致:image_token_id248056config.json:5),video_token_id248057config.json:121),vision_start_token_id / vision_end_token_id248053 / 248054config.json:138-139)。这一层两个文件是对得上的。

顺带一提,模板里的 <think> / </think>(id 248068 / 248069)、<tool_call> / <tool_response> 等条目的 specialfalse,也不在那 13 个 additional_special_tokens 里——和 vision 系列的标记方式不同。

第三层:preprocessor_config.json 里到底有什么

preprocessor_config.json 全文 21 行,只有 9 个键,逐个列全:

字段行号
size.longest_edge16777216:3
size.shortest_edge65536:4
patch_size16:6
temporal_patch_size2:7
merge_size2:8
image_mean[0.5, 0.5, 0.5]:9-13
image_std[0.5, 0.5, 0.5]:14-18
processor_class"Qwen3VLProcessor":19
image_processor_type"Qwen2VLImageProcessorFast":20

比「有什么」更值得记的是「没有什么」。在放着这些配置文件的目录里逐词检查:

python -c "
import io
for name in ['preprocessor_config.json','video_preprocessor_config.json']:
    d=io.open(name,encoding='utf-8').read()
    for kw in ['fps','num_frames','do_sample_frames','max_pixels','min_pixels','longest_edge','shortest_edge']:
        print(name,kw,kw in d)
print('fps in config.json:', 'fps' in io.open('config.json',encoding='utf-8').read())
"

结果是:fpsnum_framesdo_sample_framesmax_pixelsmin_pixels 在两个预处理配置里全部为 Falselongest_edge / shortest_edge 为 True;config.jsonfps 也是 False。文件里同样没有 do_resize / do_rescale / do_normalize / rescale_factor / do_convert_rgb 这些常见键——全文就是上表那 9 个。这些键的缺省行为由哪一层提供,仓库里没有写,我们不做任何推断。

图片与视频两个预处理配置的差异只有三处:size.longest_edge1677721625165824)、size.shortest_edge655364096)、以及末尾的类型键(image_processor_typevideo_processor_type,后者值为 "Qwen3VLVideoProcessor")。归一化参数、patch_size=16temporal_patch_size=2merge_size=2processor_class 这四项两个文件完全一致。

size 的单位与换算,model card 只在视频那一处括注过一句(README.md:561 里把 469762048 括注为「对应 224k video tokens」)。图片侧的 16777216 / 65536 对应多少 token,仓库里没有说明,我们不做换算,也不做任何估算

和 config.json 里的 vision_config 对一遍

config.jsonvision_config 有几项与预处理配置同名或近名,值是对得上的:patch_size 都是 16config.json:134preprocessor_config.json:6),temporal_patch_size 都是 2config.json:136:7);名字不同但值相同的是 spatial_merge_size: 2config.json:135)与 merge_size: 2preprocessor_config.json:8)。

vision_config 里我们在 config.json:123-136 这一段核到的其余字段有:deepstack_visual_indexes[](空数组)、depth27hidden_act"gelu_pytorch_tanh"hidden_size1152in_channels3intermediate_size4304num_heads16num_position_embeddings2304out_hidden_size5120deepstack_visual_indexes 是个空数组这件事只照实记录,它意味着什么我们不推断;这些字段与实际权重的对应关系也无从核对,因为我们没有下载权重。

顺手记下的两处文本差异

一是预处理器类名的代际写法不统一:preprocessor_config.json:20"Qwen2VLImageProcessorFast",同文件 :19"Qwen3VLProcessor"video_preprocessor_config.json:20"Qwen3VLVideoProcessor",而 config.json:3architectures"Qwen3_5ForConditionalGeneration"config.json:7model_type"qwen3_5"。四处出现了三种代际写法。

二是示例资源的 URL 路径段:README.md:357:389:440 三个示例图片与视频的 URL 路径里都含 /Qwen3.5/demo/,而本仓模型是 Qwen3.8-27B(README.md:7)。

两处都只陈述位置与差异,不推断原因,也不据此评价任何东西。

你可以自己怎么核

如果你手上有这个仓库的文本文件,上面每一条都能自己复核一遍,顺序是固定的:

  1. 先看 README.md:343-373,确认请求体里字段名的字面写法;
  2. 再打开 chat_template.jinja,看第 8 行与第 19 行两个判定条件,确认你要传的键能不能命中;
  3. tokenizer_config.jsonadded_tokens_decoder 里查一遍占位符对应的 id,再回 config.json:5 / :121 / :138-139 交叉核对;
  4. 最后用上面那段 Python 检查预处理配置里到底有没有你以为存在的字段——max_pixels / min_pixels 这类在别处见惯的键,在这个仓库里是不存在的。

本文核不到的部分

  • base64 与本地文件路径:README 示例只有 https URL,是否支持其它形式未说明。
  • size 的单位与换算:图片侧没有任何换算说明,不推算。
  • 视频时长上限与支持的容器/编码格式:README 未说明,README.md:29 只有 hour-scale videos 这一句表述。
  • 抽帧相关参数fps=2do_sample_frames=True 这两个默认值写在 README.md:409,但它出自哪一层,我们在本地这几个文本文件里检索不到对应字段;而且 README.md:405-417 那整段示例在 README 里是注释状态(每行以 # 起始),README.md:407 另写明 This feature is currently supported only in vLLM.
  • 托管服务README.md:15-16 提到官方 API 服务由 Qwen Cloud 提供、并会有带更多生产特性的托管版本,但同句原文写着 The service is coming soon. Stay tuned for updates.——按 model card 自述尚未上线,不能当成现有能力用。这里只中立转述其存在。

延伸阅读


本文依据 Hugging Face 仓库 Qwen/Qwen3.8-27B 的 model card 与随仓配置文件 (config.jsongeneration_config.jsonpreprocessor_config.jsonchat_template.jinja 等)整理, 核对日 2026-08-16,对应仓库快照 1d4bf0f。 本文内容为 model card 与配置文件口径,我们没有下载权重、没有部署、也没有推理过这个模型, 因此不涉及生成质量、推理速度与显存占用的任何描述;文中所有评测数字均为 model card 自述,我们没有复现。 模型仓库内容随上游更新而变动,请以官方最新说明为准。 安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。