项目中为AI添加对话记忆
大部分时候根据用户提交的提示词给AI生成的网站没办法一次性满足需求,所以需要为用户提交网站修改功能。如果AI没有对话记忆,则每次修改都是重新生成代码,而不是在原有的基础上修改。
因此我们需要为AI添加对话记忆,使得每次的生成都携带着之前的对话。
一. 方案设计
LangChain4j不仅提供了对话记忆能力,而且还能结合Redis持久化对话记忆。
1.不用内存来存储会话记忆:
首先重启后会丢失记忆;其次如果每个应用都在内存中维护对话历史,很容易出现OOM。
2.不用MySQL来存储会话记忆
Redis作为内存数据库,在读写对话记忆时性能更高;另一方面是数据库中的对话历史表包含其他业务字段,不适合在世界交给LangChain4j的对话记忆组件管理。
加载历史:
为Redis的每个Key都设置合理的过期时间,只需在初始化会话记忆时,加载最新的对话记录到Redis中,就能确保AI了解交互历史。
对话隔离:LangChain4j提供了对话记忆隔离的能力,主要是根据id。
二. 开发实现
1.引入依赖
<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连接信息。
spring:# redisdata:redis:host: localhostport: 6379password: ttl: 3600
这里的ttl是key的过期时间。
2.2 在config下新建Redis对话记忆存储配置类,初始化RedisChatMemoryStore的Bean
@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
@Data
public class RedisChatMemoryStoreConfig {private String host;private int port;private String password;private long ttl;@Beanpublic RedisChatMemoryStore redisChatMemoryStore() {return RedisChatMemoryStore.builder().host(host).port(port).password(password)//.user("你的用户名").ttl(ttl).build();}
}
如果Redis的密码不为空,就要配置用户名。
2.3 在启动类中排除embedding的自动装配
@SpringBootApplication(exclude = {RedisEmbeddingStoreAutoConfiguration.class})
否则启动时会报错。
3. 使用对话记忆
利用appId来实现对话隔离。LangChain4j有两个实现方案。
方案一:内置隔离机制
可以给AI服务方法增加memoryId注解和参数,然后通过chatMemoyProvider为每个appId分配对话记忆。
1. 为代码生成方法添加appId参数
/*** 生成HTML代码** @param userMessage 用户提示词* @return 生成的代码结果*/@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")HtmlCodeResult generateHTMLCode(@MemoryId int memoryId String userMessage);
2. 在工厂类中创建AI Service时,通过chatMemoryProvider为每个memoryId来构造专属的MessageWindowChatMemory,这个memoryId相当于key,每次通过memoryId来获取对应的对话记忆。不传入id则默认为default。
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 服务实例。
@Configuration
public class AiCodeGeneratorServiceFactory {@Resourceprivate ChatModel chatModel;@Resourceprivate StreamingChatModel streamingChatModel;@Resourceprivate 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 依赖
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency>
2. 优化AiCodeGeneratorServiceFactory,增加缓存逻辑
/*** 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();
}
3. 获取AI Service实例示例:
@Resource
private AiCodeGeneratorServiceFactory aiCodeGeneratorServiceFactory;// 根据 appId 获取对应的 AI 服务实例
AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId);
历史对话加载
对话记忆初始化时,从数据库中加载对话历史到记忆中。
开发加载对话历史的方法:
@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。
2. 反转列表的原因:AI的上下文是有顺序的,所以存入的记忆要按照时间升序;从数据库中取出最新的二十条数据,此时列表中的消息是按时间降序的,所以要将列表反转。
3.清理缓存:每次加载的时候先清理掉Redis中存储的历史记录,防止重复加载。
在初始化AI Service 的对话记忆时调用该方法,只有对话时才加载记忆,节约内存。
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 库
<!-- Spring Session + Redis -->
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
2. 修改 application.yml配置文件,更改Session的存储方式和过期时间:
spring: # session 配置session:store-type: redis# session 30 天过期timeout: 2592000
server:port: 8123servlet:context-path: /api# cookie 30 天过期session:cookie:max-age: 2592000
这样的话用户的登录状态会保存到Redis中,重启服务器后,不需要重新登录。在Redis中可以看到登录相关的key。