Qwen3 的思考模式在 Ollama 上关不掉:think:false 与 /no_think 双双失效的实测记录
按文档写了
think: false,结果答案变成了空的。 换chat端点、往 prompt 里塞/no_think,全都不行——最反直觉的是,加了think: false之后推理内容会混进回答正文,连过滤的依据都没有了。 这篇把 12 组对照实验和原始 JSON 摆出来,给出两条能落地的走法,以及 Spring AI 侧一个会直接坑到你的细节。
结论先说
think: false没有关掉思考。 它只是把推理内容从thinking字段搬进了response字段——比不设更糟,因为你连「哪段是推理」都判断不了了。/no_think同样无效。 模型会把/no_think当成你问题的一部分照读,推理照旧。- 代价是数量级的。 一个 33 字的答案,模型在推理上花了 2210 字;单次问答 19 秒。而同一台机器上纯生成速度有 88 tok/s——不是模型慢,是算力全花在推理上了。
- 预算不够时答案是空的。
num_predict: 200时response是空字符串,done_reason: length,而调用方看起来一切正常。 - Spring AI / OpenAI 兼容端点更隐蔽。 推理被塞进非标准的
reasoning字段,content是 0 字,finish_reason: length——Java 侧拿到的是一个「成功返回的空答案」。 - 两条可用走法:换非思考版
qwen3:4b-instruct;或留在思考版,把预算提到 ≥ 2048 并只取response/content(接受 10–19 秒延迟)。
一、问题现场
场景:给一个本地 RAG 问答链路的最后一步接上生成模型。检索部分已经跑通,切片、向量、关键词检索都对,卡在「让模型读 5 段原文写答案」这一步。
环境:
| 项 | 值 |
|---|---|
| 系统 | Windows 11 |
| GPU | RTX 4060 Ti 8GB |
| Ollama | 0.40.3(官方 ollama --version 输出) |
| 生成模型 | qwen3:4b,Q4_K_M,2.5 GB,digest 前缀 359d7dd4 |
| 嵌入模型 | bge-m3,1024 维 |
| 调用方式 | HTTP 直连 127.0.0.1:11434,/api/generate、/api/chat、/v1/chat/completions |
现象很直接:按文档关掉思考模式,然后提问,response 字段是一个空字符串:
{
"model": "qwen3:4b",
"response": "",
"done_reason": "length",
"eval_count": 200,
"eval_duration": 2270000000
}
done_reason: length 说明 200 个 token 的预算全用光了,但 response 里一个字都没有。token 去哪了?
二、排查过程
我按「先验证最朴素的假设」的顺序排了一遍,每一步都留了原始数据。
假设一:模型本身太慢 —— 排除
先确认底子。不设任何特殊参数的裸生成:
curl -s http://127.0.0.1:11434/api/generate -d '{
"model": "qwen3:4b",
"prompt": "用一句话说明什么是向量检索。",
"stream": false,
"options": { "num_ctx": 4096, "num_predict": 200 }
}'
结果:88.0 tok/s,预填速度 5252 tok/s(2709 token 的上下文只用了 0.24 秒)。
模型不慢。同样的速度,200 个 token 只需要 2.3 秒。 所以问题不是性能,是这 200 个 token 用在了哪里。
假设二:预算太小 —— 部分成立,但不是根因
把预算拉到 2048:
{
"thinking": "(1784 字)……",
"response": "向量检索是将数据转换为向量后,通过计算向量间相似度来检索最匹配查询向量的技术。",
"done_reason": "stop",
"eval_count": 1157
}
答案出来了,只有 30 字。但整次调用生成了 1157 个 token,耗时 13.8 秒。
注意这里的字段——thinking 和 response 是分开的。说明 Ollama 是有能力把推理摘出来的,只是默认不替你摘。
到这里可以确定:token 被推理吃掉了。推理大约固定占用 1400–1600 个 token,而答案只有 30 来个字。
假设三:think: false 能关掉 —— 排除,而且更糟
这是文档给的第一个办法:
curl -s http://127.0.0.1:11434/api/generate -d '{
"model": "qwen3:4b",
"think": false,
"prompt": "用一句话说明什么是向量检索。",
"stream": false,
"options": { "num_ctx": 4096, "num_predict": 2048 }
}'
结果:
{
"thinking": "",
"response": "嗯,用户让我用一句话说明什么是向量检索。首先,我得确定用户的需求是什么。……语义匹配的检索技术。” 这样既准确又简洁,符合用户要求。 </think> 向量检索是将数据映射为向量后,基于向量空间中的相似度计算实现语义匹配的检索技术。",
"done_reason": "stop"
}
thinking 变空了,而 response 变成了 2116 字——推理全文 + 一个裸的 </think> 标签 + 真正的答案,全糊在一起。
这是整次排查里最关键的一个转折:think: false 不是「不要思考」,而是**「不要拆分」**。模型照样思考,只是 Ollama 不再把推理单独放进 thinking,而是让它顺着输出流一起落到 response 里。
也就是说,加了这个参数之后:
- 推理没有减少(token 消耗一样)
- 你失去了唯一能过滤推理的依据(
thinking字段空了) - 只能自己按
</think>字符串去切——而那个标签是模型自己吐出来的文本,并不可靠
所以 think: false 在这个场景下是负优化。如果你已经写了,先把它删掉。
假设四:prompt 里加 /no_think —— 排除
Qwen3 官方文档提到过软开关,于是把 /no_think 放在 prompt 开头:
curl -s http://127.0.0.1:11434/api/generate -d '{
"model": "qwen3:4b",
"prompt": "/no_think\n用一句话说明什么是向量检索。",
"stream": false,
"options": { "num_ctx": 4096, "num_predict": 200 }
}'
结果:模型把 /no_think 当成了问题的正文。
{
"thinking": "首先,用户的问题是:“/no_think 用一句话说明什么是向量检索。” 这看起来像是一个指令,意思是“不要思考”或“n…",
"response": "",
"done_reason": "length"
}
它在推理里认真地分析「这看起来像是一个指令」——然后继续思考到预算耗尽。response 依然是空的。
假设五:换个端点 —— 排除
/api/chat(带 think: false)的行为和 generate 完全一致:message.thinking 为空,推理落进 message.content,预算耗尽后 done_reason: length。
换端点解决不了,因为问题不在端点上。
假设六:如果我用的是 OpenAI 兼容端点呢?—— 这才是最该看的一格
Java 服务层用 Spring AI 的话,默认走的是 Ollama 的 OpenAI 兼容接口 /v1/chat/completions。这条路径的表现更隐蔽:
curl -s http://127.0.0.1:11434/v1/chat/completions -d '{
"model": "qwen3:4b",
"messages": [{"role": "user", "content": "用一句话说明什么是向量检索。"}],
"max_tokens": 200
}'
返回的 message 对象字段是 ["role", "content", "reasoning"]——
| 字段 | 值 |
|---|---|
content | 0 字(空字符串) |
reasoning | 有内容(推理全文) |
finish_reason | length |
usage.completion_tokens | 200 |
非标准的 reasoning 字段承载了推理,content 是空的。 而 Spring AI(以及大多数 OpenAI 兼容客户端)只认 content。
结果就是:HTTP 200、结构完整、content 是空字符串——一个"成功返回的空答案"。 这类故障最难查,因为链路上每一步都不报错。
把 max_tokens 提到 2048,答案才出来:
{
"content": "向量检索是将数据转化为向量后,基于向量相似度查找语义最接近项的技术。",
"reasoning": "(2333 字)……",
"finish_reason": "stop",
"usage": { "completion_tokens": 1507 }
}
34 字的答案,1507 个 token,18.1 秒。
补充:在真实 RAG 场景下再测一次
前面都是玩具问题。换成真实链路的样子——3 段检索结果 + 强制引用约束:
你是严谨的金融文档问答助手。只根据【参考原文】回答,每句结论后标注来源编号如 [1],
找不到答案就回答「根据提供的文档无法确定」。回答不超过 80 字。
结果:
| 指标 | 值 |
|---|---|
| prompt | 135 token(约 0.03 秒预填) |
| 生成总量 | 1578 token |
| 其中推理 | 2210 字 |
| 其中答案 | 33 字 |
| 耗时 | 19.0 秒 |
答案只占输出量的 2%。 剩下 98% 是模型在自言自语。
(顺带一提:答案本身是对的——「年化利率:产品甲 3.85% [1],产品乙 8.35% [2]」。说明 4B 在这个任务上的质量没问题,问题纯粹在成本结构。)
三、根因
把上面的数据串起来,机制是三件事叠在一起:
第一,think 参数在 Ollama 0.40.3 上对 qwen3:4b 不生效。
qwen3:4b 是混合推理模型——同一份权重,既能输出思考过程,也能不输出。能不能关掉,取决于推理框架有没有实现对模型模板的 enable_thinking 控制。这次实测说明:Ollama 这个版本上没兑现。
这不是「模型做不到」,而是框架层的一层承诺没落地。所以你会看到一个很微妙的状态:参数被接受了(不报错),行为没变。
第二,think: false 的语义是「不拆分」,不是「不思考」。
Ollama 的输出里有两条通道:thinking(推理)和 response(回答)。思考模式关闭的正确实现应该是让模型不产生推理 token;而实际观察到的是——模型照旧产生推理 token,只是 Ollama 不再把它路由到 thinking 字段。
于是推理流量全部涌进了 response。这就是为什么「关掉思考」之后,回答反而变长了。
第三,推理长度和问题难度基本无关。
| 问题 | 答案长度 | 推理长度 |
|---|---|---|
| 「用一句话说明什么是向量检索」 | 30 字 | 1784 字 |
| 上面 RAG 场景(3 段上下文 + 引用约束) | 33 字 | 2210 字 |
一个「一句话解释」的问题,推理一千七百多字。这是 4B 小模型的自检习惯:它会反复确认「用户到底想要什么」「我要不要精炼」「不能太专业」——每一步都在自问自答,而且停不下来。
所以不要指望「简单问题推理就短」。预算是按最坏情况留的。
四、正确做法
方案 A(推荐):换非思考版模型
Ollama 官方的 qwen3 模型库里,除了 qwen3:4b,还有明确分开的两个变体:
| 标签 | 大小 | 上下文 | 说明 |
|---|---|---|---|
qwen3:4b | 2.5 GB | 256K | 混合推理(本文实测的问题主体) |
qwen3:4b-instruct | 2.5 GB | 256K | 非思考版,同一尺寸 |
qwen3:4b-thinking | 2.5 GB | 256K | 纯思考版 |
ollama pull qwen3:4b-instruct
同样 2.5 GB、同样 256K 上下文,只是不做推理输出。RAG 这类「读原文写短答案」的任务,推理带来的质量提升有限,而它还占着 98% 的输出预算——换模型是最省事的一刀。
判断你该不该换:如果你的 prompt 已经明确约束了「只依据原文、不超过 N 字、编号范围」,那么模型的大部分推理是在重复确认这些约束——这部分思考对结果没有增益。
方案 B:留在思考版,改预算和取值方式
如果因为某些原因必须用思考版:
- 预算提到 ≥ 2048。 200 不够,700 也不够(实测
response仍是空的)。 - 不要设
think: false。 让推理待在thinking字段里,你才有干净的输出。 - 只取
response(或 OpenAI 端点的content),忽略thinking。 - 对外声明延迟预期。19 秒和 2 秒是两个产品。
用原生端点时的最小可用写法:
curl -s http://127.0.0.1:11434/api/generate -d '{
"model": "qwen3:4b",
"prompt": "你的 RAG 提示词",
"stream": false,
"options": { "num_ctx": 4096, "num_predict": 2048, "temperature": 0.3 },
"keep_alive": "5m"
}' | jq -r '.response'
一个关于端点选择的小结论
| 端点 | 推理放哪 | 回答放哪 | 预算不足时 |
|---|---|---|---|
/api/generate | thinking | response | response 为空,thinking 有内容 |
/api/chat | message.thinking | message.content | 同上 |
/v1/chat/completions | message.reasoning(非标准) | message.content | content 为空,HTTP 仍然 200 |
原生端点的字段是标准的,OpenAI 兼容端点的 reasoning 是 Ollama 自己加的扩展字段。 这也是为什么它更难被发现——你用标准客户端读标准字段,读到的是空的,而且不报错。
怎么确认自己有没有中招
三条,任中一条就是这个坑:
- 响应里
done_reason == "length"但内容是空的 → 预算被推理吃光了 - 回答正文里出现「嗯,用户让我……」「我需要确认……」这类口吻 → 推理漏进了正文(说明你设了
think: false) - 输出 token 数远大于答案长度(比如答案 30 字、输出 1500 token)→ 推理在记账,只是没显示
五、踩坑清单
think: false是负优化。 它不减少 token,只破坏输出分离。已经写了就删掉。/no_think在 Ollama 这条链路上不生效。 它会被当成问题正文读进去,实测无效。(这一点和 Qwen3 官方文档的说法不一致——文档描述的是模型自己的模板行为,Ollama 不一定透传。)- 默认预算给 200 是不够的,给 700 也不够。 推理稳定占 1400–1600 个 token,按这个量级留。
- 不要用
context_length的默认值。 这个模型自报最大 262144,在 8GB 卡上按这个开 KV cache 会直接爆——永远显式传num_ctx: 4096。 - OpenAI 兼容端点会给出「成功返回的空答案」。 Java / Spring AI 侧要额外判空,不要把空字符串当成模型的回答渲染出去。
- 推理长度和问题难度无关。 一个「一句话说明」的问题也能推理一千七百字,别用问题简单程度去估预算。
- 「模型慢」是个容易误判的结论。 这台机器上纯生成有 88 tok/s,一点都不慢;慢是因为 98% 的 token 花在了推理上。先看 token 花在哪,再谈优化。
六、一句话总结
think: false 不是关掉思考,是把思考藏进回答里;/no_think 在这个链路上根本没人听——两个开关都失效的时候,别在提示词上继续打补丁,把模型换成非思考版。
本文在 Windows 11 + RTX 4060 Ti 8GB + Ollama 0.40.3 + qwen3:4b (Q4_K_M) 上实测,所有 JSON 均为真实响应。
如果你想复现:先按第二节的六组命令跑一遍,重点看 done_reason、thinking 和 eval_count 三个字段。
欢迎在评论区补充你的环境与结论——这类「文档承诺 vs 实际行为」的差异,换个版本就可能反过来。