实现大模型聊天记忆就是把用户所有的提问、大模型回答/产生的内容,放在一个
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);
}
}
LangChain4j仅提供一个基于内存实现的存储。
如果有特殊需求,可以实现
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实现:
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.
}
}
独享ChatMemory实现:
定义memoryId,根据memoryId来获取是否是同一组上下文信息。
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?"));
}
}
自定义MemoryStore:
自定义的关键几点:
- 消息的序列化/反序列化,可以使用LangChain4j提供的工具。
- 选择存储方式,比如示列中使用的
mapdb
文件形式。
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();
}
}
执行流程
用户发起提示词 prompt 提问。
根据chatMemoryId 查询历史回话,并返回。
通过提示词模板将 prompt 和 history messages 组合成一个提示词发送给大模型。
大模型根据提示词进行回答,将prompt + AiMessages同时放入到ChatMemory中。
最后将大模型生成的结果返回给用户。