Qwen3.8-27B 的 preserve_thinking 控制什么、在哪一层生效
如果你在 Qwen3.8-27B 的权重仓里全文搜 preserve_thinking,会得到一个可能和直觉相反的结果:它不在 generation_config.json 里,也不在 config.json 里,一次都没有出现。
我们在 2026-08-16 对该仓的 8 个文本文件逐词统计过命中次数,结果是这样的:
| 关键词 | 命中文件与次数 |
|---|---|
preserve_thinking | tokenizer_config.json 2 次、chat_template.jinja 2 次;其余文件 0 |
enable_thinking | tokenizer_config.json 4 次、chat_template.jinja 4 次;其余文件 0 |
reasoning_effort | tokenizer_config.json 6 次、chat_template.jinja 6 次;其余文件 0 |
而 tokenizer_config.json 的 chat_template 字段与 chat_template.jinja 文件字节级完全一致——两者长度同为 8,952 字节,md5 同为 519239a4908bb1f805bbce5fa8c8a242,且都不以换行结尾。也就是说上表里看起来的「两个文件」,实际是同一份模板的两处存放。
所以第一条结论就已经很清楚了:preserve_thinking、enable_thinking、reasoning_effort 三者在这个仓库里的唯一实现处是 chat template,是模板变量,不是采样参数、也不是模型结构里的开关。 generation_config.json 全文只有 202 字节、13 行,字段就是 bos_token_id、do_sample、eos_token_id、pad_token_id、temperature、top_k、top_p,里面没有这三个词中的任何一个。
唯一的那行条件:chat_template.jinja:116
preserve_thinking 在整个模板里真正参与判断只有一处,在消息主循环的 assistant 分支里:
{%- if preserve_thinking is undefined or preserve_thinking is true or loop.index0 > ns.last_query_index %}
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
{%- else %}
{{- '<|im_start|>' + message.role + '\n' + content }}
{%- endif %}
(chat_template.jinja:116-120)
把这段拆开看,有几件事值得单独拎出来:
第一,它作用的对象是历史 assistant 消息的渲染方式,不是模型的生成行为。 两个分支的差别只有一处:走上面那支时,assistant 消息头之后会插入 <think>\n + reasoning_content + \n</think>\n\n,然后才是正文;走下面那支时直接接正文。换句话说,这个变量决定的是「你这一次请求送进去的 prompt 文本里,历史回合的思考块要不要一起送」。
第二,reasoning_content 从哪来是有前置条件的。 紧邻上面的 111-115 行里,模板先把 reasoning_content 置空串,只有当 message.reasoning_content is string 成立时才采用消息里带的值,随后再 |trim。也就是说,如果你回填历史消息时没有带 reasoning_content 这个键、或者带的不是字符串,模板此处拼进 <think> 与 </think> 之间的就是那个空串。这是我们对模板字面写法的陈述——我们没有用 apply_chat_template 渲染过任何一条消息,不给出「某种输入最终会渲染成什么文本」的结论。
第三,条件是三项 or。 preserve_thinking is undefined 排在最前面,意味着调用方什么都不传时走的就是带思考块的那一支——这与 model card 的自述一致:README.md:264 写 preserve_thinking 对所有 workload 默认启用。
ns.last_query_index 是怎么算出来的
上面那行条件的第三项 loop.index0 > ns.last_query_index,是理解「设为 False 之后到底会保留哪些」的关键。这个下标由主循环之前的一段倒序扫描算出(chat_template.jinja:88-98):
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
{%- for message in messages[::-1] %}
{%- set index = (messages|length - 1) - loop.index0 %}
{%- if ns.multi_step_tool and message.role == "user" %}
{%- set content = render_content(message.content, false)|trim %}
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
{%- set ns.multi_step_tool = false %}
{%- set ns.last_query_index = index %}
{%- endif %}
{%- endif %}
{%- endfor %}
它从消息列表末尾往前扫,找最后一条「不是纯工具返回」的 user 消息的下标。判定「纯工具返回」的条件写在第 93 行:trim 之后的正文同时以 <tool_response> 开头、以 </tool_response> 结尾。若扫完整个列表都没找到(ns.multi_step_tool 仍为真),模板直接抛 No user query found in messages.(chat_template.jinja:99-101)。
这个「跳过纯工具返回」的判定不是可有可无的细节。模板在 tool 角色的处理里(147-158 行)明确把工具返回渲染成 \n<tool_response>\n + 正文 + \n</tool_response>,并且在上一条消息不是 tool 时先输出一个 <|im_start|>user——工具返回是以 user 角色的消息承载的。所以在一段带工具调用的多轮对话里,末尾往往堆着好几条形式上是 user、实质是工具结果的消息;last_query_index 的算法把它们排除在外。
回到第 116 行:preserve_thinking 为 false 时,前两项判定都不成立,只剩 loop.index0 > ns.last_query_index 这一项决定去留——下标严格大于该值的 assistant 消息仍带思考块,其余只输出正文。
README 与模板的措辞粒度不同
这里有一处需要照实标出来的差异,我们只陈述两边写了什么,不判断哪边「对」:
README.md:474写:把preserve_thinking设为False时,只保留最新一条 user 消息的 thinking 块。chat_template.jinja:116的字面条件是preserve_thinking is undefined or preserve_thinking is true or loop.index0 > ns.last_query_index,其中ns.last_query_index由chat_template.jinja:88-98定义为「最后一条正文不是纯<tool_response>包裹的 user 消息的下标」。
两处措辞的粒度不一样。我们没有用 apply_chat_template 实际渲染过任何一条消息,因此不对两种表述做等价性判断,只把两处位置标出来,让你能自己去比对。要自己核的话,动作很具体:打开 chat_template.jinja 看第 116 行与第 88 至 98 行,再翻 model card 的 Disable Preserved Thinking 一节(README.md:469-493),两边对着读一遍即可。
两套传法,别搞混
model card 里 preserve_thinking 的传法出现了两套,都是原文写明的:
开源框架侧,enable_thinking 与 preserve_thinking 放在 extra_body.chat_template_kwargs 里,而 reasoning_effort 是 create() 的顶层参数。Text-Only 示例的原文是这样(README.md:291-303):
completion = client.chat.completions.create(
model="Qwen/Qwen3.8-27B",
messages=messages,
extra_body={
"chat_template_kwargs": {
"enable_thinking": True, # on by default
"preserve_thinking": True, # on by default
},
},
reasoning_effort="xhigh", # xhigh by default; supported levels are xhigh, medium, and low
stream=True,
stream_options={"include_usage": True},
)
关掉时的写法在 README.md:485-488:extra_body={"chat_template_kwargs": {"preserve_thinking": False}}。
Qwen Cloud 侧则不同。README.md:492-493 的 Note 明写:用 Qwen Cloud 的 API 时直接传 "preserve_thinking": False,不要包在 chat_template_kwargs 里;README.md:465-466 对 enable_thinking 也有同样口径的说明。这两处 Note 是 model card 自己给的,我们没有调用过任何 API 去验证。顺带一提,该托管服务按 README.md:15-16 的自述是 coming soon——这里只中立记录它的存在。
同一个参数在 model card 内部就摆着两种放法,这里只把两处位置标出来、不推断为什么不一样:开源框架侧的写法在 README.md:294-299、README.md:457-460 与 README.md:485-488,Qwen Cloud 侧的说明在 README.md:466 与 README.md:493。写代码前照着你实际调用的那一侧核一遍,比按印象传参稳妥。至于各推理框架具体如何把这些键映射到 chat_template_kwargs,本仓的文件里没有说明,README 只给了 OpenAI SDK 侧的写法(README.md:300),我们也没有去查框架源码。
顺便厘清它和另外两个开关的分工
同一份模板里另外两个变量的落点和 preserve_thinking 完全不同,放一起看更容易记住:
| 变量 | 出现行 | 模板里的判定写法 |
|---|---|---|
enable_thinking | 46、165 | 46 行按「undefined 或 true」进入思考分支;165 行按「defined 且 false」预填空思考块 |
reasoning_effort | 47、49 | 47 行用 default('xhigh') 过滤器取默认值,合法值集合是 xhigh/medium/low,不在集合内抛异常 |
preserve_thinking | 116 | 「undefined 或 true 或 loop.index0 > ns.last_query_index」时渲染 <think> 块 |
enable_thinking 影响的是 prompt 结尾的生成起点:chat_template.jinja:163-170 里,add_generation_prompt 为真时先输出 <|im_start|>assistant\n,随后若 enable_thinking 已定义且为 false,就预填一个空的 <think>\n\n</think>\n\n,否则预填 <think>\n。
reasoning_effort 影响的是 prompt 开头:档位解析块(45-56 行)会把一段英文提示词赋给 reasoning_instructions,再由 57-87 行以 system 消息的形式拼进 prompt——有 tools 时它排在 # Tools 文案之前。这里还有一处照实记录的差异:README.md:260 把 medium 与 xhigh、low 并列描述为一档,而 chat_template.jinja:48 的合法值集合虽然包含 medium,51 至 55 行却只有 xhigh 与 low 两个分支会设置 reasoning_instructions,选 medium 时该变量保持第 45 行设的空串。
三者的位置各不相同:reasoning_effort 动的是开头的 system 段,preserve_thinking 动的是中间历史消息的渲染,enable_thinking 动的是结尾的生成起点。它们都在模板这一层,但改的不是同一段文本。
关于「该不该关」,我们不给建议
model card 在 README.md:472 为 preserved thinking 列了若干自述的好处,涉及上下文连续性、对需要决策一致性与减少重复推理的 agent 场景的价值,以及 KV cache 利用率——这些都是 model card 自述,我们没有验证过,也不在此基础上做任何推算或延伸。
我们能给的只有可执行的核对路径:想知道这个开关到底改了 prompt 里的哪些字节,就去读 chat_template.jinja 的第 116 至 120 行和第 88 至 98 行;想知道你的框架认不认这个键,就去比对你所用服务端的文档与上面两套传法。至于该不该关、关了会怎样,取决于你的会话形态与调用方式,这个仓库里没有给出通用答案,我们也不替它编一个。
延伸阅读
- 从头读起:Qwen3.8-27B 是什么:一个模型仓里有哪些文件、各自负责什么
- 本专题共 35 篇,完整分组目录见专题页
- Qwen3.8-27B 的 eos / bos / pad:三个配置文件里是三套值
- Qwen3.8-27B 的 reasoning_effort 三档:模板里只有两档有分支
本文依据 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 自述,我们没有复现。模型仓库内容随上游更新而变动,请以官方最新说明为准。