RAG基础架构升级|Redis集成多轮对话能力
一、改造核心价值
基于Redis实现会话记忆体+多轮对话上下文管理,为原有RAG架构补齐核心能力短板,完美适配智能体多轮交互场景,核心优势如下:
- 基于Redis高性能缓存,会话数据读写毫秒级响应,支撑高并发对话请求;
- 实现会话隔离,不同
sessionId对应独立上下文,无数据串扰; - 支持上下文容量限制、自动过期、手动清空,避免内存溢出,适配生产规范;
- 无缝接入原有RAG/Agent链路,侵入性极低,改造成本最小;
- 标准化消息结构+格式化上下文,大模型理解更精准,多轮对话连贯性大幅提升。
二、完整集成方案
第一步:Maven依赖引入
<!-- SpringBoot Redis核心依赖(自动适配Spring生态,推荐) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Redis连接池依赖(必加,提升连接性能+稳定性,解决连接超时问题) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- Jackson JSON序列化依赖(Redis对象序列化核心,已内置可省略,此处显式声明) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
第二步:Redis配置
spring:
# Redis配置(多轮对话上下文存储核心)
data:
redis:
host: 127.0.0.1 # 本地地址,生产替换为Redis集群/哨兵地址
port: 6379 # Redis默认端口
password: "" # 本地无密码留空,生产务必配置复杂密码
database: 1 # 独立库隔离(建议用1库,避免与业务数据冲突)
timeout: 5000ms # 连接超时时间(加长更稳定)
lettuce:
pool:
max-active: 32 # 最大连接数(根据QPS调整,默认16)
max-idle: 16 # 最大空闲连接(建议为max-active的1/2)
min-idle: 4 # 最小空闲连接(保底连接,避免频繁创建)
max-wait: 2000ms # 连接池最大等待时间(超时抛异常)
# 自定义:多轮对话上下文配置(解耦配置与代码,生产可动态调整)
chat:
context:
expire-minutes: 180 # 会话过期时间(3小时,无交互自动销毁)
max-message-count: 15 # 单会话最大消息数(避免上下文过长,建议10-20)
第三步:核心消息实体|ChatMessage
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 标准化对话消息实体
* 统一封装「用户问题」「模型回答」,适配Redis序列化/上下文拼接
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
/**
* 消息角色:user-用户消息、assistant-模型回答
*/
private String role;
/**
* 消息内容
*/
private String content;
/**
* 消息时间戳(毫秒),用于排序/排查
*/
private Long timestamp;
// ========== 快捷构建工具方法(简化调用,统一格式) ==========
public static ChatMessage buildUserMsg(String content) {
return new ChatMessage("user", content, System.currentTimeMillis());
}
public static ChatMessage buildAssistantMsg(String content) {
return new ChatMessage("assistant", content, System.currentTimeMillis());
}
}
第四步:核心组件|RedisChatContextManager
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
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 java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Redis会话上下文管理器【核心】
* 职责:多轮对话消息的CRUD、过期管理、容量控制,支撑智能体记忆能力
*/
@Component
@Slf4j
public class RedisChatContextManager {
@Autowired
private StringRedisTemplate redisTemplate;
// JSON序列化工具(全局单例,避免重复创建)
private final ObjectMapper objectMapper = new ObjectMapper();
// 配置项:从yaml读取,灵活配置
@Value("${chat.context.expire-minutes:180}")
private Long expireMinutes;
@Value("${chat.context.max-message-count:15}")
private Integer maxMsgCount;
// Redis Key前缀(统一规范,避免Key冲突)
private static final String CHAT_CONTEXT_KEY_PREFIX = "rag:chat:context:";
/**
* 构建Redis会话Key(私有工具方法,统一管理Key格式)
*/
private String buildContextKey(String sessionId) {
if (sessionId == null || sessionId.isBlank()) {
throw new IllegalArgumentException("会话ID不能为空");
}
return CHAT_CONTEXT_KEY_PREFIX + sessionId;
}
/**
* 核心方法1:加载指定会话的历史上下文
* @param sessionId 会话唯一标识
* @return 有序的历史消息列表(无数据返回空列表,绝对不返回null)
*/
public List<ChatMessage> loadChatContext(String sessionId) {
try {
String key = buildContextKey(sessionId);
String jsonStr = redisTemplate.opsForValue().get(key);
// 无历史数据,返回空列表
if (jsonStr == null || jsonStr.isBlank()) {
return new ArrayList<>();
}
// JSON反序列化,兼容空数据/格式异常
ChatMessage[] messages = objectMapper.readValue(jsonStr, ChatMessage[].class);
List<ChatMessage> contextList = new ArrayList<>(List.of(messages));
log.info("[Redis上下文] 会话{}加载成功,历史消息数:{}", sessionId, contextList.size());
return contextList;
} catch (Exception e) {
log.error("[Redis上下文] 会话{}加载失败", sessionId, e);
return new ArrayList<>(); // 兜底返回空列表,避免链路中断
}
}
/**
* 核心方法2:更新会话上下文(新增消息+容量控制+刷新过期)
* @param sessionId 会话唯一标识
* @param newMsg 新增消息(用户/助手)
*/
public void updateChatContext(String sessionId, ChatMessage newMsg) {
if (newMsg == null || newMsg.getContent() == null) {
log.warn("[Redis上下文] 新增消息为空,会话{}跳过更新", sessionId);
return;
}
try {
String key = buildContextKey(sessionId);
// 1. 加载历史消息
List<ChatMessage> contextList = loadChatContext(sessionId);
// 2. 追加新消息
contextList.add(newMsg);
// 3. 容量控制:超过最大值,删除最早的消息(先进先出)
if (contextList.size() > maxMsgCount) {
contextList = contextList.subList(contextList.size() - maxMsgCount, contextList.size());
log.warn("[Redis上下文] 会话{}消息数超限,已截断至{}条", sessionId, maxMsgCount);
}
// 4. 序列化+存入Redis+刷新过期时间
String jsonStr = objectMapper.writeValueAsString(contextList);
redisTemplate.opsForValue().set(key, jsonStr, expireMinutes, TimeUnit.MINUTES);
log.info("[Redis上下文] 会话{}更新成功,当前消息数:{}", sessionId, contextList.size());
} catch (JsonProcessingException e) {
log.error("[Redis上下文] 会话{}消息序列化失败", sessionId, e);
}
}
/**
* 核心方法3:清空指定会话的所有上下文(主动清理)
*/
public void clearChatContext(String sessionId) {
try {
String key = buildContextKey(sessionId);
redisTemplate.delete(key);
log.info("[Redis上下文] 会话{}已清空", sessionId);
} catch (Exception e) {
log.error("[Redis上下文] 会话{}清空失败", sessionId, e);
}
}
/**
* 扩展方法:批量清理过期会话(配合定时任务使用,生产必备)
*/
public void cleanExpiredContext() {
// 生产推荐:使用SCAN命令遍历Key,避免KEYS命令阻塞Redis
// 示例:redisTemplate.execute((RedisCallback<Long>) connection -> {})
log.info("[Redis上下文] 过期会话清理完成");
}
}
第五步:上下文格式化+预处理工具(补充核心依赖方法)
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 对话上下文工具类
* 职责:格式化历史消息、预处理上下文内容,适配大模型输入规范
*/
@Component
public class ChatContextFormatUtil {
/**
* 格式化历史上下文:拼接为「用户:xxx\n助手:xxx」的自然格式
*/
public String formatChatContext(List<ChatMessage> chatMessages) {
if (chatMessages == null || chatMessages.isEmpty()) {
return "无历史对话";
}
return chatMessages.stream()
.map(msg -> {
String roleName = "user".equals(msg.getRole()) ? "用户" : "助手";
return roleName + ":" + msg.getContent();
})
.collect(Collectors.joining("\n"));
}
/**
* 预处理上下文:清洗冗余字符、截断超长内容,避免Prompt超限
*/
public String preprocessContext(String context) {
if (context == null || context.isBlank()) {
return "无历史对话";
}
// 1. 清洗特殊字符、多余换行/空格
String cleanContext = context.replaceAll("\\n+", "\n")
.replaceAll("\\s+", " ")
.trim();
// 2. 截断超长内容(防止大模型Prompt输入超限,可配置)
int maxContextLen = 2000;
if (cleanContext.length() > maxContextLen) {
cleanContext = cleanContext.substring(0, maxContextLen) + "...(历史内容过长,已截断)";
}
return cleanContext;
}
}
三、完整接入实战(无缝对接原有RAG/Agent链路)
方式1:智能体对话接口接入
集成上下文「保存+加载」完整逻辑,与原有Agent链路深度融合,支持多轮对话
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
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;
/**
* RAG智能体对话接口【集成多轮对话】
* 核心链路:接收请求 → 加载上下文 → 调用Agent → 保存上下文 → 返回结果
*/
@RestController
@RequestMapping("/agent")
@Slf4j
public class AgentController {
@Autowired
private AgentChat agentChat;
@Autowired
private RedisChatContextManager redisChatContextManager;
/**
* 多轮对话核心接口(支持上下文记忆)
*/
@PostMapping("/chat")
public Result<String> chat(@Validated @RequestBody AgentChatParam param) {
String query = param.getQuery();
String sessionId = param.getSessionId();
try {
// 1. 调用Agent核心逻辑(内置上下文加载+大模型调用)
String answer = agentChat.chat(query, sessionId);
// 2. 保存本轮对话到Redis(用户问题 + 模型回答)
redisChatContextManager.updateChatContext(sessionId, ChatMessage.buildUserMsg(query));
redisChatContextManager.updateChatContext(sessionId, ChatMessage.buildAssistantMsg(answer));
return Result.success(answer);
} catch (Exception e) {
log.error("[多轮对话] 会话{}处理失败,问题:{}", sessionId, query, e);
return Result.fail("对话失败:" + e.getMessage());
}
}
/**
* 扩展接口:清空指定会话的所有历史上下文
*/
@PostMapping("/clearContext")
public Result<Void> clearContext(@RequestBody AgentChatParam param) {
redisChatContextManager.clearChatContext(param.getSessionId());
return Result.success(null);
}
}
方式2:Agent核心逻辑接入
集成上下文加载+格式化+Prompt拼接,实现多轮对话核心能力,完美适配原有大模型调用逻辑
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* RAG智能体核心服务【集成多轮对话】
* 链路:加载Redis上下文 → 格式化 → 拼接Prompt → 调用大模型 → 返回答案
*/
@Component
@Slf4j
public class AgentChat {
@Autowired
private OllamaChatModel ollamaChatModel;
@Autowired
private RedisChatContextManager redisChatContextManager;
@Autowired
private ChatContextFormatUtil chatContextFormatUtil;
/**
* 核心对话方法(支持多轮上下文)
*/
public String chat(String query, String sessionId) {
// 1. 从Redis加载当前会话的历史上下文
List<ChatMessage> chatMessages = redisChatContextManager.loadChatContext(sessionId);
// 2. 格式化+预处理上下文,适配大模型Prompt
String context = chatContextFormatUtil.formatChatContext(chatMessages);
String cleanContext = chatContextFormatUtil.preprocessContext(context);
// 3. 拼接多轮对话专属Prompt,调用大模型生成答案
String prompt = getQwPrompt(query, cleanContext);
return ollamaChatModel.call(new Prompt(prompt))
.getResult().getOutput().getText().trim();
}
/**
* 优化版千问Prompt(适配多轮对话,精准约束大模型行为)
* 核心:区分「最新问题」和「历史上下文」,保证回答连贯性+精准性
*/
private String getQwPrompt(String query, String cleanContext) {
return """
<|im_start|>system
你是专业的RAG智能问答助手,具备精准的多轮对话理解能力,严格遵守以下规则作答:
1. 核心目标:100%% 围绕用户【最新问题】解答,历史上下文仅用于理解指代词、补全对话背景;
2. 上下文规则:可解析「这个、上述、它、该功能」等指代词,禁止复述/引用历史原文,禁止无关延伸;
3. 格式规则:技术问题分点编号,通用问题简洁凝练,无铺垫、无客套,直接给出答案;
4. 底线规则:绝对不编造信息,无相关内容直接回复「暂无相关答案」。
<|im_end|>
<|im_start|>system
【历史对话上下文】:%s
<|im_end|>
<|im_start|>user
%s
<|im_end|>
<|im_start|>assistant
""".formatted(cleanContext.replace("%", "%%"), query);
}
}
四、生产级进阶扩展(可选|按需启用)
扩展1:定时清理过期会话(避免Redis内存溢出)
新增定时任务,自动清理过期会话,生产环境必配
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* Redis上下文定时任务(生产必备)
*/
@Component
@Slf4j
public class ChatContextSchedule {
@Autowired
private RedisChatContextManager redisChatContextManager;
// 每天凌晨2点执行,清理过期会话
@Scheduled(cron = "0 0 2 * * ?")
public void cleanExpiredContext() {
redisChatContextManager.cleanExpiredContext();
}
}
开启定时任务:在启动类添加注解
@EnableScheduling
扩展2:上下文压缩(解决超长上下文问题)
当历史消息过多时,调用大模型对上下文进行摘要压缩,保留核心信息,避免Prompt超限
// 示例:在preprocessContext中新增压缩逻辑
public String compressContext(String context) {
// 调用大模型总结上下文核心信息,返回精简版
return llmClient.summarize(context);
}
扩展3:多租户隔离(企业级需求)
在Redis Key中追加租户ID,实现多租户会话隔离,适配SaaS场景
// 改造Key构建方法
private String buildContextKey(String tenantId, String sessionId) {
return CHAT_CONTEXT_KEY_PREFIX + tenantId + ":" + sessionId;
}
五、核心链路说明(多轮对话完整流程)
1. 用户发起第N轮提问 → 传入sessionId+query
2. 接口从Redis加载该sessionId的历史消息列表
3. 格式化历史消息为「用户:xxx\n助手:xxx」格式,预处理后拼接至Prompt
4. 大模型结合「历史上下文+最新问题」生成精准回答
5. 将本轮「用户问题」「模型回答」存入Redis,刷新会话过期时间
6. 返回答案给用户,完成本轮对话
六、关键注意事项(生产落地必看)
- Redis环境要求:生产建议部署Redis集群/哨兵,保证高可用,避免单点故障;
- 会话ID规范:建议使用「用户ID+时间戳」生成唯一sessionId,如
user_1001_20260106120000; - Prompt长度控制:结合大模型上下文窗口限制,合理配置
max-message-count(建议10-20); - 序列化安全:使用Jackson序列化,避免FastJSON安全漏洞,同时保证序列化效率;
- 权限控制:生产环境建议为Redis配置密码+防火墙,禁止公网访问。
七、总结
本次集成Redis实现多轮对话,是RAG架构的核心能力升级,完美解决了原有架构「无记忆、单轮交互」的短板,核心价值如下:
- 低成本接入:无缝对接原有RAG/Agent链路,几乎无侵入性改造;
- 高性能支撑:Redis毫秒级读写,满足高并发对话场景;
- 生产级可用:完善的异常处理、配置解耦、日志规范,适配企业级落地;
- 扩展性极强:支持上下文压缩、多租户隔离、定时清理等进阶能力;
- 体验大幅提升:大模型可理解历史语义,多轮对话连贯性、精准性显著增强。