AI 深度思考过程前端展示方法详解(SSE 双通道渲染)

展示 AI 深度思考过程的关键,是把模型的推理内容与最终回答拆成两条独立的数据流,通过 SSE 的命名事件分别下发,前端按事件类型分流渲染。我们在企业级 AI 网关项目中,后端用 event: reasoning 与 event: token 两个命名事件分别推送思考片段与正文片段,前端用可折叠面板承载思考内容、打字机效果承载正文,全程不打断、不阻塞,思考过程默认折叠、一键展开,既保留透明度又不干扰阅读。这个方案在后端只增加一个字段判断,前端只增加一个渲染分支,改动成本极低。

一、为什么需要把思考过程单独拎出来

1.1 双通道数据的天然差异

支持深度思考的模型(如 deepseek-reasoner、o 系列)在流式返回里会同时产出两类内容:reasoning_content(推理轨迹)和 content(最终答案)。两类内容在语义上完全独立,混在一起渲染会把界面变得难以阅读,也不利于历史记录压缩。

维度 reasoning_content content
内容性质 推理步骤、假设、自我校验 面向用户的最终结论
渲染样式 灰色背景、斜体、可折叠 正常正文、代码高亮
是否入库 不保存,节省 token 完整保存到会话历史
是否可被打断 可以,思考中可停止 不可,停止即弃

1.2 展示思考过程带来的三个收益

用户能看到 AI 是否理解了上下文,理解错了可以提前打断纠正;开发排查问题时能顺着推理轨迹定位模型行为异常;新开发者通过观察推理步骤,能反过来学习拆解需求的思路。这是企业级场景里”模型可信度”的一部分,比单纯的回答质量更早建立。

二、SSE 双通道事件的设计

2.1 后端如何区分两类内容

网关在转发模型流时,对每个 delta 做一次字段判断:存在 reasoning_content 就走思考事件,存在 content 就走正文事件。Spring AI 侧用 ChatResponse 的流式回调,通过 Flux 过滤后重新映射为自定义事件。

// 网关层将模型流拆分为两类 SSE 事件
flux.map(chunk -> {
    if (chunk.getReasoningContent() != null) {
        return new SseEvent("reasoning", chunk.getReasoningContent());
    }
    if (chunk.getOutput() != null) {
        return new SseEvent("token", chunk.getOutput());
    }
    return null;
})
.filter(Objects::nonNull)
.doOnNext(evt -> session.sendMessage(
    SseEmitter.event().name(evt.type()).data(evt.content())
));

思考内容只透传、不落库。多轮对话的历史里只保留最终答案,避免思考文本占满上下文窗口,也能防止内部推理信息在后续轮次中被再次投喂给模型。

2.2 前端如何解析命名事件

浏览器原生 EventSource 支持 addEventListener 监听命名事件,但如果想拿到更细粒度的控制(如超时中断、断线重连状态),用 fetch + ReadableStream 手写解析更稳。缓冲拆分按空行切分 SSE 帧,最后一帧不完整时留在 buffer 等下一次数据到达。

const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''

while (true) {
  const { value, done } = await reader.read()
  if (done) break
  buffer += decoder.decode(value, { stream: true })
  const frames = buffer.split('\n\n')
  buffer = frames.pop() ?? ''
  for (const frame of frames) {
    handleFrame(frame)
  }
}

function handleFrame(frame) {
  const event = frame.match(/^event: (.+)$/m)?.[1]
  const data = frame.match(/^data: (.+)$/m)?.[1]
  if (!data) return
  if (event === 'reasoning') {
    reasoningStore.append(JSON.parse(data).content)
  } else if (event === 'token') {
    answerStore.append(JSON.parse(data).content)
  }
}

三、前端渲染的四个处理要点

3.1 思考区与正文区分开管理

消息条目里维护两个独立的响应式状态:reasoningText 与 answerText。渲染时思考区用 v-show 控制折叠,正文区走 Markdown 渲染。折叠状态下只显示”已深度思考 n 秒”的摘要行,展开后展示完整轨迹。

3.2 打断时区分两类内容

停止生成时,思考区只保留已产出的部分,正文区标记”已中断”。两者互不覆盖,用户在思考阶段打断,不会得到半截正文。

3.3 性能兜底

深度思考模型的首段思考往往要几秒才出现,期间页面必须保持可交互。我们用骨架屏 + “思考中”动画占位,同时把思考区的 DOM 更新节流到每 50ms 一次,避免高频率 append 引起长任务卡顿。

3.4 网关层面的流式参数

  • 关闭响应压缩,避免流被整包缓冲;
  • 响应头带 Cache-Control: no-cache,禁止中间层缓存;
  • 反向代理侧关闭对 SSE 的缓冲(X-Accel-Buffering: no),否则事件会被攒到一大块才推到浏览器,打字机效果名存实亡。

四、前后端联调的检查清单

  1. 打开浏览器网络面板,确认事件帧按 reasoning / token 交替出现;
  2. 断网 10 秒后恢复,确认页面能提示连接中断而不白屏;
  3. 用思考型模型连续问答 5 轮,核对历史记录里没有混入推理文本;
  4. 在大模型思考期间点击停止,确认思考区内容保留、正文区正确标记中断。

常见问题(FAQ)

Q1:思考内容会占用上下文吗?

不会。网关只透传不落库,多轮对话历史仅保留最终答案。

Q2:EventSource 和 fetch 流式解析怎么选?

EventSource 简单但有断线重连无补偿;fetch 可控性强,适合需要中断与状态管理的场景。

Q3:思考过程展示会拖慢首字响应吗?

不会。思考与正文各走各的通道,正文首字仍按首包到达时间渲染。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
其AI的头像其AI普通用户

相关推荐

返回顶部