众所周知,DeepSeek 能够火出圈,除了回答质量非常高,另外一个亮点就是 —— 其推理能力,当时刚出来的时候,可以说是惊艳的很,上手感受下来,可以说是秒杀 OpenAI 的产品。

本文中,我们就来通过 Spring AI 对接官方 DeepSeek-R1 模型,实现一下推理效果!

修改模型

编辑 application.yml 配置文件,将模型名称修改为 deepseek-reasoner , 这表示使用 DeepSeek-R1 模型,而不是 V3 模型:
相关配置可以看上一篇文章:Spring AI:对接DeepSeek实战

spring:
  ai:
    deepseek:
      // 省略...
      chat:
        options:
          model: deepseek-reasoner # 使用哪个模型
		 // 省略...

修改完毕后,重启项目。打开浏览器,请求地址 http://localhost:8080/ai/generateStream?message=一加一等于多少 , 看看流式响应是否有推理效果。

你会发现并没有输出推理的过程,而是直接给出了回答。

添加工具类依赖

为了能够实现推理效果,我们需要重构一下后端代码。首先,编辑 pom.xml 文件,添加 commons-lang3 工具类,等会需要用到,如下:

  <properties>
        // 省略...
        <!-- 常用工具类 -->
        <commons-lang3.version>3.12.0</commons-lang3.version>
    </properties>
    
    // 省略...
    
    <dependencies>
        // 省略...

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

        // 省略...
    </dependencies>

依赖添加完毕后,刷新一下 Maven, 将依赖下载到本地 Maven 仓库中。

新增 Controller

接着,在 /controller 包下,单独新建 DeepSeekR1ChatController 控制器,用于测试 R1 模型,防止需要修改上文的接口:

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

    @Resource
    private DeepSeekChatModel chatModel;

    /**
     * 流式对话
     * @param message
     * @return
     */
    @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
        // 构建提示词
        Prompt prompt = new Prompt(new UserMessage(message));

        // 流式输出
        return chatModel.stream(prompt)
                .mapNotNull(chatResponse -> {
                    // 获取响应内容
                    DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) chatResponse.getResult().getOutput();
                    // 推理内容
                    String reasoningContent = deepSeekAssistantMessage.getReasoningContent();
                    // 推理结束后的正式回答
                    String text = deepSeekAssistantMessage.getText();

                    // 若推理内容有值,则响应推理内容,否则,说明推理结束了,响应正式回答
                    return StringUtils.isNotBlank(reasoningContent) ? reasoningContent : text;
                });



    }
}

代码大体上一致,说一下核心修改的地方:

  • 为了区别接口地址,防止和之前的冲突,在接口前面添加 /v1 , 表示 v1 新版本;
  • 修改 chatModel.stream() 流式输出逻辑:
    将 getOutput() 返回值转换成专属 DeepSeek 消息对象 DeepSeekAssistantMessage ;
    通过 getReasoningContent() 方法,能够获取到推理内容;
    通过 getText() 方法,能够获取推理完毕后的回答内容;
    注意,推理内容和回答不会同时有值,所以,返参的时候判空,响应不为空的那个即可;

重启项目,浏览器请求地址 http://localhost:8080/v1/ai/generateStream?message=一加一等于多少,看看是否包含了推理过程。

添加换行效果

目前响应的内容,都黏在一起了,也没有个换行效果,阅读起来很费劲,能不能完善一下呢?通过 Debug 调试,我们会发现,其实流式响应中,是返回了换行符的,不过是 /n/n 的形式,如下图所示,无法被浏览器识别到:

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

    @Resource
    private DeepSeekChatModel chatModel;

    /**
     * 流式对话
     * @param message
     * @return
     */
    @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
        // 构建提示词
        Prompt prompt = new Prompt(new UserMessage(message));

        // 流式输出
        return chatModel.stream(prompt)
                .mapNotNull(chatResponse -> {
                    // 获取响应内容
                    DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) chatResponse.getResult().getOutput();
                    // 推理内容
                    String reasoningContent = deepSeekAssistantMessage.getReasoningContent();
                    // 推理结束后的正式回答
                    String text = deepSeekAssistantMessage.getText();

                    // 若推理内容有值,则响应推理内容,否则,说明推理结束了,响应正式回答
                    String rawContent = StringUtils.isNotBlank(reasoningContent) ? reasoningContent : text;

                    // 将 \n 替换为 HTML 换行标签 <br>
                    return StringUtils.isNotBlank(rawContent) ? rawContent.replace("\n", "<br>") : rawContent;
                });
    }
}

重启项目。

将推理过程和正式回答分隔开

虽然是换行了,但是推理过程和正式的回答,没有区分开来,能否再改善一下,通过分割线分隔一下呢,看的更明了一些?继续修改代码如下:

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

    @Resource
    private DeepSeekChatModel chatModel;

    /**
     * 流式对话
     * @param message
     * @return
     */
    @GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
    public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
        // 构建提示词
        Prompt prompt = new Prompt(new UserMessage(message));

        // 使用原子布尔值跟踪分隔线状态(每个请求独立)
        AtomicBoolean needSeparator = new AtomicBoolean(true);

        // 流式输出
        return chatModel.stream(prompt)
                .mapNotNull(chatResponse -> {
                    // 获取响应内容
                    DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) chatResponse.getResult().getOutput();
                    // 推理内容
                    String reasoningContent = deepSeekAssistantMessage.getReasoningContent();
                    // 推理结束后的正式回答
                    String text = deepSeekAssistantMessage.getText();

                    // 是否是正式回答
                    boolean isTextResponse = false;
                    // 若推理内容有值,则响应推理内容,否则,说明推理结束了,响应正式回答
                    String rawContent;
                    if (Objects.isNull(text)) {
                        rawContent = reasoningContent;
                    } else {
                        rawContent = text;
                        isTextResponse = true; // 标记为正式回答
                    }

                    // 处理换行
                    String processed = StringUtils.isNotBlank(rawContent) ? rawContent.replace("\n", "<br>") : rawContent;

                    // 在正式回答内容之前,添加一个分隔线
                    if (isTextResponse
                            && needSeparator.compareAndSet(true, false)) {
                        processed = "<hr>" + processed; // 使用 HTML 的 <hr> 标签实现
                    }

                    return processed;
                });
    }
}

解释一下上述代码中,核心修改的地方:

  • 定义一个 AtomicBoolean 变量 needSeparator, 使用原子布尔值跟踪分隔线状态(每个请求独立),默认值为 true, 表示需要分隔;
  • 修改 chatModel.stream() 流式输出逻辑:
    定义一个 boolean isTextResponse 布尔变量,用于标识当前响应的内容,是否是正式的回答,若不是,则表示还是推理内容;
    在响应数据之前,判断 isTextResponse 是否为 true , 若是,则表示是正式回答,若同时满足 needSeparator 为 true , 说明推理结束了,则 将 needSeparator 设置为 false , 避免下次响应依然添加分割线。
    最后,在响应数据的前面,拼接 hr 分割线标签;

再次重启项目,测试一波接口。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐