聊天记忆持久化(Persistence)

默认情况下,聊天记忆存储在内存中。如果需要持久化存储,可以实现一个自定义的聊天记忆存储类,以便将聊天消息存储在你选择的任何持久化存储介质中。

存储介质的选择

大模型中聊天记忆的存储选择哪种数据库,需要综合考虑数据特点、应用场景和性能要求等因素,以下是一些常见的选择及其特点:

(1)MySQL:

  • 特点:关系型数据库。支持事务处理,确保数据的一致性和完整性,适用于结构化数据的存储和查询。
  • 适用场景:如果聊天记忆数据结构较为规整,例如包含固定的字段如对话 ID、用户 ID、时间戳、消息内容等,且需要进行复杂的查询和统计分析,如按用户统计对话次数、按时间范围查询特定对话等,MySQL是不错的选择。

(2)Redis:

  • 特点:内存数据库,读写速度极高。它适用于存储热点数据,并且支持多种数据结构,如字符串、哈希表、列表等,方便对不同类型的聊天记忆数据进行处理。
  • 适用场景:对于实时性要求极高的聊天应用,如在线客服系统或即时通讯工具,Redis可以快速存储和获取最新的聊天记录,以提供流畅的聊天体验。

(3)MongoDB:

  • 特点:文档型数据库,数据以JSON - like的文档形式存储,具有高度的灵活性和可扩展性。它不需要预先定义严格的表结构,适合存储半结构化或非结构化的数据。
  • 适用场景:当聊天记忆中包含多样化的信息,如文本消息、图片、语音等多媒体数据,或者消息格式可能会频繁变化时,MongoDB能很好地适应这种灵活性。例如,一些社交应用中用户可能会发送各种格式的消息,使用MongoDB可以方便地存储和管理这些不同类型的数据。

(4)Cassandra:

  • 特点:是一种分布式的 NoSQL 数据库,具有高可扩展性和高可用性,能够处理大规模的分布式数据存储和读写请求。适合存储海量的、时间序列相关的数据。
  • 适用场景:对于大型的聊天应用,尤其是用户量众多、聊天数据量巨大且需要分布式存储和处理的场景,Cassandra 能够有效地应对高并发的读写操作。例如,一些面向全球用户的社交媒体平台,其聊天数据需要在多个节点上进行分布式存储和管理,Cassandra可以提供强大的支持。

MongoDB

MongoDB简介

(1)MongoDB是一个基于文档的NoSQL数据库,由MongoDB Inc.开发。NoSQL指的是非关系型的数据库。NoSQL有时也称作Not Only SQL的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称。

(2)MongoDB的设计理念是为了应对大数据量、高性能和灵活性需求。

(3)MongoDB使用集合(Collections)来组织文档(Documents),每个文档都是由键值对组成:

  • 数据库(Database):存储数据的容器,类似于关系型数据库中的数据库。
  • 集合(Collection):数据库中的一个集合,类似于关系型数据库中的表。
  • 文档(Document):集合中的一个数据记录,类似于关系型数据库中的行(row),以 BSON 格式存储。

(4)MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成,文档类似于JSON对象,字段值可以包含其他文档,数组及文档数组:

MongoDB本地安装及使用

【传统方式安装】

第一步,安装 mongodb-windows-x86_64-8.0.6-signed.msi,这是服务端;

第二步,安装 mongosh-2.5.0-win32-x64.zip,这是命令行客户端;

第三步,安装 mongodb-compass-1.39.3-win32-x64.exe,这是图形客户端,方便操作MongoDB。

接下来介绍如何使用mongosh,具体的步骤如下:

第一步,启动 MongoDB Shell。在命令行中输入 mongosh 命令,启动 MongoDB Shell,如果 MongoDB 服务器运行在本地默认端口(27017),则可以直接连接。

1
mongosh

第二步,连接到 MongoDB 服务器。如果 MongoDB 服务器运行在非默认端口或者远程服务器上,可以使用以下命令连接:

1
mongosh --host <hostname>:<port>

其中是MongoDB服务器的主机名或 IP 地址,是 MongoDB 服务器的端口号。

第三步,执行基本操作。连接成功后,可以执行各种 MongoDB 数据库操作,如:

  • 查看当前数据库:db
  • 显示数据库列表:show dbs
  • 切换到指定数据库:use <database_name>
  • 执行查询操作:db.<collection_name>.find()
  • 插入文档:db.<collection_name>.insertOne({ ... })
  • 更新文档:db.<collection_name>.updateOne({ ... })
  • 删除文档:db.<collection_name>.deleteOne({ ... })
  • 退出 MongoDB Shell:quit() 或者 exit

第四步,执行一些CRUD操作:

1
2
3
4
5
6
7
8
9
10
# 插入文档
test> db.mycollection.insertOne({ name: "Alice", age: 30 })
# 查询文档
test> db.mycollection.find()
# 更新文档
test> db.mycollection.updateOne({ name: "Alice" }, { $set: { age: 31 } })
# 删除文档
test> db.mycollection.deleteOne({ name: "Alice" })
# 退出 MongoDB Shell
test> quit()

Docker安装MongoDB

第一步,在本地创建数据持久化目录:

1
E:/DockerVolume/mongo

第二步,执行如下命令:

1
docker run -d --name my-mongo7 -p 27117:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=123456 -v E:/DockerVolume/mongo7.0:/data/db mongo:7.0

解释一下上述命令的含义:

(1)-d表示后台运行;

(2)–name my-mongo7表示指定容器名称为my-mongo7;

(3)-p 27117:27017表示将虚拟机的27017端口映射到宿主机的27117端口;

(4)-e MONGO_INITDB_ROOT_USERNAME=admin表示设置超级管理员用户名;

(5)-e MONGO_INITDB_ROOT_PASSWORD=123456表示设置超级管理员密码;

(6)-v E:/DockerVolume/mongo7.0:/data/db表示将容器的/data/db目录挂载到宿主机的E:/DockerVolume/mongo7.0目录。

第三步,验证连接。执行如下命令进入到Docker容器中:

1
docker exec -it my-mongo7 bash

之后切换到bin目录:

1
cd bin

执行如下命令:

1
mongosh -u root -p 123456 --authenticationDatabase admin

出现下面的信息,则表示连接成功:

第四步,使用Navicat进行连接:

SpringBoot整合MongoDB

第一步,在项目POM文件引入mongodb依赖:

1
2
3
4
5
<!-- Spring Boot Starter Data MongoDB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

第二步,在application.properties配置文件中新增如下配置:(这里使用了Docker安装的MongoDB,端口有变化)

1
2
3
4
5
6
7
# mongodb数据库配置
spring.data.mongodb.host=127.0.0.1
spring.data.mongodb.port=27217
spring.data.mongodb.username=root
spring.data.mongodb.password=123456
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.database=chat_memory_db

第三步,新建一个名为pojo的包,并在里面定义一个名为MyChatMessage的类:

1
2
3
4
5
6
7
8
9
10
11
12
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("my_chat_message")
public class MyChatMessage {
//唯一标识,映射到 MongoDB 文档的 _id 字段
@Id
private ObjectId messageId;

//存储当前聊天记录列表的json字符串
private String content;
}

请注意,这里的messageId是唯一标识,用于映射到MongoDB文档的_id字段,它的类型是<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">ObjectId</font>,不可以是字符串。

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">ObjectId</font><font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">MongoDB</font>默认使用的主键类型,它是一个 12 字节<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">BSON</font>类型,通常用作<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">_id</font>字段。<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">ObjectId</font>的值是一个有效的<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">24 字符</font>的十六进制字符串。<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">12*8=24*4=96</font>

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">ObjectId</font> 的值由以下部分组成:

4 字节:当前时间戳(秒级,表示创建的时间)

5 字节:机器标识符和进程ID(唯一)

3 字节:计数器(递增值,用于确保在同一毫秒内创建多个 <font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">ObjectId</font> 时的唯一性)

<font style="color:rgba(0, 0, 0, 0.8);background-color:rgb(247, 247, 249);">new ObjectId()</font>默认使用当前时间戳,当然开发者也可以传入一个时间戳。

第四步,在controller包内定义一个名为MongoController的类,用于测试Mongo的增删改查操作:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
@RestController
@RequestMapping("/mongo")
public class MongoController {
@Autowired
private MongoTemplate mongoTemplate;

@GetMapping("/create")
public String create(){
MyChatMessage message = new MyChatMessage();
message.setMessageId(new ObjectId());
message.setContent("{\"message\":\"Hello, world!\"}");

MyChatMessage saveMessage = mongoTemplate.save(message);
return saveMessage.toString();
}

@GetMapping("/read")
public String read(){
//假设存在一个id为objectId的记录
ObjectId id = new ObjectId("68f46b6133a9f889676511a5");
MyChatMessage readMessage = mongoTemplate.findById(id, MyChatMessage.class);
return readMessage.toString();
}

@GetMapping("/update")
public String update(){
//假设存在一个id为objectId的记录
ObjectId id = new ObjectId("68f46b6133a9f889676511a5");
MyChatMessage readMessage = mongoTemplate.findById(id, MyChatMessage.class);

readMessage.setContent("{\"message\":\"Updated content!\"}");

MyChatMessage updateMessage = mongoTemplate.save(readMessage);
return updateMessage.toString();
}

@GetMapping("/delete")
public String delete(){
//假设存在一个id为objectId的记录
ObjectId id = new ObjectId("68f46b6133a9f889676511a5");
MyChatMessage readMessage = mongoTemplate.findById(id, MyChatMessage.class);

if(readMessage != null){
mongoTemplate.remove(readMessage);
}
MyChatMessage deleteMessage = mongoTemplate.findById(id, MyChatMessage.class);
if(deleteMessage == null){
return "删除成功";
}
return "删除失败";
}
}

第五步,启动项目,访问如下地址来往MongoDB中插入一条数据:

1
http://localhost:8080/mongo/create

页面显示结果如下:

说明数据已经添加成功,查看一下MongoDB数据库:

在read方法中传入之前创建数据的id,之后访问如下地址:

1
http://localhost:8080/mongo/read

页面显示结果如下:

说明我们已经可以根据id来查询对应数据了。在update方法中添加数据的id以及更新后的内容,之后访问如下地址:

1
http://localhost:8080/mongo/update

页面显示结果如下:

说明此时数据被更新了,查看一下MongoDB数据库:

说明数据确实被更新。最后我们在update方法中传入对应数据的id,然后访问如下地址:

1
http://localhost:8080/mongo/delete

页面显示结果如下:

说明数据被成功删除了,这样我们就实现了使用SpringBoot来操作MongoDB的目的。

聊天持久化

优化消息实体类

回到MyChatMessage类中,我们修改一下消息实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@AllArgsConstructor
@NoArgsConstructor
@Document("my_chat_message")
public class MyChatMessage {
//唯一标识,映射到 MongoDB 文档的 _id 字段
@Id
private ObjectId id;

private int messageId;

//存储当前聊天记录列表的json字符串
private String content;
}

这里我们使用id属性作为与MongoDB文档的_id字段进行映射,然后messageId作为备用字段。

创建持久化类

创建一个名为store的包,并在里面定义一个名为MongoChatMemoryStore的类,这个类需要实现ChatMemoryStore接口。这个ChatMemoryStore接口中的代码如下:

1
2
3
4
5
6
7
8
9
10
public interface ChatMemoryStore {
//检索对应的聊天消息列表
List<ChatMessage> getMessages(Object memoryId);

//更新指定memoryId的聊天消息列表
void updateMessages(Object memoryId, List<ChatMessage> messages);

//删除指定memoryId的所有聊天消息
void deleteMessages(Object memoryId);
}

可以看到这个ChatMemoryStore 接口定义了三个方法:

  1. List<ChatMessage> getMessages(Object memoryId),根据memoryId(通常是用户 ID 或会话 ID)检索对应的聊天消息列表。
  2. void updateMessages(Object memoryId, List<ChatMessage> messages),更新指定memoryId的聊天消息列表。每当有新的消息添加到聊天内存中时,LangChain4j会调用此方法。
  3. void deleteMessages(Object memoryId),删除指定memoryId的所有聊天消息。

这些方法允许开发者实现自定义的持久化逻辑,以满足特定的存储需求。

MongoChatMemoryStore类中的代码如下所示:

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
40
41
42
43
44
45
46
47
@Component
public class MongoChatMemoryStore implements ChatMemoryStore {
@Autowired
private MongoTemplate mongoTemplate;

/**
* 获取聊天记录
* @param memoryId
*/
@Override
public List<ChatMessage> getMessages(Object memoryId) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
MyChatMessage chatMessage = mongoTemplate.findOne(query, MyChatMessage.class);
if(chatMessage == null){
return List.of();
}
return ChatMessageDeserializer.messagesFromJson(chatMessage.getContent());
}

/**
* 更新聊天记录
* @param memoryId
* @param messages
*/
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);

Update update = new Update();
update.set("content", ChatMessageSerializer.messagesToJson(messages));
//根据query条件能查询出文档,则修改文档;否则新增文档
mongoTemplate.upsert(query, update, MyChatMessage.class);
}

/**
* 删除聊天记录
* @param memoryId
*/
@Override
public void deleteMessages(Object memoryId) {
Criteria criteria = Criteria.where("memoryId").is(memoryId);
Query query = new Query(criteria);
mongoTemplate.remove(query, MyChatMessage.class);
}
}

简单解释其中的代码含义:

(1)Criteria用于构造查询条件,即where是memoryId等于传入的memoryId;

(2)Query用于封装查询请求,如条件、分页、排序等;

(3)ChatMessageDeserializer.messagesFromJson(),该方法是LangChain4j中的工具类,用于将JSON字符串反序列化为聊天消息对象;

(4)ChatMessageSerializer.messagesToJson(),该方法是LangChain4j中的工具类,用于将聊天消息对象序列化为JSON字符串。

更新配置类信息

回到config包中,之前我们使用SeparateChatAssistantConfig实现了隔离聊天,接下来我们在里面注入持久化对象,并进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class SeparateChatAssistantConfig {
@Autowired
private MongoChatMemoryStore mongoChatMemoryStore;

/**
* 创建一个ChatMemoryProvider,实现会话隔离
*/
@Bean
ChatMemoryProvider chatMemoryProvider(){
return memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(mongoChatMemoryStore)
.build();
}
}

可以看到,这里使用MongoChatMemoryStore来存储会话,实现聊天消息持久化功能。

启动项目进行测试

启动项目,访问如下地址:

1
http://localhost:8080/test/testChatMemoryV4

然后查看一下MongoDB数据库,可以看到聊天记录已经存储了: