Qwen3.8-27B 的图像输入怎么传:示例字段与预处理配置的对应
想把一张图片喂给 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 文本本身:
- 图像走的是 OpenAI 兼容的
"type": "image_url"加"image_url": {"url": ...}两层结构(README.md:355-358)。 - 示例给的是一个
.jpg的 https URL(README.md:357)。README 未在此处说明是否支持 base64 或本地文件路径。 - 这个请求没有传任何
extra_body、没有传采样参数、也没有传reasoning_effort(README.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-280:pip install -U openai,然后 export OPENAI_BASE_URL='your-base-url'、export OPENAI_API_KEY='your-api-key'——your-base-url 与 your-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_url;chat_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.json 的 added_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_tokens(tokenizer_config.json:269,共 13 个)里。
config.json 侧的对应字段与上面一致:image_token_id 是 248056(config.json:5),video_token_id 是 248057(config.json:121),vision_start_token_id / vision_end_token_id 是 248053 / 248054(config.json:138-139)。这一层两个文件是对得上的。
顺带一提,模板里的 <think> / </think>(id 248068 / 248069)、<tool_call> / <tool_response> 等条目的 special 是 false,也不在那 13 个 additional_special_tokens 里——和 vision 系列的标记方式不同。
第三层:preprocessor_config.json 里到底有什么
preprocessor_config.json 全文 21 行,只有 9 个键,逐个列全:
| 字段 | 值 | 行号 |
|---|---|---|
size.longest_edge | 16777216 | :3 |
size.shortest_edge | 65536 | :4 |
patch_size | 16 | :6 |
temporal_patch_size | 2 | :7 |
merge_size | 2 | :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())
"
结果是:fps、num_frames、do_sample_frames、max_pixels、min_pixels 在两个预处理配置里全部为 False,longest_edge / shortest_edge 为 True;config.json 里 fps 也是 False。文件里同样没有 do_resize / do_rescale / do_normalize / rescale_factor / do_convert_rgb 这些常见键——全文就是上表那 9 个。这些键的缺省行为由哪一层提供,仓库里没有写,我们不做任何推断。
图片与视频两个预处理配置的差异只有三处:size.longest_edge(16777216 对 25165824)、size.shortest_edge(65536 对 4096)、以及末尾的类型键(image_processor_type 对 video_processor_type,后者值为 "Qwen3VLVideoProcessor")。归一化参数、patch_size=16、temporal_patch_size=2、merge_size=2、processor_class 这四项两个文件完全一致。
size 的单位与换算,model card 只在视频那一处括注过一句(README.md:561 里把 469762048 括注为「对应 224k video tokens」)。图片侧的 16777216 / 65536 对应多少 token,仓库里没有说明,我们不做换算,也不做任何估算。
和 config.json 里的 vision_config 对一遍
config.json 的 vision_config 有几项与预处理配置同名或近名,值是对得上的:patch_size 都是 16(config.json:134 与 preprocessor_config.json:6),temporal_patch_size 都是 2(config.json:136 与 :7);名字不同但值相同的是 spatial_merge_size: 2(config.json:135)与 merge_size: 2(preprocessor_config.json:8)。
vision_config 里我们在 config.json:123-136 这一段核到的其余字段有:deepstack_visual_indexes 为 [](空数组)、depth 为 27、hidden_act 为 "gelu_pytorch_tanh"、hidden_size 为 1152、in_channels 为 3、intermediate_size 为 4304、num_heads 为 16、num_position_embeddings 为 2304、out_hidden_size 为 5120。deepstack_visual_indexes 是个空数组这件事只照实记录,它意味着什么我们不推断;这些字段与实际权重的对应关系也无从核对,因为我们没有下载权重。
顺手记下的两处文本差异
一是预处理器类名的代际写法不统一:preprocessor_config.json:20 是 "Qwen2VLImageProcessorFast",同文件 :19 是 "Qwen3VLProcessor",video_preprocessor_config.json:20 是 "Qwen3VLVideoProcessor",而 config.json:3 的 architectures 是 "Qwen3_5ForConditionalGeneration"、config.json:7 的 model_type 是 "qwen3_5"。四处出现了三种代际写法。
二是示例资源的 URL 路径段:README.md:357、:389、:440 三个示例图片与视频的 URL 路径里都含 /Qwen3.5/demo/,而本仓模型是 Qwen3.8-27B(README.md:7)。
两处都只陈述位置与差异,不推断原因,也不据此评价任何东西。
你可以自己怎么核
如果你手上有这个仓库的文本文件,上面每一条都能自己复核一遍,顺序是固定的:
- 先看
README.md:343-373,确认请求体里字段名的字面写法; - 再打开
chat_template.jinja,看第 8 行与第 19 行两个判定条件,确认你要传的键能不能命中; - 在
tokenizer_config.json的added_tokens_decoder里查一遍占位符对应的 id,再回config.json:5/:121/:138-139交叉核对; - 最后用上面那段 Python 检查预处理配置里到底有没有你以为存在的字段——
max_pixels/min_pixels这类在别处见惯的键,在这个仓库里是不存在的。
本文核不到的部分
- base64 与本地文件路径:README 示例只有 https URL,是否支持其它形式未说明。
size的单位与换算:图片侧没有任何换算说明,不推算。- 视频时长上限与支持的容器/编码格式:README 未说明,
README.md:29只有hour-scale videos这一句表述。 - 抽帧相关参数:
fps=2、do_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 自述尚未上线,不能当成现有能力用。这里只中立转述其存在。
延伸阅读
- 从头读起:Qwen3.8-27B 是什么:一个模型仓里有哪些文件、各自负责什么
- 本专题共 35 篇,完整分组目录见专题页
- Qwen3.8-27B 的文本调用示例逐行读:哪些参数是官方写的
- Qwen3.8-27B 的视频输入怎么传:那段被整体注释掉的抽帧示例
本文依据 Hugging Face 仓库 Qwen/Qwen3.8-27B 的 model card 与随仓配置文件
(config.json、generation_config.json、preprocessor_config.json、chat_template.jinja 等)整理,
核对日 2026-08-16,对应仓库快照 1d4bf0f。
本文内容为 model card 与配置文件口径,我们没有下载权重、没有部署、也没有推理过这个模型,
因此不涉及生成质量、推理速度与显存占用的任何描述;文中所有评测数字均为 model card 自述,我们没有复现。
模型仓库内容随上游更新而变动,请以官方最新说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。