RAG基础架构升级|Redis集成多轮对话能力

26 阅读10分钟

RAG基础架构升级|Redis集成多轮对话能力

一、改造核心价值

基于Redis实现会话记忆体+多轮对话上下文管理,为原有RAG架构补齐核心能力短板,完美适配智能体多轮交互场景,核心优势如下:

  1. 基于Redis高性能缓存,会话数据读写毫秒级响应,支撑高并发对话请求;
  2. 实现会话隔离,不同sessionId对应独立上下文,无数据串扰;
  3. 支持上下文容量限制、自动过期、手动清空,避免内存溢出,适配生产规范;
  4. 无缝接入原有RAG/Agent链路,侵入性极低,改造成本最小;
  5. 标准化消息结构+格式化上下文,大模型理解更精准,多轮对话连贯性大幅提升。

二、完整集成方案

第一步: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. 返回答案给用户,完成本轮对话

六、关键注意事项(生产落地必看)

  1. Redis环境要求:生产建议部署Redis集群/哨兵,保证高可用,避免单点故障;
  2. 会话ID规范:建议使用「用户ID+时间戳」生成唯一sessionId,如user_1001_20260106120000
  3. Prompt长度控制:结合大模型上下文窗口限制,合理配置max-message-count(建议10-20);
  4. 序列化安全:使用Jackson序列化,避免FastJSON安全漏洞,同时保证序列化效率;
  5. 权限控制:生产环境建议为Redis配置密码+防火墙,禁止公网访问。

七、总结

本次集成Redis实现多轮对话,是RAG架构的核心能力升级,完美解决了原有架构「无记忆、单轮交互」的短板,核心价值如下:

  1. 低成本接入:无缝对接原有RAG/Agent链路,几乎无侵入性改造;
  2. 高性能支撑:Redis毫秒级读写,满足高并发对话场景;
  3. 生产级可用:完善的异常处理、配置解耦、日志规范,适配企业级落地;
  4. 扩展性极强:支持上下文压缩、多租户隔离、定时清理等进阶能力;
  5. 体验大幅提升:大模型可理解历史语义,多轮对话连贯性、精准性显著增强。