Qwen3.8-27B 的 eos / bos / pad:三个配置文件里是三套值
翻一个模型权重仓,最省事的读法是把散在几个 JSON 里的字段在脑子里合并成一张表:一个 bos、一个 eos、一个 pad,三个数字记下来就完事。Qwen3.8-27B 这个仓不吃这一套。截至 2026-08-16、对应快照 1d4bf0f,同一次提交里的三份配置文件,对这三个 token 给出了三种不同形态的写法——不是数值上的小出入,是字段名、数据类型、有没有这个值都不一样。
这篇只做一件事:把三处原样摆出来,标清楚在哪个文件的哪一行,再给出你自己去核的动作。哪一处在推理时真正生效、为什么会长成这样,我们不判断也不推测——这个仓库里只有权重与配置文件,没有建模源码、没有推理代码,本仓文件也没有对此作出说明。
三处并列:先看表
下面三行分别来自仓库根目录的三个文件(行号为我们采集时实读的行号):
| 文件 | bos | eos | pad |
|---|---|---|---|
config.json 的 text_config | bos_token_id = 248044(第 12 行) | eos_token_id = 248044(第 14 行) | pad_token_id = null(第 101 行) |
generation_config.json | bos_token_id = 248044(第 2 行) | eos_token_id = [248046, 248044](第 4-7 行) | pad_token_id = 248044(第 8 行) |
tokenizer_config.json | bos_token = null(第 284 行),另有 add_bos_token = false(第 294 行) | eos_token = <|im_end|>(第 287 行) | pad_token = <|endoftext|>(第 290 行) |
光看这张表还不够,因为前两行是数字、第三行是字符串。要对上,得先把 id 翻译成 token 文本。翻译表在 tokenizer_config.json 的 added_tokens_decoder 里:248044 是 <|endoftext|>(该条目起于第 4 行),248046 是 <|im_end|>(起于第 20 行)。这个字典我们数过,共 33 个条目,id 从 248044 连续排到 248076,中间没有缺号。
翻译完再看,差异就清楚了:
eos三处三个形态。config.json里是单值248044,即<|endoftext|>;generation_config.json里是一个长度为 2 的数组[248046, 248044],且把248046(<|im_end|>)放在首位;tokenizer_config.json的eos_token是<|im_end|>,即248046。同一个概念,一处是单值、一处是数组、一处是字符串,指向的 token 也不完全重合。bos是”有”与”没有”的差别。config.json与generation_config.json都写了bos_token_id为248044;而tokenizer_config.json的bos_token是null,同一文件第 294 行的add_bos_token还是false。pad是null与有值的差别。config.json的text_config.pad_token_id是null;另外两处都指向<|endoftext|>(generation_config.json写 id248044,tokenizer_config.json写字符串<|endoftext|>)。
三处并列陈述到此为止。哪个”对”、哪个被框架优先采用、作者是不是漏了同步,这些我们一概不写——本仓没有任何一份文件回答了这个问题,我们也没有下载权重、没有加载模型、没有推理过一个 token。
自己核一遍:两条可执行的命令
这类事实最忌讳听二手转述。下面两条命令原样出自我们读取这几个文本文件时的核对记录,你在自己拉下来的模型仓目录里同样可以执行(目录路径换成你自己的位置)。
第一条,把 generation_config.json 整个打出来。这个文件很小,只有 202 字节、13 行,一屏就看完:
python -c "import json,io;print(json.load(io.open('generation_config.json',encoding='utf-8')))"
我们核对时得到的输出是 {'bos_token_id': 248044, 'do_sample': True, 'eos_token_id': [248046, 248044], 'pad_token_id': 248044, 'temperature': 1.0, 'top_k': 20, 'top_p': 0.95}——一共只有 7 个键,除了上面三个 token 字段,剩下的就是 do_sample、temperature、top_k、top_p。
第二条,把 id 翻译成 token 文本,顺便看 tokenizer 侧的三个字段:
python -c "
import json,io
tc=json.load(io.open('tokenizer_config.json',encoding='utf-8'))
at=tc['added_tokens_decoder']
for i in ['248044','248046','248053','248054','248056','248057']: print(i, at.get(i,{}).get('content'))
print('total added tokens', len(at)); print('max added id', max(int(k) for k in at))
print('model_max_length', tc.get('model_max_length'))
print('eos_token', tc.get('eos_token'), 'pad', tc.get('pad_token'), 'bos', tc.get('bos_token'))
"
我们的输出里,248044 <|endoftext|>、248046 <|im_end|>,最后一行是 eos_token <|im_end|> pad <|endoftext|> bos None。
剩下的 config.json 直接打开翻就行。这份文件共 4,312 字节、140 行,只有顶层、text_config、vision_config 三层结构;上面表里那三个 token 字段都在 text_config 里,顶层的 11 个键中没有它们,别在顶层找。
把这两条跑完、再把 config.json 的 text_config 对一眼,上面那张表你就有了自己的版本,不必信任何人的转述。这也是读模型仓的一个通用习惯:字段按文件读,不要在脑子里合并。合并这个动作本身,就是把”三处不一致”抹平成”一处结论”的地方。
顺带看一眼模板层的收尾
<|im_end|> 这个 token 不只出现在 tokenizer_config.json 里。chat_template.jinja(170 行,与 tokenizer_config.json 的 chat_template 字段字节级完全一致,两者长度同为 8,952、md5 相同)在拼装对话时,user 消息、assistant 消息都以 <|im_end|> 加一个换行收尾;工具返回那一段(第 147-158 行)也是在下一条不是 tool 或本条是最后一条时补 <|im_end|>\n。
模板的另一头也值得一起看。chat_template.jinja:163-170 是生成提示那一段:add_generation_prompt 为真时先输出 <|im_start|>assistant 加换行,随后分两种情况——enable_thinking 已定义且为 false 时预填一个空的思考块 <think>\n\n</think>\n\n,其余情况只预填 <think>\n,把生成起点放在思考块内部。顺带记一笔:同一份模板里两处 enable_thinking 的判定写法并不相同,第 46 行是 enable_thinking is undefined or enable_thinking is true,第 165 行是 enable_thinking is defined and enable_thinking is false。这两处的字面条件我们只做记录,不推断其后果。
同一份 added_tokens_decoder 里还有一个容易看漏的细节:<think>、</think>、<tool_call>、</tool_call>、<tool_response>、</tool_response> 这些条目的 special 字段都是 false,而 <|endoftext|>、<|im_start|>、<|im_end|> 这一系是 true;第 269 行的 additional_special_tokens 里列了 13 个 token,其中不含前面那六个。所以”长得像特殊标记”和”在配置里被标为 special”是两回事,看 id 的时候顺手看一眼 special 字段,能省掉不少来回。
这不是孤例:同一个仓里还有几处同类差异
把眼光放宽一点,这个仓里”文档里写的”和”配置里有的”对不上,不止 token 这一处:
- model card 的 Best Practices 一节(
README.md:500-505)自述建议了min_p、presence_penalty、repetition_penalty三个采样参数。我们对仓内的配置文件逐词检索,这三个词的命中数全部为 0——generation_config.json的 7 个键里没有它们,其它配置文件里也没有;它们只出现在 model card 正文(README.md:502-503)。model card 同一节还写明,采样参数的支持情况因推理框架而异。 config.json第 113 行的rope_parameters.rope_type现值是"default",而 model card 在讲长文本处理时给出的是把它改成"yarn"并补上factor、original_max_position_embeddings的做法——发布出来的这份配置里,后两个键根本不存在。也就是说,这部分能力按 model card 自述需要使用者自行改配置或传启动参数才生效。- 上下文长度也有类似情况:
config.json第 93 行的max_position_embeddings与tokenizer_config.json第 289 行的model_max_length都是262144,而 model card 里出现的 1,000,000 这个数,在发布的配置文件里没有任何字段承载。
列这几条不是要评价什么,只是想说明:这个仓里,README 与配置、配置与配置之间,本来就存在若干处需要分别读、分别记的地方。token 那三处只是其中最容易被合并掉的一处。
什么情况说明你碰到的不是这件事
最后划一下边界,免得把不相干的现象往这里套:
- 如果你的问题来自推理框架的启动参数,那不在本文范围内。
reasoning_effort、preserve_thinking、enable_thinking这三个控制变量在本仓的唯一落点是 chat template(模板变量),generation_config.json、config.json、两个 preprocessor 配置里都没有对应字段;至于各推理框架如何把它们映射到自己的参数上,本仓文件没有说明,我们也没有查阅框架源码。 - 如果你用的是官方托管服务,参数传法与开源框架侧不同:model card 有两处 Note 明写,用 Qwen Cloud 的 API 时要直接传
"enable_thinking": False/"preserve_thinking": False,而不是包在chat_template_kwargs里。另外,该托管版本按 model card 自述标注为 coming soon,尚未上线,这里只中立记录其存在。 - 如果你比对的是别的快照,先确认版本。上面所有行号与取值都对应 2026-08-16 我们采集时的快照
1d4bf0f,模型仓内容会随上游更新变动,行号尤其容易漂。
真正能带走的只有一句:这三个 token 在三份文件里各有各的写法,你想知道自己那份是什么,就把上面三条命令跑一遍,别在脑子里替它们合表。
延伸阅读
- 从头读起:Qwen3.8-27B 是什么:一个模型仓里有哪些文件、各自负责什么
- 本专题共 35 篇,完整分组目录见专题页
- Qwen3.8-27B 的 chat_template 逐段拆:四类消息分别怎么渲染
- Qwen3.8-27B 的 preserve_thinking 控制什么、在哪一层生效
本文依据 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 自述,我们没有复现。
模型仓库内容随上游更新而变动,请以官方最新说明为准。