聊天记忆详解与实战!
发表于更新于
AI知识AI聊天记忆详解与实战!
月伴飞鱼
实现大模型聊天记忆就是把用户所有的提问、大模型回答/产生的内容,放在一个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接口:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public interface ChatMemory { Object id(); void add(ChatMessage message);
List<ChatMessage> messages(); void clear(); }
|
ChatMemory实现类:
MessageWindowChatMemory
「简单」:
- 滑动窗口,保留
N
最新的消息并淘汰不再适合的旧消息。
TokenWindowChatMemory
「复杂」:
- 滑动窗口运行,
N
但专注于保留最新的令牌,根据需要淘汰较旧的消息。
- 需要
Tokenizer
配合使用计算ChatMessage
的Token的数量。
持久化机制(Persistence):
默认情况下,ChatMemory
实现是将 ChatMessage
存储在内存中的。
如果需要将会话持久化,可以自定义 ChatMemoryStore
,将ChatMessage
存储到您选择的任何持久化存储中。
ChatMemoryStore接口:
1 2 3 4 5 6 7 8 9 10 11
| public interface ChatMemoryStore {
List<ChatMessage> getMessages(Object memoryId);
void updateMessages(Object memoryId, List<ChatMessage> messages); void deleteMessages(Object memoryId); }
|
InMemoryChatMemoryStore实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class InMemoryChatMemoryStore implements ChatMemoryStore { private final Map<Object, List<ChatMessage>> messagesByMemoryId = new ConcurrentHashMap<>();
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代码实践
引入依赖包:
1 2 3 4 5 6 7 8 9 10 11
| <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实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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);
String answerWithName = assistant.chat("What is my name?"); System.out.println(answerWithName); } }
|
独享ChatMemory实现:
定义memoryId,根据memoryId来获取是否是同一组上下文信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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"));
System.out.println(assistant.chat(2, "Hello, my name is Francine"));
System.out.println(assistant.chat(1, "What is my name?"));
System.out.println(assistant.chat(2, "What is my name?")); } }
|
自定义MemoryStore:
自定义的关键几点:
- 消息的序列化/反序列化,可以使用LangChain4j提供的工具。
- 选择存储方式,比如示列中使用的
mapdb
文件形式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| 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中。
最后将大模型生成的结果返回给用户。