告别无状态对话,用Spring AI Alibaba+Redis打造智能体记忆中枢
手写一个带记忆的AI客服,让你的大模型真正"记住"你说过的话
现在AI应用遍地开花,但你会发现:大多数Demo级别的AI对话,问完一句就忘了上一句。你跟它说"我叫张三",下一轮问"我叫什么",它一脸懵地回答"你还没有告诉我你的名字"。这就是典型的无状态AI,毫无实用价值。
真正能落地的AI应用,比如智能客服、角色陪伴、多轮对话Agent,核心挑战从来不是"怎么调API",而是状态管理------如何高效、低成本地管理多轮对话的上下文记忆、控制Token消耗、实现历史截断。
而这恰恰是Java后端工程师的主场。今天,我们就用Spring AI Alibaba + Redis,手把手打造一个带记忆的AI客服,让你的LLM应用拥有"超强大脑"。
一、问题揭示:无状态AI有多尴尬?
先来看一个典型"无状态"实现的伪代码:
java
@PostMapping("/chat")
public String chat(String userMessage) {
// 每次调用都只发当前消息,不带历史
return chatClient.call(userMessage);
}
效果是这样的:
用户:我叫张三。
AI:好的,张三。
用户:我刚才说我的名字是什么?
AI:抱歉,我不知道你的名字,你还没有告诉我。
更真实一点的场景:用户问"我家空调不制冷了怎么办",AI给出了一系列排查步骤,然后用户追问"第二步说的过滤网在哪个位置",AI完全不知道"第二步"指的是什么。
没有记忆的AI,就像一个金鱼------每次对话都是"全新的开始"。 在业务场景中,这种断片式的交互体验用户根本无法接受。
二、核心方案:四种记忆存储方案对比
在给AI加记忆之前,先明确我们要存储什么:
- 对话历史:用户每轮说的话、AI的回复。
- 会话元数据:会话ID、用户ID、创建时间、最后活跃时间等。
- Token消耗:用于计费和截断策略。
常见的存储方案及优缺点:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 客户端存储(LocalStorage) | 零服务端成本 | 不安全、不可跨端、数据易丢失 | 纯前端演示 |
| 内存缓存(ConcurrentHashMap) | 实现简单、极快 | 无法集群共享、重启丢失、内存不可控 | 单机原型 |
| Redis | 高性能、支持集群、过期策略、数据结构丰富 | 需要额外组件 | 生产级首选 |
| 数据库(MySQL) | 持久化、可复杂查询 | 性能较低、不适合高频读写 | 需要长期存档+审计 |
对于大多数对话场景,Redis是最佳平衡点。尤其是在生产环境中,我们需要多节点集群共享记忆------用户请求可能被负载均衡到任意一台服务器,如果某台机器把对话记忆存在本地内存里,换一台机器就全丢了。而Redis作为集中式存储,天然支持跨节点共享。本文基于Spring AI Alibaba框架 + Redis,实现一个支持滑动窗口、自动截断的生产级记忆方案。
为什么选择Spring AI Alibaba?
Spring AI Alibaba是阿里云推出的面向Java开发者的AI接入方案,构建在Spring AI基础之上,对接了阿里云的DashScope平台(通义千问系列模型)。它与Spring Boot紧密集成,符合传统Java编程习惯,封装了复杂的底层通信逻辑,支持Prompt模板、多轮对话、RAG检索增强等能力。在国内Java生态中使用广泛,特别是需要对接通义大模型或阿里百炼平台的项目。
三、实战代码:Spring AI Alibaba + Redis 实现滑动窗口记忆
3.1 技术栈
- Spring Boot 3.x
- Spring AI Alibaba 1.1.2.0+
- Redis(推荐使用Redis 6.x+)
- 阿里云DashScope(通义千问模型)
3.2 项目搭建与依赖配置
首先在pom.xml中添加Spring AI Alibaba的依赖:
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.10</version>
</parent>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Alibaba DashScope Starter -->
<dependency>
<groupId>com.alibaba.spring</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>1.1.2.0</version>
</dependency>
<!-- Redis 整合 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Jackson JSON 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Commons Pool 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
接着配置API Key和Redis连接信息:
yaml
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
timeout: 5000ms
lettuce:
pool:
max-active: 8
max-idle: 8
ai:
dashscope:
api-key: sk-xxx-your-real-key
注意 :API Key可以在阿里云DashScope控制台获取。
3.3 核心数据结构设计
我们用Redis的List结构存储某个会话的对话历史,每条记录包含角色、内容和时间戳:
java
// 对话消息实体
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
private String role; // "user" 或 "assistant"
private String content; // 消息内容
private Long timestamp; // 时间戳,用于排序和过期判断
}
Redis Key命名规则:chat:history:{sessionId}
3.4 实现基于Redis的ChatMemoryRepository
Spring AI Alibaba提供了ChatMemoryRepository接口作为存储抽象层,我们需要实现一个Redis版本,负责消息的持久化存储和检索:
java
@Component
public class RedisChatMemoryRepository implements ChatMemoryRepository {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String KEY_PREFIX = "chat:history:";
private static final int MAX_HISTORY_SIZE = 20; // 最大历史消息数,超过则滑动窗口截断
@Override
public List<Message> findByConversationId(String conversationId) {
String key = KEY_PREFIX + conversationId;
List<Object> range = redisTemplate.opsForList().range(key, 0, -1);
if (range == null || range.isEmpty()) {
return new ArrayList<>();
}
// 将存储的ChatMessage转换为Spring AI的Message对象
return range.stream()
.map(obj -> {
ChatMessage msg = (ChatMessage) obj;
return new Message(msg.getRole(), msg.getContent());
})
.collect(Collectors.toList());
}
@Override
public void saveAll(String conversationId, List<Message> messages) {
String key = KEY_PREFIX + conversationId;
// 将Message转换为ChatMessage并存储
for (Message msg : messages) {
ChatMessage chatMsg = new ChatMessage(
msg.getRole(),
msg.getContent(),
System.currentTimeMillis()
);
redisTemplate.opsForList().rightPush(key, chatMsg);
}
// 限制队列长度,实现滑动窗口
Long size = redisTemplate.opsForList().size(key);
if (size != null && size > MAX_HISTORY_SIZE) {
// 弹出超出部分的老消息
for (int i = 0; i < size - MAX_HISTORY_SIZE; i++) {
redisTemplate.opsForList().leftPop(key);
}
}
// 设置会话过期时间(30分钟无活动则自动清理)
redisTemplate.expire(key, 30, TimeUnit.MINUTES);
}
@Override
public void deleteByConversationId(String conversationId) {
String key = KEY_PREFIX + conversationId;
redisTemplate.delete(key);
}
@Override
public List<String> findConversationIds() {
// 简化实现,生产环境可通过SCAN命令遍历
throw new UnsupportedOperationException("如需遍历会话ID,请使用KeysCommand或SCAN命令");
}
}
3.5 配置ChatMemory
通过配置类,将我们的Redis存储库绑定到滑动窗口记忆策略上:
java
@Configuration
public class ChatMemoryConfig {
@Bean
public ChatMemoryRepository chatMemoryRepository() {
// 使用我们实现的Redis版本
return new RedisChatMemoryRepository();
}
@Bean
public ChatMemory chatMemory(ChatMemoryRepository repository) {
// MessageWindowChatMemory是Spring AI提供的内置实现,维护一个固定大小的消息窗口
// 当消息数量超过maxMessages时,会自动移除最老的消息,同时保留系统消息[reference:6][reference:7]
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository)
.maxMessages(20) // 最大保留20条消息(约10轮对话)
.build();
}
}
3.6 将ChatMemory集成到ChatClient
Spring AI Alibaba通过Advisor(顾问)机制 来为ChatClient添加增强功能。MessageChatMemoryAdvisor会在每次请求时自动从记忆库加载历史消息,并附加到当前Prompt中:
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(ChatMemory chatMemory) {
return ChatClient.builder()
// 添加记忆增强顾问,自动注入历史对话
.build(advisor -> advisor
.with(MessageChatMemoryAdvisor.class, advisorSpec -> advisorSpec
.chatMemory(chatMemory))
);
}
}
3.7 完整的Controller:带记忆的聊天接口
java
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@Autowired
private ChatClient chatClient;
@PostMapping("/send")
public ResponseEntity<ChatResponse> sendMessage(@RequestBody ChatRequest request) {
String sessionId = request.getSessionId(); // 由前端传入,如UUID
String userMessage = request.getMessage();
// chatClient会自动根据conversationId参数加载历史记忆
// Spring AI的MessageChatMemoryAdvisor会识别conversationId参数,
// 自动从对应的记忆存储中加载历史消息,并注入到本次请求中[reference:9]
String aiReply = chatClient.prompt()
.user(userMessage)
.withSystemParam("conversationId", sessionId) // 关键:绑定会话ID,实现会话隔离
.call()
.content();
return ResponseEntity.ok(new ChatResponse(aiReply, sessionId));
}
}
注意 :Spring AI的MessageChatMemoryAdvisor基于AOP实现环绕增强逻辑。当我们在请求中传入
conversationId参数后,Advisor会自动完成两件事:请求前 从记忆库加载历史消息合并到Prompt中;请求后 将本轮问答结果自动保存回记忆库。开发者无需手动维护任何存储逻辑。
3.8 效果演示
启动项目后,用curl测试:
bash
curl -X POST http://localhost:8080/api/chat/send \
-H "Content-Type: application/json" \
-d '{"sessionId": "user-001", "message": "我叫张三"}'
# 返回: "你好张三!很高兴认识你。有什么我可以帮你的吗?"
curl -X POST http://localhost:8080/api/chat/send \
-H "Content-Type: application/json" \
-d '{"sessionId": "user-001", "message": "我刚才说我叫什么?"}'
# 返回: "你刚才说你叫张三。"
同一个sessionId下,AI记住了之前的对话。换一个sessionId,对话历史完全独立,互不干扰------这就是会话隔离。
四、进阶优化:Token消耗控制与智能截断
真实场景下,对话越长,发送给LLM的token就越多,费用和延迟都会增加。我们需要更精细的控制。
4.1 滑动窗口内存修剪
我们已经在RedisChatMemoryRepository.saveAll()中实现了滑动窗口机制,通过限制MAX_HISTORY_SIZE来控制记忆长度。需要调整窗口大小时,直接修改配置即可。对于更精细的控制,Spring AI的MessageWindowChatMemory支持自定义消息窗口大小。
4.2 按Token数智能截断
如果单纯按消息数量截断不够精细(有的消息短,有的消息长),可以考虑实现按Token数量截断 。借助阿里百炼平台提供的tiktoken-java库:
java
public List<Message> getRecentWithinTokenLimit(String conversationId, int maxTokens) {
List<Message> all = chatMemory.get(conversationId);
List<Message> result = new ArrayList<>();
int currentTokens = 0;
for (int i = all.size() - 1; i >= 0; i--) {
Message msg = all.get(i);
int tokens = encoding.encode(msg.getContent()).size();
if (currentTokens + tokens > maxTokens) break;
currentTokens += tokens;
result.add(0, msg);
}
return result;
}
4.3 长对话摘要压缩
当对话轮次超过阈值后,可以触发后台任务,使用LLM本身将旧对话总结为摘要,替换原始历史。Spring AI Alibaba支持通过ReactAgent的上下文工程(Context Engineering)技术实现这一能力,通过消息修剪(Message Trimming)和摘要生成(Summarization) 来管理长对话中的上下文过载问题。
五、进阶思路:向量数据库与阿里百炼长期记忆
以上方案使用Redis存储短期记忆(最近N轮对话),适合会话级多轮交互。但如果要实现"永久记忆"------比如AI能记住用户一年前说过的喜好、历史订单、过往投诉------就需要长期记忆方案。
5.1 向量数据库方案
通过向量数据库(如Milvus、Qdrant、PgVector)配合嵌入模型,可以为记忆建立语义索引:
- Embedding:把每轮对话内容用嵌入模型转为向量。
- 存储:把向量连同元数据存入向量数据库。
- 检索:新消息到来时,同样转为向量,查询最相似的K条历史记忆,注入到Prompt中。
Spring AI Alibaba已提供VectorStore、Retriever、DocumentReader等模块化组件,支持ElasticSearch、Redis、PGVector等后端。
5.2 阿里百炼"记忆库"功能
更便捷的选择是利用阿里百炼平台提供的记忆库(Memory Vault) 功能。该功能于2026年4月正式上线,让Agent具备跨会话的长期记忆能力,真正实现"越聊越懂用户"的个性化体验。
记忆库系统内置了 "提取-存储-检索-注入" 四大模块:用户每次与AI Agent对话结束后,系统可根据配置的记忆规则自动提取关键信息并存储;当用户再次提问时,系统会触发语义检索召回相关记忆并附加至上下文中,实现个性化回答。
开发者可通过API直接调用,同时Spring AI Alibaba已深度集成阿里百炼平台,支持DashScope Agent与记忆系统的无缝对接。
六、生产环境最佳实践与踩坑指南
6.1 不同用户/会话的隔离
在调用时,务必通过withSystemParam("conversationId", sessionId)为不同用户传入不同的会话标识。同一会话ID自动共享记忆,不同会话ID之间完全隔离,避免"串话"问题。
6.2 Redis连接池配置
生产环境中,Redis的高并发至关重要,建议:
yaml
spring:
data:
redis:
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 10000ms
6.3 记忆过期策略管理
对于不再活跃的会话,应自动清理避免Redis内存膨胀。可以在RedisChatMemoryRepository.saveAll()中添加过期时间:
java
// 设置会话30分钟无活动则自动过期
redisTemplate.expire(key, 30, TimeUnit.MINUTES);
根据业务场景灵活调整TTL时长:高频对话场景可设为1小时,离线场景可设为7天。
6.4 工具调用结果的记忆回写
如果你的Agent调用了外部工具(如查询订单、获取天气),记得将工具返回结果以系统消息形式回写到记忆。否则,后续轮次中Agent不知道它自己之前查到了什么信息:
java
// 工具执行后,将结果追加到记忆
String toolResult = orderService.query(userId, lastWeek);
memory.add("system", "工具返回:" + toolResult);
七、总结:你的AI应用离"智能"只差一个记忆中枢
今天我们从一个痛点出发,逐步实现了一个生产级的AI对话记忆方案:
- 问题:无状态LLM毫无价值,无法支撑真实业务场景。
- 方案对比:Redis是生产最优解,支持分布式部署和会话隔离。
- 实战:用Spring AI Alibaba + Redis实现滑动窗口记忆,代码可直接复用。
- 框架优势 :Spring AI Alibaba提供
ChatMemoryRepository存储抽象、MessageWindowChatMemory滑动窗口实现、以及MessageChatMemoryAdvisor自动增强机制------开发者只需专注业务,框架负责复杂的记忆管理逻辑。 - 优化:Token截断、摘要压缩。
- 进阶:向量数据库实现长期记忆、阿里百炼"记忆库"功能(限时免费,可直接通过API调用)。
下一步你可以做什么?
- 按照本文代码,10分钟内跑通一个带记忆的AI客服Demo。
- 根据业务需求调整记忆窗口大小和会话过期时间。
- 在阿里百炼控制台探索"记忆库"功能,为你的应用加上长期记忆。
- 阅读Spring AI Alibaba官方文档中关于
ChatMemory和Agent框架的更多内容。
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、评论。下期我会写《Java工程师如何用向量数据库为AI构建长期记忆》,届时也会结合阿里百炼的最佳实践,敬请期待。