前言
什么是记忆功能?默认情况下当我们向大模型每次发起的提问都是新的,大模型无法把我们的每次对话形成记忆,也无法根据对话上下文给出人性化的答案。比如:我的第一次提问是“懂王有哪些特点”,然后大模型会给出我懂王的特点结果列表,当我再次提问“这些特点中哪个最惹人争议”的时候,它就不知道我在说什么了,因为大模型已经失去了上一次的提问记忆。所以让智能体(如AI助手、机器人、虚拟角色等)拥有记忆功能不仅能提升交互体验,还能增强其功能性、适应性和长期价值。
ChatMemory
书接上回《Langchain4j开发智能体-FunctionCalling的使用》,在Langchain4j中提供了 ChatMemory接口 作为历史消息存储的容器,该接口提供了消息的添加,获取,清理等基本方法。并通过memory Id(自定义的一个ID)来实现不同场次对话的消息隔离。简单看一下该接口源码
/**
表示聊天对话的内存 (历史记录) 。由于语言模型不保留对话的状态,
因此有必要在与语言模型的每次交互中提供所有以前的消息。
ChatMemory 有助于跟踪对话并确保消息适合语言模型的上下文窗口。
*/
public interface ChatMemory {
Object id();
void add(ChatMessage message);
List<ChatMessage> messages();
void clear();
}
ChatMemory提供了2个实现,MessageWindowChatMemory 以及 TokenWindowChatMemory
- MessageWindowChatMemory :作为滑动窗口,保留最新的N条消息,并逐出不再合适的旧消息。
- TokenWindowChatMemory :作为滑动窗口,保留N个最新的令牌,根据需要逐出旧消息。使用这个的话需要有一个Tokenizer来计算每个ChatMessage中的令牌
消息持久化
Langchain4j 通过 ChatMemoryStore 进行消息的持久化,用有一个默认的实现 InMemoryChatMemoryStore :基于内存的消息持久化,底层是一个ConcurrentHashMap,以memoryId作为key,List作为值 ,如果需要把持久化的消息保存到数据库中需要自己实现 ChatMemoryStore 接口。
构造ChatMemory
为了方便的构建ChatMemory 我们可以通过 MessageWindowChatMemory.builder 和 TokenWindowChatMemory.builder 来构建不同类型的ChatMemory
/**
* 创建智能体
* @param qwenChatModel :大模型
*/
@Bean
public ChatAssistant chatAssistant(QwenChatModel qwenChatModel,SearchApiWebSearchEngine webSearchEngine){
return AiServices.builder(ChatAssistant.class)
//指定对话记忆 - 记忆10次对话 - 基于内存
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
//指定模型
.chatLanguageModel(qwenChatModel)
//function call调用tools
.tools(new WeatherTool(),
//web搜索引擎
new WebSearchTool(webSearchEngine))
.build();
}
注意:这里的 withMaxMessages(10)指的是最大的消息存储数量,如果没有足够的空间容纳新消息,则最旧的消息将被逐出。
当然如果要自定义持久化位置,那么需要自己实现 ChatMemoryStore 接口,复写其中的三个方法:getMessages 获取消息,updateMessages 修改消息,deleteMessages删除消息
/**
* 消息对话记忆功能 - 消息存储
*/
public class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
//TODO 根据memoryId去数据库查询
return chatMessages;
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
//TODO 根据memoryId去数据库修改
}
@Override
public void deleteMessages(Object memoryId) {
//TODO 根据memoryId去数据库删除
}
}
自定义了ChatMemoryStore 后,智能体的创建也需要做些调整,需要指向自定义的存储类,如下
/**
* 创建智能体
* @param qwenChatModel :大模型
*/
@Bean
public ChatAssistant chatAssistant(QwenChatModel qwenChatModel,SearchApiWebSearchEngine webSearchEngine){
return AiServices.builder(ChatAssistant.class)
//指定对话记忆 - 记忆10次对话
//.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
//自定义存储方式
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder().chatMemoryStore(new PersistentChatMemoryStore()).maxMessages(10).build())
//指定模型
.chatLanguageModel(qwenChatModel)
//function call调用tools
.tools(new WeatherTool(),
//web搜索引擎
new WebSearchTool(webSearchEngine))
.build();
}
消息隔离
修改 ChatAssistant 的 对话方法,增加 memoryId,如下
public interface ChatAssistant {
...
/**
* 对话方法
* @param memoryId :消息存储-消息隔离ID
* @param userMessage :用户消息
* @return :结果
*/
@SystemMessage("你是一位诗人,请根据用户的提问为用户作诗")
String chat(@MemoryId String memoryId, @UserMessage String userMessage);
}
- @MemoryId String memoryId :用户消息隔离的ID,需要调用自己指定,想通的memoryId视为同一批次的消息
- @SystemMessage(“你是一位诗人,请根据用户的提问为用户作诗”) :该注解是用来指定系统提示词的,通过系统提示词来约束根据提示词生成内容
- @UserMessage String userMessage :标记为用户输入的消息内容
编写Controller
controller需要指定 memoryId 来指定消息隔离的ID,如下
/**
* 通过 AiService - 增加消息记忆
*/
@RequestMapping("/store")
public String store(@RequestParam("memoryId") String memoryId,
@RequestParam("message") String message) {
return chatAssistant.chat(memoryId,message);
}
测试效果如下:让它帮我们作一首描写春天是诗句
紧接着让他解释第四句诗的含义,看他是否能够根据记忆完成解释
很明显它拥有了记忆,它在尝试解释第四句“蜂舞蝶飞绕” ,但是它并未按照句号来断句。
总结
本篇文章介绍了大模型拥有记忆功能的重要性,以及介绍了基于内存实现记忆消息存储以及基于自定义的持久化方式,如果文章对你有帮助请三连,你的鼓励是我最大的动力。