Qwen3.8-27B 的 eos / bos / pad:三个配置文件里是三套值

2026-08-16

翻一个模型权重仓,最省事的读法是把散在几个 JSON 里的字段在脑子里合并成一张表:一个 bos、一个 eos、一个 pad,三个数字记下来就完事。Qwen3.8-27B 这个仓不吃这一套。截至 2026-08-16、对应快照 1d4bf0f,同一次提交里的三份配置文件,对这三个 token 给出了三种不同形态的写法——不是数值上的小出入,是字段名、数据类型、有没有这个值都不一样。

这篇只做一件事:把三处原样摆出来,标清楚在哪个文件的哪一行,再给出你自己去核的动作。哪一处在推理时真正生效、为什么会长成这样,我们不判断也不推测——这个仓库里只有权重与配置文件,没有建模源码、没有推理代码,本仓文件也没有对此作出说明。

三处并列:先看表

下面三行分别来自仓库根目录的三个文件(行号为我们采集时实读的行号):

文件boseospad
config.jsontext_configbos_token_id = 248044(第 12 行)eos_token_id = 248044(第 14 行)pad_token_id = null(第 101 行)
generation_config.jsonbos_token_id = 248044(第 2 行)eos_token_id = [248046, 248044](第 4-7 行)pad_token_id = 248044(第 8 行)
tokenizer_config.jsonbos_token = null(第 284 行),另有 add_bos_token = false(第 294 行)eos_token = <|im_end|>(第 287 行)pad_token = <|endoftext|>(第 290 行)

光看这张表还不够,因为前两行是数字、第三行是字符串。要对上,得先把 id 翻译成 token 文本。翻译表在 tokenizer_config.jsonadded_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.jsoneos_token<|im_end|>,即 248046。同一个概念,一处是单值、一处是数组、一处是字符串,指向的 token 也不完全重合。
  • bos 是”有”与”没有”的差别config.jsongeneration_config.json 都写了 bos_token_id248044;而 tokenizer_config.jsonbos_tokennull,同一文件第 294 行的 add_bos_token 还是 false
  • padnull 与有值的差别config.jsontext_config.pad_token_idnull;另外两处都指向 <|endoftext|>generation_config.json 写 id 248044tokenizer_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_sampletemperaturetop_ktop_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_configvision_config 三层结构;上面表里那三个 token 字段都在 text_config 里,顶层的 11 个键中没有它们,别在顶层找。

把这两条跑完、再把 config.jsontext_config 对一眼,上面那张表你就有了自己的版本,不必信任何人的转述。这也是读模型仓的一个通用习惯:字段按文件读,不要在脑子里合并。合并这个动作本身,就是把”三处不一致”抹平成”一处结论”的地方。

顺带看一眼模板层的收尾

<|im_end|> 这个 token 不只出现在 tokenizer_config.json 里。chat_template.jinja(170 行,与 tokenizer_config.jsonchat_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_ppresence_penaltyrepetition_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" 并补上 factororiginal_max_position_embeddings 的做法——发布出来的这份配置里,后两个键根本不存在。也就是说,这部分能力按 model card 自述需要使用者自行改配置或传启动参数才生效。
  • 上下文长度也有类似情况:config.json 第 93 行的 max_position_embeddingstokenizer_config.json 第 289 行的 model_max_length 都是 262144,而 model card 里出现的 1,000,000 这个数,在发布的配置文件里没有任何字段承载。

列这几条不是要评价什么,只是想说明:这个仓里,README 与配置、配置与配置之间,本来就存在若干处需要分别读、分别记的地方。token 那三处只是其中最容易被合并掉的一处。

什么情况说明你碰到的不是这件事

最后划一下边界,免得把不相干的现象往这里套:

  • 如果你的问题来自推理框架的启动参数,那不在本文范围内。reasoning_effortpreserve_thinkingenable_thinking 这三个控制变量在本仓的唯一落点是 chat template(模板变量),generation_config.jsonconfig.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 在三份文件里各有各的写法,你想知道自己那份是什么,就把上面三条命令跑一遍,别在脑子里替它们合表。

延伸阅读


本文依据 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?报名体系课或加入会员,照着学、照着用。