教你使用SpringAI集成DeepSeek构建聊天机器人

173 阅读7分钟

1. 环境准备

  • JDK版本:17 或更高
  • Spring Boot版本:3.2 或更高(我使用的是3.4.4)
  • 构建工具:Maven
  • DeepSeek API Key:在DeepSeek官网注册并获取 API Key

2.依赖准备

使用openAI的客户端进行集成Deepseek.DeepSeek, 模型与 OpenAI API 完全兼容,可以使用任何 OpenAI 客户端或库进行访问。

首先,在项目的 pom.xml 文件中添加 Spring AI 的 OpenAI Starter 依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
    <version>1.0.0-M6</version>
</dependency>

在 pom.xml 中添加 Spring Milestones Repository:

<repositories>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

最终的pom文件如图所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lcw.mcp</groupId>
    <artifactId>spring-mcp-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-mcp-test</name>
    <description>spring-mcp-test</description>
    <url/>
    <licenses>
        <license/>
    </licenses>
    <developers>
        <developer/>
    </developers>
    <scm>
        <connection/>
        <developerConnection/>
        <tag/>
        <url/>
    </scm>
    <properties>
        <java.version>17</java.version>
        <spring-ai.version>1.0.0-M6</spring-ai.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework.ai</groupId>-->
<!--            <artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>-->
<!--        </dependency>-->
<!--        <dependency>-->
<!--            <groupId>org.springframework.ai</groupId>-->
<!--            <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>-->
<!--        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-actuator</artifactId>-->
<!--        </dependency>-->
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>

依赖引入完成后,我们进行构建yaml文件或者application配置文件。

spring:
  ai:
    openai:
      api-key: 你自己上面生产的apikey
      chat:
        options:
          model: deepseek-chat
      base-url: https://api.deepseek.com
      embedding:
        enabled: false

这里我们指定了 DeepSeek API 的 base URL 并禁用了 embedding(嵌入),因为 DeepSeek 目前还没有提供任何与 embedding 兼容的模型。

上述配置完成后,运行时,springAI会自动生成一个 ChatModel 类型的 Bean,允许我们与指定的模型交互。接下来,我们将使用它为聊天机器人定义其他几个 Bean。这里我们先不关注这个Bean如何使用,后面我会单独有个文章分析整个Bean的一致执行过程。 完成必要的配置后,我们开始进行相应的bean的编写

3、构建聊天机器人

首先,编写ChatConfiguration类,该类中加载chat机器人必要的bean对象

package com.lcw.mcp.springmcptest.chat;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ChatConfiguration {
    /**
     * 我们使用 InMemoryChatMemory
     * 实现定义了一个 ChatMemory Bean,它将聊天历史记录存储在内存中,以保持对话上下文。
     * @return
     */
    @Bean
    ChatMemory chatMemory() {
        return new InMemoryChatMemory();
    }

    /**
     * 使用 ChatModel
     * 和 ChatMemory Bean
     * 创建一个 ChatClient Bean。
     * ChatClient 类是与我们配置的 DeepSeek 模型交互的主要入口。
     * @param chatModel
     * @param chatMemory
     * @return
     */
    @Bean
    ChatClient chatClient(ChatModel chatModel, ChatMemory chatMemory) {
        return ChatClient
                .builder(chatModel)
                .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory))
                .build();
    }
}

然后我们创建一个自定义 StructuredOutputConverter,因为DeepSeek-R1 模型的响应包括其 CoT,我们得到的响应格式如下:

<think>
Chain of Thought
</think>
Answer

所以我们需要解析cot以及问题的答案,这里我们创建自己的自定义结构化输出转换器(StructuredOutputConverter)实现:

public record DeepSeekModelResponse(String chainOfThought, String answer) {

}
package com.lcw.mcp.springmcptest.chat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.converter.StructuredOutputConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;

public class DeepSeekModelOutputConverter implements StructuredOutputConverter<DeepSeekModelResponse> {
    private static final Logger logger = LoggerFactory.getLogger(DeepSeekModelOutputConverter.class);
    private static final String OPENING_THINK_TAG = "<think>";
    private static final String CLOSING_THINK_TAG = "</think>";

    @Override
    public DeepSeekModelResponse convert(@NonNull String text) {
        if (!StringUtils.hasText(text)) {
            throw new IllegalArgumentException("Text cannot be blank");
        }
        // 查找 <think> 和 </think> 标签的位置
        int openingThinkTagIndex = text.indexOf(OPENING_THINK_TAG);
        int closingThinkTagIndex = text.indexOf(CLOSING_THINK_TAG);
        // 如果找到了完整的 <think> 标签对
        if (openingThinkTagIndex != -1 && closingThinkTagIndex != -1 && closingThinkTagIndex > openingThinkTagIndex) {
            // 提取 <think> 标签内的内容
            String chainOfThought = text.substring(openingThinkTagIndex + OPENING_THINK_TAG.length(), closingThinkTagIndex);
            // 提取 </think> 标签之后的内容

            String answer = text.substring(closingThinkTagIndex + CLOSING_THINK_TAG.length());
            return new DeepSeekModelResponse(chainOfThought, answer);
        } else {
            logger.debug("No <think> tags found in the response. Treating entire text as answer.");
            return new DeepSeekModelResponse(null, text);
        }
    }

    @Override
    public <U> Converter<String, U> andThen(Converter<? super DeepSeekModelResponse, ? extends U> after) {
        return StructuredOutputConverter.super.andThen(after);
    }

    @Override
    public String getFormat() {
        return "";
    }
}

这样我们的就可以从 AI 模型的响应中提取思维链和答案,并将它们作为 DeepSeekModelResponse返回。

接下来我们实现Service层:

@Service
public class ChatbotService {
    @Autowired
    private ChatClient chatClient;

    public ChatResponse chat(ChatRequest chatRequest) {
        //是否可以获取到请求的id,如果没有,则创建一个新的id
        UUID chatId = Optional
                .ofNullable(chatRequest.chatId())
                .orElse(UUID.randomUUID());
        DeepSeekModelResponse response = chatClient
                .prompt()
                .user(chatRequest.question())
                .advisors(advisorSpec ->
                        advisorSpec
                                .param("chat_memory_conversation_id", chatId))
                .call()
                .entity(new DeepSeekModelOutputConverter());
        return new ChatResponse(chatId, response.chainOfThought(), response.answer());
    }
}

上述功能为如果请求中包含CharId,我们就使用原有的chatID,表示用户继续一个原有的对话;如果请求中没有chatId,我们就生成一个新的chatId,表示用户开始新的对话。 然后,我们将question(我们请求的问题)给到chatClient,并且将设置聊天会话id:chat_memory_conversation_id,用来维护聊条对话的历史记录。 最后,我们将自定义的DeepSeekModelOutputConverter传递给entity方法,这样AI模型的相应就可以解析为我们需要的记录。然后我们从中提取我们需要的chainOfThought和answer,并且将chatId一并写响应中。

最后,我们创建一个Controller层的REST请求,便于我们请求机器人接口

package com.lcw.mcp.springmcptest.chat.controller;

import com.lcw.mcp.springmcptest.chat.ChatRequest;

import com.lcw.mcp.springmcptest.chat.ChatResponse;
import com.lcw.mcp.springmcptest.chat.ChatbotService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {
    @Autowired
    private ChatbotService chatbotService;

    @PostMapping("/chat")
    ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest chatRequest) {
        ChatResponse chatResponse = chatbotService.chat(chatRequest);
        return ResponseEntity.ok(chatResponse);
    }
}

这样我们就集成好了一个聊天机器人,接下来我们发送请求:

POST http://localhost:8080/chat
Content-Type: application/json

{
  "chatId": "",
  "question": "redis如何进行主从同步"
}

答案如下:

{
  "chatId": "c35371f4-b9fb-4d75-9eae-c6cc1ee8a939",
  "chainOfThought": null,
  "answer": "Redis 的主从同步(Replication)是通过异步复制实现的,主要分为 **全量同步** 和 **增量同步** 两个阶段。以下是详细流程:\n\n---\n\n### **1. 建立主从关系**\n- 从节点(Slave)启动后,通过 `REPLICAOF <master-ip> <master-port>` 命令(或配置文件中设置)连接到主节点(Master)。\n- 从节点保存主节点的地址和端口,并建立连接。\n\n---\n\n### **2. 全量同步(Full Resynchronization)**\n**触发条件**:\n- 从节点首次连接主节点。\n- 从节点保存的主节点 `run_id`(唯一标识)与当前主节点不一致(例如主节点重启)。\n- 从节点的复制偏移量(`offset`)不在主节点的复制积压缓冲区(`repl_backlog_buffer`)中。\n\n**流程**:\n1. **从节点**发送 `PSYNC` 命令(旧版本用 `SYNC`)请求同步。\n2. **主节点**收到请求后:\n   - 生成当前数据的 **RDB 快照**(通过 `bgsave` 后台进程)。\n   - 将 RDB 文件发送给从节点,同时 **缓存期间的写命令** 到 `repl_backlog_buffer`。\n3. **从节点**接收 RDB 后:\n   - 清空自身数据,加载 RDB 恢复快照状态。\n   - 再执行主节点缓存的增量命令(通过 `repl_backlog_buffer`)。\n\n---\n\n### **3. 增量同步(Partial Resynchronization)**\n**触发条件**:\n- 主从连接短暂中断后恢复,且从节点的 `offset` 仍在主节点的 `repl_backlog_buffer` 范围内。\n\n**流程**:\n1. 主节点根据从节点发送的 `offset`,从 `repl_backlog_buffer` 中提取中断期间的写命令。\n2. 通过 **复制流(Replication Stream)** 发送这些命令给从节点。\n3. 从节点执行这些命令,保持与主节点数据一致。\n\n---\n\n### **4. 持续同步**\n- 全量或增量同步完成后,主节点会将 **所有新写入命令** 异步发送给从节点(通过 TCP 长连接)。\n- 从节点接收并执行这些命令,实时保持数据同步。\n\n---\n\n### **关键概念**\n- **run_id**:主节点的唯一标识,重启后会变化。从节点通过比较 `run_id` 判断是否需要全量同步。\n- **复制偏移量(offset)**:主从节点各自维护一个偏移量,标识同步进度。\n- **复制积压缓冲区(repl_backlog_buffer)**:主节点的环形缓冲区,默认大小 1MB,存储最近的写命令。若从节点 `offset` 仍在缓冲区范围内,可触发增量同步。\n\n---\n\n### **配置优化建议**\n1. **增大 `repl_backlog_size`**:避免网络抖动后全量同步(例如设置为 `100MB`)。\n2. **合理设置 `repl-timeout`**:防止超时误判(默认 60 秒)。\n3. **无盘复制(Redis 4.0+)**:主节点直接通过 socket 发送 RDB,避免落盘(`repl-diskless-sync yes`)。\n\n---\n\n### **注意事项**\n- **异步复制**:主节点不会等待从节点确认,因此极端情况下可能丢失数据(需依赖 `WAIT` 命令或集群方案如 Redis Sentinel/Cluster 保证一致性)。\n- **从节点只读**:默认配置下,从节点拒绝写入(可通过 `replica-read-only no` 修改,但通常不建议)。\n\n通过这种机制,Redis 实现了高效的主从数据同步,兼顾了性能和可靠性。"
}