Qwen3 的思考模式在 Ollama 上关不掉:think:false 与 /no_think 双双失效的实测记录

0 阅读13分钟

Qwen3 的思考模式在 Ollama 上关不掉:think:false 与 /no_think 双双失效的实测记录

按文档写了 think: false,结果答案变成了空的。 换 chat 端点、往 prompt 里塞 /no_think,全都不行——最反直觉的是,加了 think: false 之后推理内容会混进回答正文,连过滤的依据都没有了。 这篇把 12 组对照实验和原始 JSON 摆出来,给出两条能落地的走法,以及 Spring AI 侧一个会直接坑到你的细节。


结论先说

  1. think: false 没有关掉思考。 它只是把推理内容从 thinking 字段搬进了 response 字段——比不设更糟,因为你连「哪段是推理」都判断不了了。
  2. /no_think 同样无效。 模型会把 /no_think 当成你问题的一部分照读,推理照旧。
  3. 代价是数量级的。 一个 33 字的答案,模型在推理上花了 2210 字;单次问答 19 秒。而同一台机器上纯生成速度有 88 tok/s——不是模型慢,是算力全花在推理上了。
  4. 预算不够时答案是空的。 num_predict: 200 时 response 是空字符串,done_reason: length,而调用方看起来一切正常。
  5. Spring AI / OpenAI 兼容端点更隐蔽。 推理被塞进非标准的 reasoning 字段,content 是 0 字,finish_reason: length——Java 侧拿到的是一个「成功返回的空答案」。
  6. 两条可用走法:换非思考版 qwen3:4b-instruct;或留在思考版,把预算提到 ≥ 2048 并只取 response / content(接受 10–19 秒延迟)。

一、问题现场

场景:给一个本地 RAG 问答链路的最后一步接上生成模型。检索部分已经跑通,切片、向量、关键词检索都对,卡在「让模型读 5 段原文写答案」这一步。

环境:

项值
系统Windows 11
GPURTX 4060 Ti 8GB
Ollama0.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"]——

字段值
content0 字(空字符串)
reasoning有内容(推理全文)
finish_reasonlength
usage.completion_tokens200

非标准的 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 字。

结果:

指标值
prompt135 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:4b2.5 GB256K混合推理(本文实测的问题主体)
qwen3:4b-instruct2.5 GB256K非思考版,同一尺寸
qwen3:4b-thinking2.5 GB256K纯思考版
ollama pull qwen3:4b-instruct

同样 2.5 GB、同样 256K 上下文,只是不做推理输出。RAG 这类「读原文写短答案」的任务,推理带来的质量提升有限,而它还占着 98% 的输出预算——换模型是最省事的一刀。

判断你该不该换:如果你的 prompt 已经明确约束了「只依据原文、不超过 N 字、编号范围」,那么模型的大部分推理是在重复确认这些约束——这部分思考对结果没有增益。

方案 B:留在思考版,改预算和取值方式

如果因为某些原因必须用思考版:

  1. 预算提到 ≥ 2048。 200 不够,700 也不够(实测 response 仍是空的)。
  2. 不要设 think: false。 让推理待在 thinking 字段里,你才有干净的输出。
  3. 只取 response(或 OpenAI 端点的 content),忽略 thinking。
  4. 对外声明延迟预期。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/generatethinkingresponseresponse 为空,thinking 有内容
/api/chatmessage.thinkingmessage.content同上
/v1/chat/completionsmessage.reasoning(非标准)message.contentcontent 为空,HTTP 仍然 200

原生端点的字段是标准的,OpenAI 兼容端点的 reasoning 是 Ollama 自己加的扩展字段。 这也是为什么它更难被发现——你用标准客户端读标准字段,读到的是空的,而且不报错。

怎么确认自己有没有中招

三条,任中一条就是这个坑:

  1. 响应里 done_reason == "length" 但内容是空的 → 预算被推理吃光了
  2. 回答正文里出现「嗯,用户让我……」「我需要确认……」这类口吻 → 推理漏进了正文(说明你设了 think: false)
  3. 输出 token 数远大于答案长度(比如答案 30 字、输出 1500 token)→ 推理在记账,只是没显示

五、踩坑清单

  1. think: false 是负优化。 它不减少 token,只破坏输出分离。已经写了就删掉。
  2. /no_think 在 Ollama 这条链路上不生效。 它会被当成问题正文读进去,实测无效。(这一点和 Qwen3 官方文档的说法不一致——文档描述的是模型自己的模板行为,Ollama 不一定透传。)
  3. 默认预算给 200 是不够的,给 700 也不够。 推理稳定占 1400–1600 个 token,按这个量级留。
  4. 不要用 context_length 的默认值。 这个模型自报最大 262144,在 8GB 卡上按这个开 KV cache 会直接爆——永远显式传 num_ctx: 4096。
  5. OpenAI 兼容端点会给出「成功返回的空答案」。 Java / Spring AI 侧要额外判空,不要把空字符串当成模型的回答渲染出去。
  6. 推理长度和问题难度无关。 一个「一句话说明」的问题也能推理一千七百字,别用问题简单程度去估预算。
  7. 「模型慢」是个容易误判的结论。 这台机器上纯生成有 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 实际行为」的差异,换个版本就可能反过来。