Ollama 把 DeepSeek-R1 的思考过程放在
message.thinking,DeepSeek 云 API 放在message.reasoning_content。两个字段名不能换,客户端代码照搬云端 SDK 拿到的就是undefined或者None。同时think参数是 Ollama 顶层字段,不是options里的开关。
Issue & Env(案发现场与环境)
- 业务场景:先按 DeepSeek 官方文档的 OpenAI 兼容格式调通云 API,然后把
base_url切到本地 Ollama,发现思考过程拿不到了。 - 环境:Ollama v0.34.x(thinking 支持从 v0.5.x 开始稳定),客户端 Python
openaiSDK 或裸requests;模型必须支持 thinking(如deepseek-r1、qwen3、gpt-oss),非 thinking 模型这条规则不适用。
StackTrace(堆栈日志)
用云 API 的写法去 Ollama 拉思考字段,客户端侧常见三种现象。
字段根本不存在:
AttributeError: 'ChatCompletionMessage' object has no attribute 'reasoning_content'
字段存在但是空(Python SDK 容错后):
reasoning_content: ''
content: '' # 有时连 content 都空——思考跑满 num_predict 被打断
裸 JSON 调试打印,字段全 None:
{
"message": {
"role": "assistant",
"content": "...",
"thinking": "..."
}
}
注意 Ollama 响应里实际有 thinking,只是客户端用云 SDK 的字段名去取,等于拿空气。
Reproduce(复现代码)
云端能跑、Ollama 跑偏的最小写法:
# 1) DeepSeek 云 API:能跑通
from openai import OpenAI
cloud = OpenAI(api_key="sk-...", base_url="https://api.deepseek.com")
resp = cloud.chat.completions.create(
model="deepseek-reasoner",
messages=[{"role": "user", "content": "9.11 和 9.8 哪个大?"}],
extra_body={"thinking": {"type": "enabled"}},
)
print(resp.choices[0].message.reasoning_content) # 有内容
print(resp.choices[0].message.content) # 最终答案
# 2) 改成 Ollama:照搬 SDK 字段名就出问题
local = OpenAI(api_key="ollama", base_url="http://localhost:11434/v1")
resp = local.chat.completions.create(
model="deepseek-r1:8b",
messages=[{"role": "user", "content": "9.11 和 9.8 哪个大?"}],
)
print(resp.choices[0].message.reasoning_content) # AttributeError / 空
裸 HTTP 才是真相:
curl http://localhost:11434/api/chat -d '{
"model": "deepseek-r1:8b",
"messages": [{"role": "user", "content": "9.11 和 9.8 哪个大?"}],
"stream": false
}'
返回的字段是 message.thinking,不是 message.reasoning_content。
Root Cause(根因分析)
一、字段名是协议层定义,不是模型层
DeepSeek 云 API 的字段是 OpenAI 兼容扩展,reasoning_content 跟 content 同级。Ollama 走自家协议,message.thinking 是顶层字段。两个 API 各自独立设计,不存在别名映射。
Ollama 官方能力文档原文:
Chat returns
message.thinkingandmessage.content. Generate returnsthinkingandresponse.
DeepSeek 官方文档原文:
In thinking mode, the chain-of-thought content is returned via the
reasoning_contentparameter, at the same level ascontent.
字段名在两个 API 里都不一样,客户端代码按云 SDK 写就不能直接接 Ollama。
二、thinking 默认值依赖模型,不一定开
Ollama 的 think 参数是动态的,true / false / null / 模型定义字符串(如 low / medium / high),不传等于让模型走 /api/show 里 thinking.default 的值。deepseek-r1:8b 默认是开,但其它模型不一定。客户端云 SDK 把 thinking 塞进 extra_body 这种做法对 Ollama 完全无效——Ollama 顶层 think 才是有效位置。
更深一层:think 是 Ollama chat/generate 请求的顶层字段,不能塞进 options 里。这是个长期 bug,社区 Ollama issue #14793 里有人总结过:放在 options 里某些版本会无声忽略,或者完全无效。
三、generate 接口对 think: false 有 bug
历史上有过 generate 接口吞 think: false 的问题,即使在文档化后某些版本仍不稳定。要稳就全走 /api/chat,不要走 /api/generate。
四、<think>\n 强制开关与输出截断
DeepSeek-R1 在某些 query 上倾向直接 <think>\n</think> 跳过思考,把“思考模式”打成空。HuggingFace 官方建议在 prompt 末尾或 Modelfile 里加一个 <think>\n 强制前缀,否则性能波动大。
另外:如果 num_predict 比思考 + 最终答案的 token 数小,Ollama 会用 done_reason: "length" 砍断,结果是 content 为空、thinking 截了一半。客户端只读 content 就以为模型“哑了”,实际是思考没跑完。
The Fix(解决方案)
第一步:直接读 Ollama 字段
用 Ollama 原生 Python 客户端或 requests 时,按字段名取:
import requests
r = requests.post(
"http://localhost:11434/api/chat",
json={
"model": "deepseek-r1:8b",
"messages": [{"role": "user", "content": "9.11 和 9.8 哪个大?"}],
"think": True, # 顶层,不是 options 里
"stream": False,
},
timeout=120,
).json()
print(r["message"]["thinking"]) # 思考过程
print(r["message"]["content"]) # 最终答案
第二步:绕开 OpenAI SDK 的字段名差异
坚持用 OpenAI SDK 时,写一个统一访问层:
def reasoning_of(message):
# 兼容 Ollama 和 DeepSeek 云
return getattr(message, "reasoning_content", None) \
or message.thinking if hasattr(message, "thinking") else None
或者自定义 OpenAI 兼容的 base URL,但把响应后处理把 message.thinking 改写成 message.reasoning_content。代码侧抽适配层比改模型层稳定。
第三步:调对 think 字段位置
# 错:放在 options 里,可能被忽略
local.chat.completions.create(
model="deepseek-r1:8b",
messages=[...],
extra_body={"options": {"think": True}}, # 错位置
)
# 对:放顶层,Ollama 才认
requests.post(
"http://localhost:11434/api/chat",
json={
"model": "deepseek-r1:8b",
"messages": [...],
"think": True, # 对位置
"stream": False,
},
)
OpenAI SDK 的 extra_body 透传到底层 HTTP body,可以塞 think,但不要再包一层 options。
第四步:给思考留够输出预算
思考模型默认就要大量输出 token。num_predict 至少给 2048 起,常识题 / 数学题调到 4096 比较稳:
requests.post(
"http://localhost:11434/api/chat",
json={
"model": "deepseek-r1:8b",
"messages": [{"role": "user", "content": "..."}],
"think": True,
"stream": False,
"options": {"num_predict": 4096},
},
)
如果 client 端只看 content 字段且一直为空,先去看 thinking 是不是被截断了,再回头加 num_predict。
第五步:禁用思考模式
不需要思考链的场景,传 think: false,让 message.content 直接出答案。CLI 里对应 ollama run deepseek-r1:8b --hidethinking "..."。
requests.post(
"http://localhost:11434/api/chat",
json={
"model": "deepseek-r1:8b",
"messages": [...],
"think": False,
"stream": False,
},
)
禁用后 message.thinking 为空、message.content 直接是答案。客户端云 SDK 的字段拿法可以直接复用——只是要记得“Ollama 关掉时拿的是 content 字段,不存在 reasoning_content”。
字段名差异是协议差异,没有自动桥。客户端代码两套都写适配层,比改 Ollama / DeepSeek 一方的协议省事。
References(参考来源)
- Generate a chat message - Ollama 官方 API 文档ollama-docs
- Thinking - Ollama 官方能力文档ollama-docs
- Thinking Mode - DeepSeek API 官方文档deepseek-docs
