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

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法