目录
3.3.2 自定义chatMemoryProvider 配置bean
4.2.2 ChatMemoryProvider 配置bean改造
一、前言
在于大模型对话的时候,细心的伙伴们会发现,前面跟大模型聊的一句话,后面再基于这句话继续问问题的时候,仍然可以得到预期的回答,这就是大模型的记忆能力。什么是记忆功能?默认情况下向大模型每次发起的提问都是新的,大模型无法把每次对话形成记忆,也无法根据对话上下文给出人性化的答案。比如:我的第一次提的一个问题,大模型给出了一个回答的列表,当我再次提问这个回答列表中的一个问题时,它就不知道我在说什么了,因为大模型已经失去了上一次的提问记忆。所以让智能体(如AI助手、机器人、虚拟角色等)拥有记忆功能不仅能提升交互体验,还能增强其功能性、适应性和长期价值。
二、AI大模型会话记忆介绍
2.1 AI 大模型的会话记忆是什么
AI 大模型的会话记忆,通常指的是在对话式人工智能系统中,为了让AI能够理解并回应用户输入的信息时,考虑到之前的交互内容的能力。这种能力使得AI能够在长时间的对话中维持上下文,记住之前提到的关键信息,并根据这些信息进行更加准确和相关的回应。在下面的这2段对话中,基于第一次给出的回答内容,再次发起相关的问题时,大模型仍然能够给出预期的回答。
传统上,很多对话系统处理每个请求都是独立的,不考虑之前发生的对话内容。这种方式对于简单、直接的查询是有效的,但当涉及到需要上下文理解的复杂对话时,就显得力不从心了。为了解决这个问题,研究人员开发了不同的技术来赋予AI“记忆”功能,使它们能够在对话中保持连贯性和一致性。
2.2 AI 大模型为什么需要会话记忆
AI大模型需要会话记忆,主要是为了提升对话的质量和连贯性,使得人机交互更加自然、有效。以下是几个主要原因:
上下文理解:在实际对话中,人们经常依赖于之前提到的信息来构建后续的讨论。没有会话记忆的话,AI每次只能处理独立的请求,而无法理解或记住之前的对话内容,这会导致回答缺乏上下文关联性,显得机械且不自然。
个性化体验:通过记住用户先前提供的信息(例如偏好、历史行为等),AI可以提供更加个性化的服务和建议,增强用户体验。这种能力对于客服机器人、个人助手等应用场景尤为重要。
复杂问题解决:有些问题或任务需要跨多个对话回合进行,可能涉及收集额外的数据或者逐步细化需求。拥有会话记忆可以让AI更有效地管理这些复杂的交互过程,提高解决问题的效率。
持续学习与改进:虽然这不是传统意义上的“记忆”,但一些先进的系统能够从过去的对话中学习,识别常见模式、错误和成功案例,从而不断优化其响应策略。
维护对话的一致性:在长时间的对话过程中保持角色一致性、立场一致性和事实准确性是非常重要的。会话记忆帮助AI在整个对话过程中维持这些方面的一致性,避免出现自相矛盾的回答。
增强用户信任:当AI能够记住并正确引用早先的对话细节时,它能给用户留下深刻印象,并增加对AI系统的信任感。这对于建立长期的用户关系至关重要。
综上所述,会话记忆是使AI大模型能够在各种应用场景中实现更加智能、灵活和人性化的关键因素之一。它不仅提高了对话的质量,还为用户提供了一个更加连贯、个性化的交流体验。
2.3 AI 大模型会话记忆常用实现方案
随着大模型技术的广泛使用,AI大模型实现会话记忆的方式有多种,如下:
基于缓存的记忆:这种方法涉及存储最近几个回合的对话信息,并在处理新的输入时参考这些信息。这可以提供一定程度的上下文,但其容量有限,因为只能记住最近的交互。
外部数据库或知识图谱:在这种方法中,AI可以通过查询外部数据库或知识图谱来获取长期记忆。这种方式允许AI访问大量信息,但是如何高效地检索和利用这些信息是一个挑战。
长短期记忆网络(LSTM)和其他递归神经网络(RNNs):这些是专门设计用来处理序列数据的深度学习模型,能够通过内部状态保存一些关于过去事件的记忆。虽然它们能捕捉到一定的上下文信息,但在处理非常长的对话历史时仍面临挑战。
Transformer架构:近年来,Transformer及其变种(如BERT、GPT等)已经成为构建大规模语言模型的基础,这些模型可以同时关注多个输入部分,从而更有效地理解和记忆对话中的关键信息。特别是自注意力机制允许模型对整个对话历史进行编码,以便更好地生成响应。
随着技术的进步,现代AI大模型越来越擅长模拟人类对话,不仅能够回答问题,还能以自然流畅的方式参与复杂的对话,这部分得益于其先进的会话记忆技术。
2.4 LangChain4j 会话记忆介绍
LangChain4j 是 Java 版的 LangChain 实现,提供了构建大模型应用的组件,其中会话记忆(Memory)是核心功能之一。入口:Chat Memory | LangChain4j
2.4.1 LangChain4j 会话记忆介绍
LangChain4j 中的会话记忆是指在与大模型交互过程中,保存和管理对话历史、上下文信息的机制。它使应用能够:
维护多轮对话状态
记住用户偏好和历史交互
提供连贯的对话体验
2.4.2 LangChain4j 会话记忆类型
LangChain4j 提供了多种记忆实现:
1)基础记忆类型
LangChain4j 提供的基础记忆类型主要是其核心接口ChatMemory 下面的几种实现方式,如下:
ChatMemoryStore
基础接口,定义了记忆存储的基本操作
主要方法:
add()
,getMessages()
,clear()
MessageWindowChatMemory
基于滑动窗口的记忆实现
只保留最近N条消息(可配置)
防止记忆无限增长
TokenWindowChatMemory
基于token数量的记忆实现
保留最近的消息直到达到token限制
更适合大模型的上下文窗口限制
2)高级记忆类型
高级记忆主要是在实际开发中,借助外部的存储组件进行会话记忆的存储以及相关的扩展点,包括:
PersistentChatMemory
持久化记忆,可保存到数据库或文件
支持跨会话记忆
VectorStoreChatMemory
使用向量存储保存记忆
支持基于语义的记忆检索
SummaryChatMemory
自动生成对话摘要作为记忆
减少需要保存的原始消息数量
三、Langchain4j 会话记忆操作案例使用
接下来通过代码的案例操作,详细介绍Langchain4j 的会话记忆功能的使用。
3.1 前置准备
3.1.1 导入依赖文件
创建一个springboot 工程,pom中导入下面的核心依赖
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>3.2.6</spring-boot.version>
<langchain4j.version>1.0.0-beta3</langchain4j.version>
</properties>
<dependencies>
<!-- web应用程序核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 编写和运行测试用例 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 基于openai系列整合的springboot-starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
</dependency>
<!-- 接入阿里云百炼平台 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!--langchain4j高级功能-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.39</version> <!-- 使用最新安全版本 -->
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--引入SpringBoot依赖管理清单-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--引入langchain4j依赖管理清单-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--引入百炼依赖管理清单-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-bom</artifactId>
<version>${langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.1.2 添加配置文件
在工程配置文件中添加下面的配置内容
server:
port: 8082
#直接对接的是deepseek官网的的大模型
langchain4j:
#阿里百炼平台的模型
community:
dashscope:
chat-model:
api-key: 你的apikey #这个是百炼平台的apikey
model-name: qwen-max
logging:
level:
root: debug
注意:
如果没有apikey的同学,可以前往阿里云百炼平台注册并获取一个apikey,操作入口:
3.1.3 前置案例
1)增加一个 Assistant接口
自定义一个Assistant,里面有一个chat方法
package com.congge.assistant;
import dev.langchain4j.service.spring.AiService;
@AiService
public interface Assistant {
String chat(String userMessage);
}
2)增加一个测试接口
在工程中增加一个测试接口,如下代码:
@Resource
private Assistant assistant;
//localhost:8082/chat/test
@GetMapping("/chat/test")
public String chat() {
String result1 = assistant.chat("我是小王");
System.out.println(result1);
String result2 = assistant.chat("你知道我是谁吗?");
System.out.println(result2);
return "chat";
}
启动工程后调用该接口,通过控制台输出效果可以发现一个问题,那就是通过这种方式与大模型进行对话,大模型是没有会话记忆的。
3.2 会话记忆的实现
通过上面的接口案例不难发现,直接与大模型对话是没有会话记忆的,如果实现会话记忆功能,就需要引入其他的方式进行实现,下面分别说明。
3.2.1 基于多轮对话存储结果实现会话记忆
保存对话内容实现会话记忆是一种原始但是比较容易实现的方式,具体来说就是,在第一轮对话之后,后面的每一轮对话在调用chat方法的时候,均需要把之前的对话加入到参数里面去。
package com.congge.controller;
import com.congge.assistant.Assistant;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.response.ChatResponse;
import java.util.Arrays;
@RestController
public class ChatMemoryTestController {
@Resource
private Assistant assistant;
@Autowired
private QwenChatModel qwenChatModel;
//localhost:8082/chat/test
@GetMapping("/chat/test")
public String chat() {
String result1 = assistant.chat("我是小王");
System.out.println(result1);
String result2 = assistant.chat("你知道我是谁吗?");
System.out.println(result2);
return "chat";
}
//localhost:8082/chat/memory/v1
@GetMapping("/chat/memory/v1")
public String chatMemoryV1() {
//第一轮对话
UserMessage userMessage1 = UserMessage.userMessage("我是小王");
ChatResponse chatResponse1 = qwenChatModel.chat(userMessage1);
AiMessage aiMessage1 = chatResponse1.aiMessage();
//输出大语言模型回复
System.out.println(aiMessage1.text());
//第二轮对话
UserMessage userMessage2 = UserMessage.userMessage("你知道我是谁吗");
ChatResponse chatResponse2 = qwenChatModel.chat(
Arrays.asList(
userMessage1,
aiMessage1,
userMessage2
)
);
AiMessage aiMessage2 = chatResponse2.aiMessage();
//输出大语言模型的回复
System.out.println(aiMessage2.text());
return aiMessage2.text();
}
}
启动项目后,调用上面的接口,通过返回结果发现,这种方式可以实现多轮会话的记忆功能
3.2.2 基于ChatMemory 实现会话记忆
Langchain4j 提供了ChatMemory 组件,基于该组件可以实现会话记忆的功能
从源码中不难发现,ChatMemory 是一个接口,理论上,只要是实现了该接口,均可以实现会话记忆功能,在接口的默认实现中,可以看到基于当前的依赖引入下,有下面两个实现
以MessageWindowChatMemory这个实现来说,顾名思义,说明这个实现情况下,会话记忆是存储在内存中,一旦程序重启了会话记忆就丢失了,使用该组件进行一个会话记忆功能,参考下面的代码
//localhost:8082/chat/memory/v2?userMessage=我是小王
@GetMapping("/chat/memory/v2")
public String chatMemoryV2(@RequestParam("userMessage") String userMessage) {
//创建chatMemory
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
//创建AIService
Assistant assistant = AiServices
.builder(Assistant.class)
.chatLanguageModel(qwenChatModel)
.chatMemory(chatMemory)
.build();
//调用service的接口
String answer1 = assistant.chat(userMessage);
System.out.println(answer1);
String answer2 = assistant.chat("我是谁");
System.out.println(answer2);
return answer2;
}
调用上面的接口,可以看到通过这种方式可以实现会话记忆的功能
3.2.3 基于ChatMemory 会话记忆升级
上一步基于ChatMemory 实现了会话记忆功能,但是细心的同学会发现,这种写法比较繁琐,不方便全局使用和代码复用,于是可以想到,既然上一步用到了AiServices , 那就可以借助AiServices 将会话记忆配置到全局的bean中进行管理。
1)自定义一个全局的ChatMemory的配置类
参考下面的代码,将MessageWindowChatMemory配置为全局bean
package com.congge.assistant;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MemoryCharConfig {
@Bean
public ChatMemory chatMemory() {
//设置聊天记忆记录的message数量
return MessageWindowChatMemory.withMaxMessages(20);
}
}
2)自定义Assistant
添加一个自定义的Assistant接口,在接口的注解中,引用上一步自定义的chatMemory
package com.congge.assistant;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "qwenChatModel",
chatMemory = "chatMemory"
)
public interface ChatMemoryAssistant {
String chat(String userMessage);
}
3)添加测试接口
添加一个自定义接口用于效果测试,参考下面的代码
@Resource
private ChatMemoryAssistant chatMemoryAssistant;
//localhost:8082/chat/memory/v3?userMessage=我是小王
@GetMapping("/chat/memory/v3")
public String chatMemoryV3(@RequestParam("userMessage") String userMessage) {
//调用service的接口
String answer1 = chatMemoryAssistant.chat(userMessage);
System.out.println(answer1);
String answer2 = chatMemoryAssistant.chat("我是谁");
System.out.println(answer2);
return answer2;
}
4)效果测试
调用上面的接口,通过返回结果可以看到,通过这种方式也能达到存储会话记忆的功能
3.3 会话隔离实现
在上面的基于chatMemory 实现会话记忆中,细心的同学可以发现一个问题,那就是这样的实现方式无法做到会话隔离,而在实际应用中,不同的用户进行对话,是必须要做会话隔离的,否则A用户的历史会话信息会被B用户看到了,这就出问题了,此时需要借助AiService注解中的 chatMemoryProvider 这个属性配置参数来实现,下面来看代码具体实现过程。
3.3.1 自定义一个Assistant
自定义一个Assistant 接口
在该chat方法中,使用了2个新增的注解@MemoryId和@UserMessage,对传入的参数类型进行了限定
在接口注解中,配置了chatMemoryProvider 这个属性值,在后面还需要配置一个这样的bean
package com.congge.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
chatModel = "qwenChatModel",
chatMemoryProvider = "chatMemoryProvider"
)
public interface SeparateChatAssistant {
/**
* 分离聊天记录
* @param memoryId 聊天id
* @param userMessage 用户消息
* @return
*/
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
}
3.3.2 自定义chatMemoryProvider 配置bean
自定义一个类,并配置chatMemoryProvider 的bean
package com.congge.config;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SeparateChatAssistantConfig {
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.build();
}
}
3.3.3 添加测试接口
添加一个测试接口,参考下面的代码
@Resource
private SeparateChatAssistant separateChatAssistant;
//localhost:8082/chat/memory/v5?userId=1&userMessage=我是小王
//localhost:8082/chat/memory/v5?userId=1&userMessage=你知道我是谁吗?
//localhost:8082/chat/memory/v5?userId=2&userMessage=我是小李
//localhost:8082/chat/memory/v5?userId=2&userMessage=你知道我是谁吗?
@GetMapping("/chat/memory/v5")
public String chatMemoryV3(@RequestParam("userId") Integer userId,@RequestParam("userMessage") String userMessage) {
String answer1 = separateChatAssistant.chat(userId,userMessage);
return answer1;
}
3.3.4 效果测试
依次调用上面的4个接口,可以看到下面的效果,说明上面的实现方案是正常生效了
1)第一次调用
2)第二次调用
3)第三次调用
4)第四次调用
四、基于Redis实现会话记忆持久化存储
通过上面的案例操作,我们掌握了Langchan4j中会话记忆存储的常用实现方式,但是细心的同学可以发现,这种方式是基于内存的实现,一旦服务重启,或者服务异常宕机了,历史会话就丢失了,而在实际应用开发中,这种方式是肯定不允许存在的,接下来我们使用Redis作为持久化存储数据源来实现。
4.1 前置准备
4.1.1 导入redis依赖
基于上面的工程,在pom文件中导入下redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
4.1.2 添加redis配置信息
在配置文件中添加关于redis的配置信息
spring:
data:
redis:
host: localhost
port: 6379
4.1.3 自定义redis序列化类
为了更好的管理redis中存储的数据,这里添加一个redis的自定义配置类
package com.congge.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
4.2 会话记忆代码改造
基于会话隔离中的代码我们做一些改造即可。
4.2.1 自定义ChatMemoryStore
ChatMemoryStore是存储记忆会话必须要使用的对象,如果想要实现会话记忆的自定义数据源存储,需要实现该接口,并实现里面的三个核心方法,从源码中可以清楚看到这一点
参考下面的代码
package com.congge.config;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;
public class RedisMemoryStoreConfig implements ChatMemoryStore {
private RedisTemplate redisTemplate;
public RedisMemoryStoreConfig(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String value = (String) redisTemplate.opsForValue().get("chat:" + memoryId.toString());
if(value == null || value.isEmpty()){
return List.of();
}
return ChatMessageDeserializer.messagesFromJson(value);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> list) {
String messages = ChatMessageSerializer.messagesToJson(list);
redisTemplate.opsForValue().set("chat:" + memoryId.toString(), messages);
}
@Override
public void deleteMessages(Object memoryId) {
redisTemplate.delete("chat:" + memoryId.toString());
}
}
4.2.2 ChatMemoryProvider 配置bean改造
在上一节中我们了解到,ChatMemoryProvider 是连接大模型对话的核心配置bean,只需要在ChatMemoryProvider的配置bean中,指定chatMemoryStore使用上面自定义的这个RedisMemoryStoreConfig 即可,参考下面的代码
package com.congge.config;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
public class SeparateChatAssistantConfig {
@Resource
RedisTemplate redisTemplate;
//基于内存的实现
// @Bean
// public ChatMemoryProvider chatMemoryProvider() {
// return memoryId -> MessageWindowChatMemory.builder()
// .id(memoryId)
// .maxMessages(20)
// .chatMemoryStore(redisMemoryStoreConfig)
// .build();
// }
//基于redis的实现
@Bean
public ChatMemoryProvider chatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(20)
.chatMemoryStore(new RedisMemoryStoreConfig(redisTemplate))
.build();
}
}
4.2.3 接口效果测试
到这一步,改造就完成了,其实核心就是将chatMemoryStore使用自定义的使用redis的自定义的类替换即可,下面通过几个接口调用验证下效果如何。
1)调用userId=1的接口
依次调用userId=1的两个接口
调用成功后,检查redis中存储的信息,可以看到redis中存储了userId=1的两个序列化之后的会话信息
2)调用userId=2的接口
依次调用userId=2的两个接口
调用成功后,检查redis中存储的信息,可以看到redis中存储了userId=2的两个序列化之后的会话信息
五、写在文末
本文通过较大的篇幅并结合实际案例,详细介绍了Langchain4j会话记忆存储的功能,最后给出了基于外部存储组件Redis实现会话存储的代码演示,有兴趣的同学还可以基于此继续深入研究,本篇到此结束,感谢观看。