聊天记忆详解与实战!

实现大模型聊天记忆就是把用户所有的提问、大模型回答/产生的内容,放在一个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 {
// 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接口:

1
2
3
4
5
6
7
8
9
10
11
public interface ChatMemoryStore {

// 根据memoryId从指定的ChatMemoryStore中获取消息
List<ChatMessage> getMessages(Object memoryId);

// 根据memoryId,更新存储的消息
void updateMessages(Object memoryId, List<ChatMessage> messages);

// 根据memoryId删除存储的消息
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<>();

/**
* 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 内容,则将替换前一个内容。

工具消息的特殊处理:

所谓的工具消息就是函数调用请求消息和函数调用执行结果的消息。

如果包含ToolExecutionRequestAiMessage被淘汰。

则与其关联的返回消息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); // 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来获取是否是同一组上下文信息。

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"));
// 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 文件形式。
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中。

最后将大模型生成的结果返回给用户。