大部分时候根据用户提交的提示词给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分配对话记忆。
- 为代码生成方法添加appId参数
java
/**
* 生成HTML代码
*
* @param userMessage 用户提示词
* @return 生成的代码结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
HtmlCodeResult generateHTMLCode(@MemoryId int memoryId String userMessage);
- 在工厂类中创建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。并为其设置一个过期时间,避免内存泄漏。这样就不用每次调用时都重新生成,避免重复构造。
- 先引入 Caffeine 依赖
java
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
- 优化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();
}
- 获取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。
- 反转列表的原因: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。