项目中为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。

相关推荐
老华带你飞2 小时前
机电公司管理小程序|基于微信小程序的机电公司管理小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·微信小程序·小程序·机电公司管理小程序
Elastic 中国社区官方博客3 小时前
CI/CD 流水线与 agentic AI:如何创建自我纠正的 monorepos
大数据·运维·数据库·人工智能·搜索引擎·ci/cd·全文检索
Terio_my3 小时前
J2Cache 多级缓存配置与使用
缓存
拾忆,想起3 小时前
AMQP协议深度解析:消息队列背后的通信魔法
java·开发语言·spring boot·后端·spring cloud
I'm a winner3 小时前
护理+人工智能研究热点数据分析项目实战(五)
人工智能·数据挖掘·数据分析
PH = 73 小时前
Spring Ai Alibaba开发指南
java·后端·spring
蒋星熠3 小时前
TensorFlow与PyTorch深度对比分析:从基础原理到实战选择的完整指南
人工智能·pytorch·python·深度学习·ai·tensorflow·neo4j
qq_340474023 小时前
0.1 tensorflow例1-梯度下降法
人工智能·python·tensorflow
X.Cristiano3 小时前
MinerU2.5:一种用于高效高分辨率文档解析的解耦视觉-语言模型
人工智能·mineru