项目中为AI添加对话记忆

大部分时候根据用户提交的提示词给AI生成的网站没办法一次性满足需求,所以需要为用户提交网站修改功能。如果AI没有对话记忆,则每次修改都是重新生成代码,而不是在原有的基础上修改。

因此我们需要为AI添加对话记忆,使得每次的生成都携带着之前的对话。

一. 方案设计

LangChain4j不仅提供了对话记忆能力,而且还能结合Redis持久化对话记忆。

1.不用内存来存储会话记忆:

首先重启后会丢失记忆;其次如果每个应用都在内存中维护对话历史,很容易出现OOM。

2 .不用MySQL来存储会话记忆

Redis作为内存数据库,在读写对话记忆时性能更高;另一方面是数据库中的对话历史表包含其他业务字段,不适合在世界交给LangChain4j的对话记忆组件管理。

加载历史:

为Redis的每个Key都设置合理的过期时间,只需在初始化会话记忆时,加载最新的对话记录到Redis中,就能确保AI了解交互历史。

对话隔离:LangChain4j提供了对话记忆隔离的能力,主要是根据id。

二. 开发实现

1.引入依赖

XML 复制代码
<dependency>
  <groupId>dev.langchain4j</groupId>
  <artifactId>langchain4j-community-redis-spring-boot-starter</artifactId>
  <version>1.1.0-beta7</version>
</dependency>

这个依赖引入了Redis的Jedis客户端,以及与LangChain4j的整合组件。

2. 配置Redis

2.1 在application.yaml中添加Redis连接信息。

XML 复制代码
spring:
  # redis
  data:
    redis:
      host: localhost
      port: 6379
      password: 
      ttl: 3600

这里的ttl是key的过期时间。

2.2 在config下新建Redis对话记忆存储配置类,初始化RedisChatMemoryStore的Bean

java 复制代码
@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
@Data
public class RedisChatMemoryStoreConfig {

    private String host;

    private int port;

    private String password;

    private long ttl;

    @Bean
    public RedisChatMemoryStore redisChatMemoryStore() {
        return RedisChatMemoryStore.builder()
                .host(host)
                .port(port)
                .password(password)
                //.user("你的用户名")
                .ttl(ttl)
                .build();
    }
}

如果Redis的密码不为空,就要配置用户名。

2.3 在启动类中排除embedding的自动装配

java 复制代码
@SpringBootApplication(exclude = {RedisEmbeddingStoreAutoConfiguration.class})

否则启动时会报错。

3. 使用对话记忆

利用appId来实现对话隔离。LangChain4j有两个实现方案。

方案一:内置隔离机制

可以给AI服务方法增加memoryId注解和参数,然后通过chatMemoyProvider为每个appId分配对话记忆。

  1. 为代码生成方法添加appId参数
java 复制代码
 /**
     * 生成HTML代码
     *
     * @param userMessage 用户提示词
     * @return 生成的代码结果
     */
    @SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
    HtmlCodeResult generateHTMLCode(@MemoryId int memoryId String userMessage);
  1. 在工厂类中创建AI Service时,通过chatMemoryProvider为每个memoryId来构造专属的MessageWindowChatMemory,这个memoryId相当于key,每次通过memoryId来获取对应的对话记忆。不传入id则默认为default。
java 复制代码
private final RedisChatMemoryStore redisChatMemoryStore;

@Bean
public AiCodeGeneratorService aiCodeGeneratorService() {
    return AiServices.builder(AiCodeGeneratorService.class)
            .chatModel(chatModel)
            .streamingChatModel(streamingChatModel)
            // 根据 id 构建独立的对话记忆
            .chatMemoryProvider(memoryId -> MessageWindowChatMemory
                    .builder()
                    .id(memoryId)
                    .chatMemoryStore(redisChatMemoryStore)
                    .maxMessages(20)
                    .build())
            .build();
}

可以看到这里使用的是一个Ai Service实例。没有根据memoryId来生成不同的实例。

方案二:AI Service隔离

之前共用一个AI实例,这里我们通过appId为每个应用创建对应的AI Service,里面存放这个应用的对话记忆。

修改AI Service 工厂类,提供根据appId获取对应的AI Service 服务实例。

java 复制代码
@Configuration
public class AiCodeGeneratorServiceFactory {

    @Resource
    private ChatModel chatModel;

    @Resource
    private StreamingChatModel streamingChatModel;

    @Resource
    private RedisChatMemoryStore redisChatMemoryStore;

    /**
     * 根据 appId 获取服务
     */
    public AiCodeGeneratorService getAiCodeGeneratorService(long appId) {
        // 根据 appId 构建独立的对话记忆
        MessageWindowChatMemory chatMemory = MessageWindowChatMemory
                .builder()
                .id(appId)
                .chatMemoryStore(redisChatMemoryStore)
                .maxMessages(20)
                .build();
        return AiServices.builder(AiCodeGeneratorService.class)
                .chatModel(chatModel)
                .streamingChatModel(streamingChatModel)
                .chatMemory(chatMemory)
                .build();
    }
}

根据appId获取对应的AI Service 服务实例。

本地缓存优化

每次生成AI Service实例后,我们都将其存入Caffeine。并为其设置一个过期时间,避免内存泄漏。这样就不用每次调用时都重新生成,避免重复构造。

  1. 先引入 Caffeine 依赖
java 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
  1. 优化AiCodeGeneratorServiceFactory,增加缓存逻辑
java 复制代码
/**
 * AI 服务实例缓存
 * 缓存策略:
 * - 最大缓存 1000 个实例
 * - 写入后 30 分钟过期
 * - 访问后 10 分钟过期
 */
private final Cache<Long, AiCodeGeneratorService> serviceCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(Duration.ofMinutes(30))
        .expireAfterAccess(Duration.ofMinutes(10))
        .removalListener((key, value, cause) -> {
            log.debug("AI 服务实例被移除,appId: {}, 原因: {}", key, cause);
        })
        .build();

/**
 * 根据 appId 获取服务(带缓存),如果内存中存在直接取出,不存在再创建
 */
public AiCodeGeneratorService getAiCodeGeneratorService(long appId) {
    return serviceCache.get(appId, this::createAiCodeGeneratorService);
}

/**
 * 创建新的 AI 服务实例
 */
private AiCodeGeneratorService createAiCodeGeneratorService(long appId) {
    log.info("为 appId: {} 创建新的 AI 服务实例", appId);
    // 根据 appId 构建独立的对话记忆
    MessageWindowChatMemory chatMemory = MessageWindowChatMemory
            .builder()
            .id(appId)
            .chatMemoryStore(redisChatMemoryStore)
            .maxMessages(20)
            .build();
    return AiServices.builder(AiCodeGeneratorService.class)
            .chatModel(chatModel)
            .streamingChatModel(streamingChatModel)
            .chatMemory(chatMemory)
            .build();
}
  1. 获取AI Service实例示例:
java 复制代码
@Resource
private AiCodeGeneratorServiceFactory aiCodeGeneratorServiceFactory;

// 根据 appId 获取对应的 AI 服务实例
AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId);

历史对话加载

对话记忆初始化时,从数据库中加载对话历史到记忆中。

开发加载对话历史的方法:

java 复制代码
@Override
public int loadChatHistoryToMemory(Long appId, MessageWindowChatMemory chatMemory, int maxCount) {
    try {
        // 直接构造查询条件,起始点为 1 而不是 0,用于排除最新的用户消息
        QueryWrapper queryWrapper = QueryWrapper.create()
                .eq(ChatHistory::getAppId, appId)
                .orderBy(ChatHistory::getCreateTime, false)
                .limit(1, maxCount);
        List<ChatHistory> historyList = this.list(queryWrapper);
        if (CollUtil.isEmpty(historyList)) {
            return 0;
        }
        // 反转列表,确保按时间正序(老的在前,新的在后)
        historyList = historyList.reversed();
        // 按时间顺序添加到记忆中
        int loadedCount = 0;
        // 先清理历史缓存,防止重复加载
        chatMemory.clear();
        for (ChatHistory history : historyList) {
            if (ChatHistoryMessageTypeEnum.USER.getValue().equals(history.getMessageType())) {
                chatMemory.add(UserMessage.from(history.getMessage()));
                loadedCount++;
            } else if (ChatHistoryMessageTypeEnum.AI.getValue().equals(history.getMessageType())) {
                chatMemory.add(AiMessage.from(history.getMessage()));
                loadedCount++;
            }
        }
        log.info("成功为 appId: {} 加载了 {} 条历史对话", appId, loadedCount);
        return loadedCount;
    } catch (Exception e) {
        log.error("加载历史对话失败,appId: {}, error: {}", appId, e.getMessage(), e);
        // 加载失败不影响系统运行,只是没有历史上下文
        return 0;
    }
}

注意:

1.在LangChanin4j框架中,在将用户消息存入数据库中后,AI服务会自动将用户的最新消息存入记忆中。所以查询的起始点为1而不是0。

  1. 反转列表的原因:AI的上下文是有顺序的,所以存入的记忆要按照时间升序;从数据库中取出最新的二十条数据,此时列表中的消息是按时间降序的,所以要将列表反转。

3.清理缓存:每次加载的时候先清理掉Redis中存储的历史记录,防止重复加载。

在初始化AI Service 的对话记忆时调用该方法,只有对话时才加载记忆,节约内存。

java 复制代码
private AiCodeGeneratorService createAiCodeGeneratorService(long appId) {
    log.info("为 appId: {} 创建新的 AI 服务实例", appId);
    // 根据 appId 构建独立的对话记忆
    MessageWindowChatMemory chatMemory = MessageWindowChatMemory
            .builder()
            .id(appId)
            .chatMemoryStore(redisChatMemoryStore)
            .maxMessages(20)
            .build();
    // 从数据库加载历史对话到记忆中
    chatHistoryService.loadChatHistoryToMemory(appId, chatMemory, 20);
    return AiServices.builder(AiCodeGeneratorService.class)
            .chatModel(chatModel)
            .streamingChatModel(streamingChatModel)
            .chatMemory(chatMemory)
            .build();
}

三. Redis分布式 Session

在项目中引入了Redis,我们可以使用Redis管理Session登录态,实现分布式会话管理。这样就不用每次重启服务器都需要重新登录了。

1. 在Maven中引入 spring-session-data-redis 库

XML 复制代码
<!-- Spring Session + Redis -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

2. 修改 application.yml配置文件,更改Session的存储方式和过期时间:

XML 复制代码
spring: 
  # session 配置
  session:
    store-type: redis
    # session 30 天过期
    timeout: 2592000
server:
  port: 8123
  servlet:
    context-path: /api
    # cookie 30 天过期
    session:
      cookie:
        max-age: 2592000

这样的话用户的登录状态会保存到Redis中,重启服务器后,不需要重新登录。在Redis中可以看到登录相关的key。

相关推荐
启效云7 小时前
AI赋能制造丨启效云亮相2026工业母机产业链高质量发展大会
大数据·人工智能·制造
小小龙学IT7 小时前
C++ Redis 客户端深度解析(hiredis 与 redis-plus-plus)——工业数采实时缓存 C++ 侧实战
c++·redis·缓存
冰暮流星7 小时前
sql之is null 运算符
java·数据库·sql
Rain的Java大神之路7 小时前
🔥13年Java老兵转型AI Agent:90%的人挂在同一个坑,根本不用学Python!(万字实战复盘,建议收藏)
java·后端·面试
yuniko-n7 小时前
【Java】关于容器选型:Deque、PriorityQueue、LinkedList
java·开发语言
TK泰妞7 小时前
如何利用跨境女装提示词库提升销售效率?从商品图片到TikTok内容的完整方法
大数据·人工智能
鲜于言悠9057 小时前
opencode
人工智能
猛犸象限7 小时前
【CJMP Grok Bot实践】用 CJMP 搬游戏时碰到的三个缺口,和我们的绕法
人工智能·grok
用户abcde7 小时前
RAG 检索增强生成
人工智能
文谦南宁AI普工7 小时前
RAG 检索全对,回答全错?用位置扫描 10 分钟定位 Lost in the Middle
人工智能·aigc