Spring AI2.0 + DeepSeek 完整版实战:上下文记忆|内存/Redis/官方ChatMemory三种方案|流式SSE|Tool工具调用|结构化输出|超时异常处理
Spring AI2.0 + DeepSeek 完整版实战:上下文记忆|内存/Redis/官方ChatMemory三种方案|流式SSE|Tool工具调用|结构化输出|超时异常处理
博客简介:基于 Spring‑AI 2.0.0‑M1 + DeepSeek,从零实现一个工业可用对话服务。包含手写内存会话管理器、手写Redis会话存储、SpringAI官方ChatMemory三套上下文记忆实现、SSE流式输出、token日志打印、防上下文溢出、系统角色词分离、结构化输出、
@Tool函数调用能力,为后续Agent开发打好基础
环境:JDK17 + SpringBoot3.4 + Spring‑AI2.0‑M1 + DeepSeek(OpenAI兼容协议) + Redis
摘要:本文详细介绍了基于Spring AI 2.0与DeepSeek构建工业级对话服务的完整实现方案。核心内容包括三种上下文记忆管理方案(手写内存管理器、手写Redis存储、官方ChatMemory组件)、SSE流式输出、Tool工具调用、结构化输出以及异常处理机制。文章提供了完整的代码示例和配置,涵盖了从环境搭建到生产部署的全流程,为后续Agent开发奠定坚实基础。
一、整体功能清单
- ✅ 独立分离System系统提示词,可全局/会话单独设置AI角色
- ✅ 手写内存上下文管理器,多轮短期对话、自动裁剪防止上下文溢出
- ✅ 手写Redis上下文存储器,手动序列化存储会话,处理异常脏数据问题
- ✅ Spring AI 官方
ChatMemory组件实现上下文记忆 - ✅ SSE流式输出、打字机效果,打印每段token分片日志
- ✅ 全局超时、网络异常、模型报错统一异常处理
- ✅ 结构化输出(Pojo实体返回)
- ✅
@Tool注解,大模型主动调用Java本地方法,Agent工具调用基础 - ✅ AI回答结果缓存、会话隔离(不同sessionId独立对话)
二、Maven完整依赖 pom.xml
<?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.0</version>
<relativePath/>
</parent>
<groupId>com.ai</groupId>
<artifactId>springai‑deepseek‑full‑demo</artifactId>
<version>0.0.1‑SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<spring-ai.version>2.0.0-M1</spring-ai.version>
</properties>
<repositories>
<repository>
<id>spring‑milestone</id>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!--web SSE-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- SpringAI openai兼容deepseek -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring‑ai‑openai‑spring‑boot‑starter</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!--tool调用、chatmemory内存组件-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring‑ai‑chat‑memory</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--json序列化-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
三、application.yml配置
server:
port: 8080
# http请求超时
tomcat:
connection-timeout: 30s
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
ai:
openai:
base-url: https://api.deepseek.com
api-key: sk-xxxxxxx
chat:
options:
model: deepseek-chat
temperature: 0.7
max-tokens: 2048
#自定义全局AI角色配置
ai:
system-prompt: "你是一名资深Java架构师,回答简洁专业,优先给出代码方案,遇到时间查询可以调用工具函数"
#单会话最大保存消息轮次,防止上下文溢出
max-history-size: 8
#redis会话过期时间 单位秒
session-expire-second: 3600
#模型调用超时
llm-timeout: 25000
四、DeepSeek基础配置类(解决2.x baseUrl不生效)
com.ai.config.DeepSeekAiConfig
package com.ai.config;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import java.time.Duration;
@Configuration
public class DeepSeekAiConfig {
@Value("${spring.ai.openai.base-url}")
private String baseUrl;
@Value("${spring.ai.openai.api-key}")
private String apiKey;
@Value("${spring.ai.openai.chat.options.model}")
private String model;
@Value("${ai.llm-timeout}")
private Long llmTimeout;
@Bean
public OpenAiApi openAiApi(RestClient.Builder restClientBuilder){
// 设置http超时,解决大模型长时间响应超时问题
RestClient.Builder builder = restClientBuilder
.defaultTimeout(Duration.ofMillis(llmTimeout));
return new OpenAiApi(baseUrl,apiKey,builder);
}
@Bean
public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi){
OpenAiChatOptions options = OpenAiChatOptions.builder()
.model(model)
.temperature(0.7)
.build();
return new OpenAiChatModel(openAiApi,options);
}
}
关键点:SpringAI2.x自动配置无法修改base‑url,必须手动构建
OpenAiApi;在这里统一设置模型http超时时间,拦截模型长时间无响应问题
五、公共DTO、常量、异常实体
5.1 流式请求DTO
package com.ai.dto;
import lombok.Data;
@Data
public class ChatReqDTO {
//会话唯一id,用来隔离不同用户上下文
private String sessionId;
//用户提问
private String message;
//可选:临时覆盖全局system角色词
private String systemPrompt;
}
5.2 消息实体,上下文存储对象
package com.ai.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.MessageType;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SessionMessage {
private MessageType type;
private String content;
}
5.3 自定义业务异常
package com.ai.exception;
public class LlmTimeoutException extends RuntimeException{
public LlmTimeoutException(String msg){
super(msg);
}
}
方案一:手写内存会话管理器(内存存储上下文)
特点:进程内存保存会话,重启丢失;自带消息裁剪,防止上下文溢出,适合测试环境com.ai.memory.InMemorySessionManager
package com.ai.memory;
import com.ai.entity.SessionMessage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.ai.chat.messages.MessageType;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class InMemorySessionManager {
@Value("${ai.max-history-size}")
private Integer maxHistorySize;
// key:sessionId value:当前会话全部消息
private final Map<String, List<SessionMessage>> sessionMap = new ConcurrentHashMap<>();
/**
* 获取会话历史
*/
public List<SessionMessage> getHistory(String sessionId){
return sessionMap.getOrDefault(sessionId,new ArrayList<>());
}
/**
* 保存单条消息
*/
public void addMessage(String sessionId, MessageType type,String content){
List<SessionMessage> msgList = getHistory(sessionId);
msgList.add(new SessionMessage(type,content));
// 上下文溢出裁剪策略:超过最大轮次,删除最早的用户+AI一对消息
if(msgList.size()>maxHistorySize){
//移除头部两条(一问一答)
msgList.remove(0);
msgList.remove(0);
}
sessionMap.put(sessionId,msgList);
}
/**
* 清空会话
*/
public void clear(String sessionId){
sessionMap.remove(sessionId);
}
}
溢出策略说明:简单实用,当消息总数超过配置阈值,删掉最早一轮问答,控制token总量,避免请求大模型时报上下文超长错误。生产环境复杂策略可以改成token计数裁剪
方案二:手写Redis存储器存储上下文记忆
解决痛点:手动Redis存储会话;先落库再请求大模型、异常回滚脏数据,持久化会话,服务重启上下文不丢失
com.ai.memory.RedisSessionManager
package com.ai.memory;
import com.alibaba.fastjson2.JSON;
import com.ai.entity.SessionMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.ai.chat.messages.MessageType;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class RedisSessionManager {
private static final String SESSION_PREFIX = "ai:session:";
@Autowired
private StringRedisTemplate redisTemplate;
@Value("${ai.session-expire-second}")
private Long expireSecond;
@Value("${ai.max-history-size}")
private Integer maxHistorySize;
/**
* 获取会话
*/
public List<SessionMessage> getHistory(String sessionId){
String key = SESSION_PREFIX+sessionId;
String json = redisTemplate.opsForValue().get(key);
if(json==null){
return new ArrayList<>();
}
return JSON.parseArray(json,SessionMessage.class);
}
/**
* 保存消息
* @param tempBackup 临时备份key,异常时用来回滚
*/
public void addMessage(String sessionId, MessageType type, String content,String tempBackup){
String key = SESSION_PREFIX+sessionId;
List<SessionMessage> history = getHistory(sessionId);
//备份修改前的数据,出现异常用来恢复,解决脏数据
String oldData = JSON.toJSONString(history);
redisTemplate.opsForValue().set(tempBackup,oldData,30, TimeUnit.SECONDS);
history.add(new SessionMessage(type,content));
//裁剪
if(history.size()>maxHistorySize){
history.remove(0);
history.remove(0);
}
String newJson = JSON.toJSONString(history);
redisTemplate.opsForValue().set(key,newJson,expireSecond,TimeUnit.SECONDS);
}
/**
* 请求异常:回滚会话,恢复保存前状态,避免只存了用户提问,AI回答失败造成脏上下文
*/
public void rollback(String tempBackupKey,String sessionId){
String backup = redisTemplate.opsForValue().get(tempBackupKey);
if(backup!=null){
redisTemplate.opsForValue().set(SESSION_PREFIX+sessionId,backup,expireSecond,TimeUnit.SECONDS);
}
redisTemplate.delete(tempBackupKey);
}
public void clear(String sessionId){
redisTemplate.delete(SESSION_PREFIX+sessionId);
}
}
核心脏数据解决方案:新增消息之前先备份原会话到临时key;当调用DeepSeek接口抛出超时/网络异常时,执行rollback方法,回滚Redis会话,防止用户问题存入、AI回答失败导致上下文错乱。
方案三:Spring AI官方ChatMemory组件存储上下文
SpringAI内置开箱即用记忆组件,支持InMemoryChatMemory、RedisChatMemory(需要引入依赖),会话隔离。
//bean注册 内存版官方chatMemory
@Bean
public ChatMemory chatMemory(){
//内存版,每个会话独立
return new InMemoryChatMemory();
}
使用方式:
//1.读取历史消息
List<Message> history = chatMemory.get(sessionId);
//2.添加用户消息
chatMemory.add(sessionId,new UserMessage(userInput));
//3.得到ai回复后存入
chatMemory.add(sessionId,new AssistantMessage(aiAnswer));
对比三种记忆方案
|方案|优点|缺点|适用场景|
|—|—|—|—|
|手写内存管理器|完全可控,裁剪策略自定义,零第三方依赖|进程重启丢失|开发、单元测试|
|手写Redis管理器|持久化,脏数据回滚机制,高度可控|需要手动序列化、自己维护过期、裁剪|正式生产环境|
|官方ChatMemory|开箱即用,代码最少,spring生态原生|裁剪策略自由度低,redis版需要额外配置|快速原型、中小型项目|
六、Tool工具调用实现,@Tool注解 Agent基础
实现一个简单获取当前时间的工具,大模型可以自主决定是否调用Java方法com.ai.tool.AiTools
package com.ai.tool;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class AiTools {
@Tool(description = "获取当前系统时间,当用户询问现在几点,当前日期的时候调用")
public String getCurrentTime(){
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return "当前时间:"+ LocalDateTime.now().format(formatter);
}
}
使用的时候,构建ChatModel调用时注入工具集合,大模型可以自动判断什么时候调用这个Java函数,这是Agent智能体最底层基础能力
七、结构化输出实体
强制大模型返回固定JSON结构
package com.ai.dto;
import lombok.Data;
@Data
public class ArticleResp {
private String title;
private String summary;
private String content;
}
八、流式SSE控制器 + token打印 + 异常处理 + Redis手写记忆示例
com.ai.controller.StreamChatController
package com.ai.controller;
import com.ai.dto.ChatReqDTO;
import com.ai.exception.LlmTimeoutException;
import com.ai.memory.RedisSessionManager;
import com.ai.tool.AiTools;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/chat")
public class StreamChatController {
@Resource
private ChatModel chatModel;
@Resource
private RedisSessionManager redisSessionManager;
@Resource
private AiTools aiTools;
@Value("${ai.system-prompt}")
private String globalSystemPrompt;
/**
* SSE流式接口,手写Redis上下文存储
*/
@PostMapping(value = "/stream-redis",produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChatRedis(@RequestBody ChatReqDTO dto){
String sessionId = dto.getSessionId();
String userMsg = dto.getMessage();
//异常回滚临时备份key
String tempBackupKey = "temp:backup:"+ UUID.randomUUID();
return Flux.create(sink->{
try{
//1、读取redis历史上下文
List<SessionMessage> history = redisSessionManager.getHistory(sessionId);
List<Message> messages = new ArrayList<>();
//2、优先使用传入的system词,没有就取全局
String sysPrompt = dto.getSystemPrompt()==null?globalSystemPrompt:dto.getSystemPrompt();
messages.add(new SystemMessage(sysPrompt));
//3、还原历史对话
for(SessionMessage sm : history){
if(sm.getType()== MessageType.USER){
messages.add(new UserMessage(sm.getContent()));
}else if(sm.getType()==MessageType.ASSISTANT){
messages.add(new AssistantMessage(sm.getContent()));
}
}
//4、添加当前用户提问,先保存到redis(带备份)
redisSessionManager.addMessage(sessionId,MessageType.USER,userMsg,tempBackupKey);
messages.add(new UserMessage(userMsg));
//5、注册Tool工具回调
ToolCallbackProvider toolProvider = MethodToolCallbackProvider.builder()
.toolObjects(aiTools).build();
Prompt prompt = new Prompt(messages,
OpenAiChatOptions.builder()
.toolCallbacks(toolProvider.getToolCallbacks())
.build());
//6、流式请求大模型
Flux<ChatResponse> stream = chatModel.stream(prompt);
StringBuilder fullAnswer = new StringBuilder();
stream.subscribe(resp->{
//分片token打印日志
String chunk = resp.getResult().getOutput().getText();
log.info("token分片:{}",chunk);
fullAnswer.append(chunk);
sink.next(chunk);
},error->{
//异常分支:执行redis会话回滚,清除刚刚存入的用户消息,防止脏数据
log.error("大模型调用异常",error);
redisSessionManager.rollback(tempBackupKey,sessionId);
sink.error(new LlmTimeoutException("模型调用超时或者接口异常"));
},()->{
//流式正常结束,把AI完整回答存入Redis
redisSessionManager.addMessage(sessionId,MessageType.ASSISTANT,fullAnswer.toString(),tempBackupKey);
sink.complete();
});
}catch (Exception e){
redisSessionManager.rollback(tempBackupKey,sessionId);
sink.error(e);
}
});
}
}
核心逻辑拆解
- 请求进来,先从Redis读取历史对话
- System提示词单独分离,支持会话级覆盖全局角色
- 写入用户提问前先备份,一旦后续调用DeepSeek报错,执行回滚,避免脏上下文
- 流式返回,实时打印每一块token分片日志
- 流式全部接收完毕,才把AI完整答案存入Redis
- 自动注入Tool工具,大模型可主动调用Java方法
九、全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(LlmTimeoutException.class)
public ResponseEntity<String> llmTimeout(LlmTimeoutException e){
log.error("模型超时异常:{}",e.getMessage());
return ResponseEntity.status(504).body("大模型服务调用超时,请稍后重试");
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> globalError(Exception e){
log.error("系统异常",e);
return ResponseEntity.status(500).body("AI服务内部异常");
}
}
十、三种上下文方案接口总结
/chat/stream‑memory使用手写内存管理器,适合本地调试/chat/stream‑redis手写Redis存储、脏数据回滚,生产首选/chat/stream‑officialSpringAI官方ChatMemory实现,代码最简
十一、踩坑汇总
- SpringAI2.x自动配置无法覆盖DeepSeek域名,必须手动构建OpenAiApi Bean
- 先存用户消息再调用模型,如果接口失败,必须回滚会话,否则上下文错乱
- 一定要限制历史消息数量,不然token溢出报错
- Tool调用必须通过
MethodToolCallbackProvider注册工具Bean - SSE流式必须捕获每一段token分片日志,方便线上排查输出异常
十二、后续Agent拓展方向
- PGVector向量库接入RAG知识库
- 多工具编排、多Agent任务分发
- token精确计数上下文裁剪(目前是轮次裁剪,可以升级)
- 对话结果全局缓存,相同问题直接返回缓存
更多推荐

所有评论(0)