1964 字
约 6 分钟
7
Spring AI 对接 DeepSeek:实现推理过程流式输出

Spring AI 对接 DeepSeek:实现推理过程流式输出

普通的大模型接口,一般只返回最终答案。

但 DeepSeek 的推理模型除了最终回答之外,还可以额外返回一段推理内容

比如我们问:

一加一等于多少?

模型返回的数据,大致可以理解为:

推理过程:
用户询问一个简单数学问题,需要直接计算……

----------------

最终回答:
1 + 1 = 2

这一篇我们就来看看,在 Spring Boot + Spring AI 中,怎么把 DeepSeek 的推理过程也流式输出到浏览器。


一、实现思路

整个过程其实并不复杂。

DeepSeek 返回的数据里面,主要有两部分:

reasoning_content

表示模型的推理内容。

以及:

content

表示模型最终返回给用户的正式答案。

所以我们的核心逻辑就是:

用户发送问题
        ↓
Spring AI 调用 DeepSeek
        ↓
模型开始推理
        ↓
reasoning_content
        ↓
推理结束
        ↓
content
        ↓
最终答案

我们只需要在流式响应中把这两个字段分别取出来即可。


二、添加 commons-lang3

为了方便处理字符串,可以先引入 commons-lang3

pom.xml 中添加:

<properties>
    <commons-lang3.version>3.18.0</commons-lang3.version>
</properties>

然后添加依赖:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>${commons-lang3.version}</version>
</dependency>

刷新 Maven 即可。


三、创建推理接口

新建一个控制器:

DeepSeekR1ChatController

完整代码如下:

package com.quanxiaoha.ai.robot.controller;

import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

@RestController
@RequestMapping("/v1/ai")
public class DeepSeekR1ChatController {

    @Resource
    private DeepSeekChatModel chatModel;

    @GetMapping(
            value = "/generateStream",
            produces = "text/html;charset=utf-8"
    )
    public Flux<String> generateStream(
            @RequestParam(
                    value = "message",
                    defaultValue = "你是谁?"
            ) String message) {

        // 1. 指定本次请求使用的模型
        DeepSeekChatOptions chatOptions =
                DeepSeekChatOptions.builder()
                        .model(
                                DeepSeekApi.ChatModel
                                        .DEEPSEEK_V4_PRO
                                        .getValue()
                        )
                        .build();

        // 2. 创建 Prompt
        Prompt prompt = new Prompt(message, chatOptions);

        // 3. 标记是否已经输出过分割线
        AtomicBoolean needSeparator =
                new AtomicBoolean(true);

        // 4. 流式调用 DeepSeek
        return chatModel.stream(prompt)
                .mapNotNull(response -> {

                    DeepSeekAssistantMessage output =
                            (DeepSeekAssistantMessage)
                                    response
                                            .getResult()
                                            .getOutput();

                    // 推理内容
                    String reasoningContent =
                            output.getReasoningContent();

                    // 最终回答
                    String text =
                            output.getText();

                    boolean isAnswer = false;

                    String content;

                    if (Objects.isNull(text)) {

                        // 还在推理
                        content = reasoningContent;

                    } else {

                        // 开始输出最终答案
                        content = text;
                        isAnswer = true;
                    }

                    // 浏览器不能直接识别 \n,
                    // 所以替换成 <br>
                    if (StringUtils.isNotBlank(content)) {
                        content =
                                content.replace(
                                        "\n",
                                        "<br>"
                                );
                    }

                    // 第一次进入正式回答时,
                    // 插入分割线
                    if (isAnswer
                            && needSeparator
                            .compareAndSet(
                                    true,
                                    false
                            )) {

                        content =
                                "<hr>" + content;
                    }

                    return content;
                });
    }
}

看起来代码不少,但真正重要的其实只有几个地方。


四、指定推理模型

首先:

DeepSeekChatOptions chatOptions =
        DeepSeekChatOptions.builder()
                .model(
                    DeepSeekApi.ChatModel
                        .DEEPSEEK_V4_PRO
                        .getValue()
                )
                .build();

这里创建了一个本次请求使用的配置。

然后:

Prompt prompt =
        new Prompt(message, chatOptions);

把配置传给 Prompt

这样做最大的好处是:

不会修改整个项目的默认模型。

比如项目默认使用:

deepseek-v4-flash

普通聊天继续使用 Flash。

而只有这个推理接口使用:

deepseek-v4-pro

相当于:

普通聊天
    ↓
V4-Flash

复杂推理
    ↓
V4-Pro

不同接口可以选择不同模型。


五、获取推理内容

最关键的一段代码是:

DeepSeekAssistantMessage output =
        (DeepSeekAssistantMessage)
                response
                        .getResult()
                        .getOutput();

为什么要转成:

DeepSeekAssistantMessage

因为普通的:

AssistantMessage

主要只能获取最终回答。

而 DeepSeek 还额外提供了:

reasoning_content

Spring AI 使用:

DeepSeekAssistantMessage

来接收这些 DeepSeek 专属字段。

所以我们就可以调用:

output.getReasoningContent();

获取推理内容:

String reasoningContent =
        output.getReasoningContent();

同时通过:

String text =
        output.getText();

获取最终答案。

可以简单理解成:

getReasoningContent()

负责拿:

模型正在想什么

而:

getText()

负责拿:

模型最后回答什么

六、区分推理和最终答案

DeepSeek 流式返回时,一般会经历两个阶段。

第一阶段:

reasoningContent 有内容
text 没内容

说明:

模型正在推理

第二阶段:

reasoningContent 没内容
text 有内容

说明:

模型开始输出最终答案

所以代码中进行了判断:

if (Objects.isNull(text)) {

    content = reasoningContent;

} else {

    content = text;
    isAnswer = true;
}

逻辑非常简单:

text == null
    ↓
输出推理过程

text != null
    ↓
输出最终回答

七、为什么要把 \n 换成

模型返回的内容里面可能包含:

\n

例如:

第一步\n第二步\n第三步

但是浏览器直接渲染 HTML 时,并不会自动把:

\n

显示成换行。

所以:

content.replace("\n", "<br>");

把它转换成:

第一步<br>
第二步<br>
第三步

这样浏览器才能正确显示换行。


八、给推理过程和答案加一条分割线

如果直接输出:

推理内容
最终答案

用户不容易看出来哪里是推理,哪里是正式答案。

所以可以在正式回答开始的时候加:

<hr>

最终效果:

推理过程……

推理过程……

------------------------

最终答案……

但是这里有个问题。

因为是流式响应,最终答案也是一小段、一小段返回的。

比如:

然后:

然后:

如果我们每收到一次正式答案都加:

<hr>

最后就会变成:

----------------
答
----------------
案
----------------
是

显然不对。

因此定义一个:

AtomicBoolean needSeparator =
        new AtomicBoolean(true);

第一次进入最终答案时执行:

needSeparator.compareAndSet(
        true,
        false
)

第一次执行:

true → false

返回:

true

于是插入:

<hr>

后面的正式回答再次执行时,因为已经是:

false

所以不会再次插入。

最终保证:

每次请求只添加一次分割线。


九、测试接口

启动 Spring Boot 项目。

浏览器访问:

http://localhost:8080/v1/ai/generateStream?message=一加一等于多少

此时浏览器就会看到模型内容一点一点输出。

整体效果类似:

用户询问一个简单的数学计算问题。

需要计算 1 + 1。

结果为 2。

--------------------------------

1 + 1 = 2。

这样,一个最基础的 DeepSeek 推理过程流式输出接口 就实现完成了。


十、核心代码再总结一下

整个功能其实只需要记住三个知识点。

第一,使用:

DeepSeekChatOptions

可以针对某一次请求单独指定模型:

DeepSeekChatOptions.builder()
        .model(...)
        .build();

第二,通过:

DeepSeekAssistantMessage

拿到 DeepSeek 特有的推理内容:

output.getReasoningContent();

以及最终答案:

output.getText();

第三,根据两者的状态判断当前模型是在推理还是在输出最终答案:

reasoningContent
      ↓
推理过程

text
      ↓
最终答案

完整的数据流就是:

浏览器提问
    ↓
Controller
    ↓
创建 Prompt
    ↓
DeepSeekChatModel.stream()
    ↓
DeepSeek
    ↓
reasoning_content
    ↓
页面流式显示推理内容
    ↓
<hr>
    ↓
content
    ↓
页面流式显示最终答案

到这里,我们就完成了:

Spring Boot + Spring AI + DeepSeek 推理模型的流式调用。

后续如果要做真正的 AI 聊天页面,还可以继续在这个基础上加入:

ChatClient
        ↓
ChatMemory
        ↓
多轮上下文
        ↓
Markdown 渲染
        ↓
Vue / React 聊天页面
        ↓
SSE 流式输出

这样就可以一步一步做出类似 ChatGPT、DeepSeek 官网的聊天效果。

Spring AI 对接 DeepSeek:实现推理过程流式输出
http://www.clxhxhhr.top/posts/610/
作者
clxstart
发布于
2026-09-14
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。