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 实现了高效的主从数据同步,兼顾了性能和可靠性。"
}