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
