前两篇文章是对LangChain4j比较全面的介绍,从本篇文章开始从某一个点进行分析。我们先从ChatMemory开始!
对于聊天记忆的场景、实现原理,在Spring AI 专栏中的# Spring AI 聊天上下文记忆源码分析以及实战文章有介绍,在这里就不多介绍直入主题。
为什么需要ChatMemory
实现聊天记忆实现起来非常简单,就是把用户所有的提问、大模型回答/产生的内容,放在一个List<ChatMessage>
中,随着用户提问将List一并发送给大模型,让大模型具备了聊天记忆功能。实现虽然简单,大家想一想有没有问题呢?
- 随着提问不断增多,上下文会变的很长,很快超出大模型的上下文token限制。
- 如果多人同时使用大模型,如何隔离不同用户的上下文信息。
手动维护和管理 ChatMessage
很麻烦。因此,LangChain4j 提供了一个 ChatMemory
抽象以及多个开箱即用的实现。ChatMemory
可以用作独立的低级组件,也可以用作高级组件(如 AI Services)的一部分。
LangChain4j 目前只提供“内存”,没有提供持久化的方式,如有特殊需求需要自行实现!
ChatMemory实现什么能力
- 容器管理机制,充当
ChatMessage
容器,对ChatMessage
进行管理。 - 淘汰机制(Eviction policy),为保证
ChatMessage
不会过多。 - 持久化机制(Persistence),防止聊天上下文丢失的问题。
- 消息特殊处理机制
SystemMessage
特殊处理。- 函数调用返回消息特殊处理。
淘汰机制(Eviction policy)
出于以下几个原因,数据淘汰机制是必要的:
-
适应 LLM的上下文窗口。一次LLM可以处理的token数量是有上限的。
一般情况将最旧的消息淘汰,如果有特殊需求,可以实现更复杂的算法。
-
控制成本。每个token都有成本,这使得每次调用LLM越来越昂贵。逐出不必要的token可降低成本。
Token=金钱,目前大模型的收费基本上都是根据Token收费。
-
控制延迟。发送到 LLM的token越多,处理它们所需的时间就越多。
ChatMemory源码分析
ChatMemory接口
public interface ChatMemory {
// ChatMemory的ID
Object id();
// 将message添加到ChatMemory中
void add(ChatMessage message);
// 从ChatMemory中获取消息,怎么取取决于实现
List<ChatMessage> messages();
// 清空ChatMemory中的消息
void clear();
}
ChatMemory实现类
-
MessageWindowChatMemory
「简单」:滑动窗口,保留N
最新的消息并驱逐不再适合的旧消息。 -
TokenWindowChatMemory
「复杂」:滑动窗口运行,N
但专注于保留最新的令牌,根据需要驱逐较旧的消息。需要Tokenizer
配合使用计算ChatMessage
的Token的数量。
持久化机制(Persistence)
默认情况下,ChatMemory
实现是将 ChatMessage
存储在内存中的,如果需要持久性,可以实现自定义 ChatMemoryStore
,将ChatMessage
存储在您选择的任何持久性存储中。
可以自定义持久化策略!!!!
ChatMemoryStore 接口
public interface ChatMemoryStore {
// 根据memoryId从指定的ChatMemoryStore中获取消息
List<ChatMessage> getMessages(Object memoryId);
// 根据memoryId,更新存储的消息
void updateMessages(Object memoryId, List<ChatMessage> messages);
// 根据memoryId删除存储的消息
void deleteMessages(Object memoryId);
}
InMemoryChatMemoryStore 实现类
public class InMemoryChatMemoryStore implements ChatMemoryStore {
private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap<>();
/**
* Constructs a new {@link InMemoryChatMemoryStore}.
*/
public InMemoryChatMemoryStore() {}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
return messagesByMemoryId.computeIfAbsent(memoryId, ignored -> new ArrayList<>());
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
messagesByMemoryId.put(memoryId, messages);
}
@Override
public void deleteMessages(Object memoryId) {
messagesByMemoryId.remove(memoryId);
}
}
LanChain4j仅提供一个基于内存实现的存储,如果有特殊需求,我们可以实现ChatMemoryStore
接口,自定义逻辑。
SystemMessage
特殊处理
SystemMessage
是一种特殊类型的消息,因此它与其他消息类型的处理方式不同:
- 一旦添加后,将始终保留
SystemMessage
。 - 一次只能持有一个
SystemMessage
。 - 忽略添加了具有相同内容的新
SystemMessage
。 - 如果添加了具有不同内容的新
SystemMessage
内容,则将替换前一个内容。
工具消息的特殊处理
所谓的工具消息就是函数调用请求消息和函数调用执行结果的消息。
如果包含ToolExecutionRequest
的AiMessage
被淘汰,则与其关联的返回消息ToolExecutionResultMessage
需要一同淘汰,否则会影响大模型的生成效果。
ChatMemory 代码实践
引入依赖包
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>${langchain4j.version}</version>
</dependency>
共享ChatMemory实现
package org.ivy.chatmemory;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.ivy.chatmemory.service.Assistant;
public class ChatMemoryJavaExample {
public static void main(String[] args) {
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(OpenAiChatModel.builder()
.baseUrl("xxxx")
.apiKey("xxxx")
.build()
)
.chatMemory(chatMemory)
.build();
String answer = assistant.chat("Hello! My name is Klaus.");
System.out.println(answer); // Hello Klaus! How can I assist you today?
String answerWithName = assistant.chat("What is my name?");
System.out.println(answerWithName); // Your name is Klaus.
}
}
执行测试结果:
Hello Klaus! How can I assist you today?
Your name is Klaus.
独享ChatMemory实现
package org.ivy.chatmemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.ivy.chatmemory.service.EachUserAssistant;
public class ChatMemoryEachUserExample {
public static void main(String[] args) {
EachUserAssistant assistant = AiServices.builder(EachUserAssistant.class)
.chatLanguageModel(
OpenAiChatModel.builder()
.baseUrl("xxx")
.apiKey("xxx")
.build())
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
.build();
System.out.println(assistant.chat(1, "Hello, my name is Klaus"));
// Hi Klaus! How can I assist you today?
System.out.println(assistant.chat(2, "Hello, my name is Francine"));
// Hello Francine! How can I assist you today?
System.out.println(assistant.chat(1, "What is my name?"));
// Your name is Klaus.
System.out.println(assistant.chat(2, "What is my name?"));
}
}
执行测试结果
Hi Klaus! How can I assist you today?
Hello Francine! How can I assist you today?
Your name is Klaus.
Your name is Francine.
定义memoryId,根据memoryId来获取是否是同一组上下文信息。
自定义MemoryStore
package org.ivy.chatmemory.persistent;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.mapdb.DB;
import org.mapdb.DBMaker;
import java.util.List;
import java.util.Map;
import static dev.langchain4j.data.message.ChatMessageDeserializer.messagesFromJson;
import static dev.langchain4j.data.message.ChatMessageSerializer.messagesToJson;
import static org.mapdb.Serializer.INTEGER;
import static org.mapdb.Serializer.STRING;
/**
* 自定义的持久化聊天存储器
*/
public class PersistentChatMemoryStore implements ChatMemoryStore {
private final DB db = DBMaker.fileDB("./chat-memory.db").transactionEnable().make();
private final Map<Integer, String> map = db.hashMap("messages", INTEGER, STRING).createOrOpen();
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = map.get((int) memoryId);
return messagesFromJson(json);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String json = messagesToJson(messages); // 序列化消息
map.put((int) memoryId, json);
db.commit(); // 提交事务
}
@Override
public void deleteMessages(Object memoryId) {
map.remove((int) memoryId);
db.commit();
}
}
自定义的关键几点:
- 消息的序列化/反序列化,可以使用LangChain4j提供的工具
- 选择存储方式,比如示列中使用的
mapdb
文件形式。
执行流程
- 用户发起提示词 prompt 提问;
- 根据chatMemoryId 查询历史回话,并返回;
- 通过提示词模板将 prompt 和 history messages 组合成一个提示词发送给大模型;
- 大模型根据提示词进行回答,将prompt + AiMessages同时放入到ChatMemory中;
- 最后将大模型生成的结果返回给用户
示例代码
- chatmemory:简单的示例
- chatmemory-eachuser: 会话维度隔离聊天上下文
- chatmemory-persistent: 自定义持久化
总结
对LangChain4j聊天记忆进行了分析,并介绍了ChatMemory四个特性。提供了使用示例,包括共享ChatMemory、会话维度隔离的ChatMemory,自定义存储机制等。实战代码可以参考Github。
那么,如何系统的去学习大模型LLM?
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
作为一名热心肠的互联网老兵,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。
但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
所有资料 ⚡️ ,朋友们如果有需要全套 《LLM大模型入门+进阶学习资源包》,扫码获取~
篇幅有限,部分资料如下:
👉LLM大模型学习指南+路线汇总👈
💥大模型入门要点,扫盲必看!
💥既然要系统的学习大模型,那么学习路线是必不可少的,这份路线能帮助你快速梳理知识,形成自己的体系。
👉大模型入门实战训练👈
💥光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
👉国内企业大模型落地应用案例👈
💥《中国大模型落地应用案例集》 收录了52个优秀的大模型落地应用案例,这些案例覆盖了金融、医疗、教育、交通、制造等众多领域,无论是对于大模型技术的研究者,还是对于希望了解大模型技术在实际业务中如何应用的业内人士,都具有很高的参考价值。 (文末领取)
💥《2024大模型行业应用十大典范案例集》 汇集了文化、医药、IT、钢铁、航空、企业服务等行业在大模型应用领域的典范案例。
👉LLM大模型学习视频👈
💥观看零基础学习书籍和视频,看书籍和视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。 (文末领取)
👉640份大模型行业报告👈
💥包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。
👉获取方式:
这份完整版的大模型 LLM 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
😝有需要的小伙伴,可以Vx扫描下方二维码免费领取🆓