做 Dify 应用接深度思考模型的时候,很多人都会卡在同一个地方:模型明明已经“想”了一大段,可前端拿到手的始终只有最终答案,那一大段关键的 reasoning_content 就像被中间层偷偷丢掉了一样。我在 Dify 1.11.4 社区版上踩完这个坑,把配置 LLM 深度思考、拿到 reasoning_content 并支持前端渲染的完整链路整理了出来,这篇就把它讲透。
这篇文章适合正在用 Dify 搭建对话应用、工作流应用,并且希望把模型的思考过程呈现给终端用户的开发者。我会按照“方案选型 -> Dify 侧配置 -> 前端渲染 -> 问题排查”四个部分来写,每一步都会给出可以直接落地的配置和代码,尽量少说废话。
1. 方案选型:获取 reasoning_content 的三条路线
1.1 先搞清楚深度思考模型的输出结构
深度思考类模型(比如 DeepSeek-Reasoner、Qwen 的 reasoner 系列、Kimi 思考版)的响应体里,核心内容通常被拆成两个字段:
reasoning_content:模型内部的思考过程,也就是我们平时说的“深度思考”内容;content:模型给出的最终可展示答案。
两者在同一个 API 响应里返回,但在 Dify 的默认处理流程中,reasoning_content并不会自动暴露给前端。原因很简单:Dify 的对话协议里没有专门承载这个字段的通道,它的answer字段只绑定最终 content。很多用户查了半天日志,发现模型节点里其实有这个字段,但不是被丢弃,就是保存在了不好取的地方。
另外要注意,不同模型厂商对思考过程的处理方式不一样。有的是独立字段;有的是直接拼在 content 里,用特定标记包裹,比如早期的...let me think...格式。你在动手配置前,最好先用官方 API 文档确认一下自己模型的输出结构,后面所有方案都依赖这一步。
1.2 路线对比:透传、HTTP 直连、自建代理
我实际验证下来,想把reasoning_content从 Dify 里安全地拿到前端,基本有三条路可以走:
路线 A:让 Dify 模型层透传思考内容。Dify 在 1.11.x 版本的模型供应商配置里,对 OpenAI-API-compatible 类的兼容供应商增加了“启用推理内容”的开关。开启后,Dify 会尝试把模型响应里的reasoning_content解析并保留到 LLM 节点的输出变量中,多轮对话时也能尽量把它回传给上游模型。这条路线对模型供应商有要求:必须是走 OpenAI 兼容协议的推理模型,而且你的模型供应商地址本身不能把该字段过滤掉。
路线 B:在工作流里用 HTTP 请求节点直接调用模型 API。如果模型比较特殊,或者 Dify 就是没透传,那就绕过 Dify 的模型管理,直接在编排里加一个 HTTP 请求节点,把用户问题发给模型 API,再从响应体里手动拆出reasoning_content和content。这种方案最灵活,适应任何模型,但你要自己管 API Key,多轮上下文也得自己拼接,适合做单轮分析、一次性问答类应用。
路线 C:自建一个后端代理。生产环境下我更推荐这条:前端不直接对接 Dify,而是接你自己的后端服务;后端统一负责调用 Dify 工作流或模型 API,解析出reasoning_content后,通过 SSE 把“思考中”和“回答中”两个通道分别推给前端。代价是要多写一套后端逻辑,但可维护性和可控性是最好的。
我在这次 1.11.4 实测里,同时用了路线 A 和路线 B,下面分别说明操作细节。你可以根据自己的场景选,不一定非要上自建代理。
2. Dify 1.11.4 配置 LLM 深度思考的实操步骤
2.1 模型供应商侧:启用推理内容支持
先说路线 A 的标准操作。登录 Dify 管理后台,进入“设置 -> 模型供应商”,添加一个新的 OpenAI-API-compatible 供应商。需要填的内容包括 API Base 地址、API Key、模型名称,比如deepseek-reasoner或者qwen3-reasoner,这些按你自己的模型服务填就行。
关键的一步在高级配置里:找到 “Enable Reasoning Content” 或者叫“支持推理内容”的开关,必须打开。Dify 1.11.4 的社区版里,这个开关在你新增兼容供应商的时候就能看到。如果没有这个选项,通常是供应商配置类型不对,或者版本较老,可以先升级再试,或者直接跳到 2.2 的 HTTP 节点兜底方案。
保存之后,到应用编排里选择刚才配置的模型。这里有两个地方会影响最终效果:
- 模型参数:比如 temperature、max_tokens,建议把 max_tokens 调高一点,因为思考过程和最终答案都在这同一个模型里产出,token 不够容易被截断。
- 调试运行:在右上角“运行”窗口发起一次测试,然后查看 LLM 节点的输出明细。如果配置成功,你会看到输出里出现
reasoning_content字段;如果看不到,就要检查模型供应商是否真的返回了这个字段。
有个容易忽略的点:如果你的模型走的是自建网关或第三方中转,网关可能会主动移除reasoning_content,即使源模型返回了。这种情况其实很常见,建议先在网关层打开字段透传,或者在模型响应日志里确认字段确实存在,再回过头排查 Dify 配置。
2.2 工作流里把思考内容变成结构化输出
Dify 的chat-messages接口标准事件流里没有专门放reasoning_content的通道,所以我更推荐用工作流的方式,把思考内容变成一个显式的输出变量。这个方案不依赖自建后端,前端拿到的是一个干净的结构化 JSON。
具体做法是在工作流里,LLM 节点成功返回后接一个“代码执行”节点,用 Python 把 reasoning 和 answer 拼成一个 JSON 字符串。示例代码:
import json def main(thinking: str, answer: str) -> dict: return { "result": json.dumps( {"thinking": thinking, "answer": answer}, ensure_ascii=False ) }代码节点的输入变量thinking和answer分别取自上游 LLM 节点的输出。如果你的 LLM 节点确实没有暴露reasoning_content,那就在上游再加一个 HTTP 请求节点,自己调用模型 API 获取,并把响应体里的两个字段分别存入变量即可。下面是一个 HTTP 请求节点的配置参考:
请求方法: POST URL: https://api.deepseek.com/chat/completions 请求头: Authorization: Bearer sk-xxxx Content-Type: application/json 请求体: { "model": "deepseek-reasoner", "messages": [ {"role": "user", "content": "{{#sys.query#}}"} ], "stream": false }注意这里stream设置为 false,是为了让响应一次性返回,HTTP 节点才好在后续步骤里取值。响应体中的choices[0].message.reasoning_content和choices[0].message.content分别就是思考内容和最终答案。
拿到这两个值之后,再通过代码节点包装成 JSON 字符串,放到工作流的“输出变量”里。前端调用workflows/run接口时,就可以在返回结果的outputs字段中直接取到类似{"thinking": "...", "answer": "..."}的数据。
这一步是整篇文章的核心,也是最容易被忽略的关键点:Dify 能力本身不缺,缺的是把reasoning_content放到正确传递通道里的思路。
2.3 thinking 模式下必须回传 reasoning_content,否则报 400
这是我在实践里踩得最深的一个坑。现在部分模型 API(特别是 DeepSeek)对思考模式有硬性要求:当上下文里存在上一轮 assistant 的消息,并且该消息带有reasoning_content字段时,下一轮请求必须把原来的 reasoning 内容原样回传。如果你在拼接多轮对话时把这个字段丢了,API 会直接抛出类似下面的错误:
the `reasoning_content` in the thinking mode must be passed back to the api.这个问题的根源是模型厂商为了保证思维链的连贯性,要求思考内容随上下文一起提交。OpenAI 的规范里没有这个要求,所以很多人第一次遇到会懵。
如果你用的是 Dify 原生的模型管理来做多轮对话,这个问题 Dify 内部通常会处理好;但如果你按 2.2 的路线 B 自己用 HTTP 请求节点拼消息,就一定要把上一轮 assistant 消息里的reasoning_content缓存到会话变量里,并在下一轮请求时放回 messages 数组的 assistant 消息中。简单来说,多轮上下文结构应该是:
{ "model": "deepseek-reasoner", "messages": [ {"role": "user", "content": "第一轮问题"}, { "role": "assistant", "content": "第一轮最终答案", "reasoning_content": "第一轮思考内容" }, {"role": "user", "content": "第二轮问题"} ] }如果不缓存、不传回,等待你的就是 400。
2.4 用命令行验证模型到底返回了什么
在配置过程中,我建议你先用一条 curl 直接请求模型 API,确认输出结构,这样能快速把问题定位在模型侧还是 Dify 侧。命令示例:
curl https://api.deepseek.com/chat/completions \ -H "Authorization: Bearer $DEEPSEEK_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"deepseek-reasoner","messages":[{"role":"user","content":"请解释一下什么是递归"}],"stream":false}'返回体里找choices[0].message,你会看到reasoning_content和content两个字段。如果这条命令返回正常,说明模型 API 没问题,后面就是 Dify 配置和通道的问题;如果这条命令本身就看不到 reasoning 字段,那要检查的则是模型选择或者账户权限。
这个验证步骤不到一分钟,但能节省后面排查问题的绝大部分时间。
3. 前端渲染 reasoning_content 的落地细节
3.1 流式协议设计:把 thinking 和 answer 分成两个通道
当思考内容比较长的时候,用户等待答案的过程可能长达十几秒。如果一次性等全部返回再渲染,体验非常糟糕,所以生产环境一般会走流式。我推荐的自建后端代理输出 SSE 协议格式如下:
event: thinking data: {"content":"正在分析问题的背景..."} event: answer data: {"content":"这是一个典型的递归问题..."} event: done data: {}前面说自己处理多轮上下文比较麻烦,但如果你的场景只是单轮问答加深度思考展示,那么后端代理只需要做三件事:接收前端请求、调用 Dify 工作流或模型 API、把解析后的字段分别塞进 SSE 事件。前端也不需要关心 Dify 协议的细节。
如果你不想写后端,那么建议用 2.2 的方式,通过workflows/run拿到 JSON 后一次性渲染。到底选哪种,核心看你对首字延迟的容忍度:能等,就简单化;不能等,就上流式。
3.2 前端思考面板组件:默认折叠,支持展开
拿到 thinking 内容后,前端界面最好不要把它和最终答案平铺展示。思考过程往往又长又杂,用户真正需要的是“知道模型想过”,而不是必须读完。我的建议是设计一个可折叠面板:
- 思考过程中:面板显示“正在思考…”状态,并实时展示增量文本;
- 思考结束后:面板自动折叠,只显示“已展示思考过程”和耗时;
- 用户点击展开:加载并渲染完整的 Markdown 内容。
一个 React 版本的简化实现如下:
import { useState } from "react"; import ReactMarkdown from "react-markdown"; function ReasoningPanel({ content, status }) { const [collapsed, setCollapsed] = useState(true); return ( <div className="reasoning-panel"> <div className="reasoning-header" onClick={() => setCollapsed((v) => !v)} > <span>{status === "thinking" ? "正在思考…" : "已展示思考过程"}</span> </div> {!collapsed && ( <div className="reasoning-content"> <ReactMarkdown>{content}</ReactMarkdown> </div> )} </div> ); }如果思考内容是以流式片段到达的,建议在父组件里维护一个累积字符串,每次新增片段只更新 state,不要用重新赋值覆盖。渲染层要做增量追加而不是整块替换,否则长文本场景下会明显卡顿。
3.3 Markdown 安全与长文本性能优化
reasoning_content是模型自由生成的文本,里面可能出现 Markdown、代码块、数学公式甚至 HTML 片段。为了避免 XSS 隐患,不要直接用dangerouslySetInnerHTML去渲染。我这边用的是 ReactMarkdown 配合 rehype 生态做代码高亮和公式支持,默认转义机制对安全更友好。
长文本性能方面,有几个实测有效的做法:
- 设置一个阈值,比如内容大于 2000 字符时,默认只渲染前后各 500 字符,中间用“点击展开查看完整思考过程”代替,避免初始渲染卡顿;
- 代码块高亮放在懒加载逻辑里,用户展开面板后再初始化高亮;
- 思考过程中只是实时更新文本节点,不要每次都走 Markdown 全量解析,等思考结束后再渲染正式视图。
这些细节看起来小,但思考内容往往是普通答案的好几倍长度,不做优化,页面会明显吃帧。
4. 常见问题排查与避坑速查
4.1 模型日志里有 reasoning_content,但前端拿不到
这个问题出现频率最高。先确认你的前端是走chat-messages还是workflows/run。如果走的是chat-messages,标准事件流里确实没有对应字段,answer只包含最终回答。解决办法有两个:切换成工作流输出变量的方式,或者自建后端代理解析后重新推送。如果你还想继续用chat-messages,那你必须自己做一轮中间层转换,把 Dify 的流式响应拿到后端解析,再向前端转发自定义事件。
4.2 多轮对话报 400:reasoning_content must be passed back to the api
其实就是 2.3 节讲的回传问题。排查步骤:先检查请求体里上一轮 assistant 消息是否带了reasoning_content;再检查是不是自己在上游 HTTP 节点里只在第一次请求时获取了它,后续轮次没有从会话变量里取出来拼接。解决方式是在会话结构里用一个字段专门缓存最近一轮的 reasoning 内容,每次请求前拼进去。
4.3 思考内容出现乱码或者换行丢失
大部分情况是 SSE 解析里对 JSON 字符串的转义处理不完整。当reasoning_content中包含换行符时,后端在拼接事件数据时必须用JSON.stringify序列化,不能直接手工拼字符串。前端解析时,也不要用简单split('\n')就结束,要按 SSE 规范处理data:前缀,并在JSON.parse前去掉多余的空白字符。
4.4 一个容易被忽略的体验问题
思考内容和最终答案属于两种不同的信息形态,交互设计上建议明确区分:思考过程可以用浅色背景、折叠样式、非等宽字体,答案用正常阅读布局。不要把两者混在同一个气泡里。用户如果只需要答案,折叠状态不会打扰他;如果他想验证模型逻辑,展开就能看到完整推理链路。这个交互细节做得好,整个应用的“专业感”会明显不一样。
最后再说一个我个人的体会:把 reasoning_content 真正跑通之后,最大的收获不是多了一个字段,而是整个应用的可解释性变强了。用户能看到模型先分析、再回答,信任感会高很多。遇到问题不要急着怪 Dify,先按照“模型接口验证 -> Dify 配置验证 -> 前端通道验证”的顺序排查,大部分坑都能快速定位。这套链路在你后续接入更多推理模型时也能复用,值得认真跑一遍。