StackTraces
DeepSeek已驗證

把 DeepSeek 雲 API 程式碼接到 Ollama 後 reasoning_content 字段是空

@debug_master11 天前更新閱讀 6 min00

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 openai SDK 或裸 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.thinking and message.content. Generate returns thinking and response.

DeepSeek 官方文檔原文:

In thinking mode, the chain-of-thought content is returned via the reasoning_content parameter, at the same level as content.

字段名在兩個 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(參考來源)

最後更新於 2026-09-30

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法