一、ChatMemory
由于手动维护和管理ChatMessages很麻烦,LangChain4j提供了ChatMemory抽象以及多个开箱即用的实现。
ChatMemory可以作为独立的低级组件来使用,也可以作为高级组件(AiService)的一部分使用。
ChatMemory作为ChatMessages的容器,它有如下附加功能:
- 逐出策略
- 持久化
- SystemMessage的特殊处理
- 工具信息的特殊处理
二、记忆与历史
记忆与历史是两个不同的概念,我们要做好区分:
历史记录:它保留的是用户与AI之间的所有信息。用户可以在UI中看到的内容,代表了之前用户与AI之间所发生过的对话。
记忆:保存了一些信息,这些信息会呈现给LLM,使其看上去就像记住了之前对话一样,它与历史不同,会根据所使用的内存算法,可以以各种方式修改历史记录、汇总单独的消息、从消息中删除不重要的细节、向消息中注入额外的信息或指令。
LangChain4j当前只提供记忆而不会提供历史,如果我们要保存历史则需要我们自行实现对过对话信息的保存。
三、逐出策略
逐出策略非常重要也是必须的。原因如下:
- LLM的上下文窗口一次可以处理的令牌长度是有限的。在有些情况下,会话超过这个限制则逐出一些消息,而逐出策略就是用来控制哪些消息要逐出。我们最为常用的策略是:最老的消息被逐出
- 控制成本。每个令牌都是有一定的成本的,每次对LLM的调用逐渐会变得更加昂贵,为了降低这个成本可以考虑清除掉不必要的消息
- 加快响应。发送给到LLM的令牌越多,处理它们所需要的时间则会越多
LangChain4j当前提供了两个开箱即用的实现
- MessageWindowChatMemory:作为滑动窗口,保留最新的N条消息,并逐出不再合适的旧消息。
- TokenWindowChatMemory:作为滑动窗口,保留N个最新的令牌,根据需要逐出旧消息。使用这个的话需要有一个Tokenizer来计算每个ChatMessage中的令牌
四、持久化
默认情况下,我们的记忆的消息会存储到内存当中,如果我们需要进行持久化的存储,则可以自定义ChatMemoryStore,把ChatMessage存储到我们自己的持化久设备当中。如下所示的实现方式
class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
// TODO: Implement getting all messages from the persistent store by memory ID.
// ChatMessageDeserializer.messageFromJson(String) and
// ChatMessageDeserializer.messagesFromJson(String) helper methods can be used to
// easily deserialize chat messages from JSON.
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
// TODO: Implement updating all messages in the persistent store by memory ID.
// ChatMessageSerializer.messageToJson(ChatMessage) and
// ChatMessageSerializer.messagesToJson(List<ChatMessage>) helper methods can be used to
// easily serialize chat messages into JSON.
}
@Override
public void deleteMessages(Object memoryId) {
// TODO: Implement deleting all messages in the persistent store by memory ID.
}
}
构造ChatMemory对象的方式如下:
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.id("12345")
.maxMessages(10)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();
关于ChatMemoryStore接口中的方法说明:
updataMessages()方法,在每次向ChatMemory添加ChatMessage时被调用。通常在每次与LLM交互时会调用两次,一次在添加UserMessage时,一次在添加新的AiMessage时。这个方法会更新与给定的MemoryId关联的所有消息。
注意:从ChatMemory中删除消息也会从ChatMemoryStore中删除。当消息被逐出时,会调用updateMessages()方法,更新时消息内容是不包含被逐出的消息列表。
每当ChatMemory的用户请求所有消息时,会调用getMessages()方法。这个方法通常与LLM每次交互中发生一次。其中MemoryId参数的值对应于ChatMemory创建时指定的id。
当调用ChatMemory.clear()时,会调用deleteMessages()方法。如果不希望一次性清空可以把这个deleteMessages()方法留空。
持久化聊天记忆实例
第一步:pom文件新增依赖如下:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
mybatis-spirng-boot-starter版本:3.0.4
druid-spirng-boot-3-starter版本:1.2.23
mysql-connector-j沿用了Springboot3.3.5的集成依赖8.3.0
第二步:新增一个关于数据库连接的配置文件application-druid.yml
# 数据源配置
spring:
datasource:
druid:
driverClassName: com.mysql.cj.jdbc.Driver
# 默认据源
url: jdbc:mysql://localhost:3306/llm?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
username: root
password: A7#mZ9!pL3$q
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置连接超时时间
connectTimeout: 30000
# 配置网络超时时间
socketTimeout: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
webStatFilter:
enabled: true
statViewServlet:
enabled: true
# 设置白名单,不填则允许所有访问
allow:
url-pattern: /druid/*
# 控制台管理用户名和密码
login-username: druid
login-password: 123456
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
第三步:主配置文件(application.yml)中新增如下内容:
spring:
profiles:
active: druid
################# mybatis ###############
mybatis:
mapper-locations: classpath:mapper/**/*Mapper.xml
type-aliases-package: com.xiaoxie.**.domain
configuration:
map-underscore-to-camel-case: true
type-handlers-package: com.xiaoxie.**.mapper.typehandler
logging:
level:
com.xiaoxie: debug
org.springframework: warn
org.mybatis.spring: debug # MyBatis Spring 框架的日志
org.apache.ibatis: debug # MyBatis 核心包的日志
java.sql: debug
注意:关于Springboot3的依赖,langchain4j的依赖,langchain4j集成百炼平台Springboot的starter,以及大语言模型的配置请参考:01、聊天与语言模型-CSDN博客 中的说明!!
第四步:数据库中新增表用来存储聊天记忆信息
CREATE TABLE `chat_memory_message` (
`id` int NOT NULL AUTO_INCREMENT,
`memory_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '',
`message` json DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
第五步:新增domain包及实体类:ChatMemoryMessage
@Data
public class ChatMemoryMessage {
private Integer id;
private String memoryId;
private String message;
}
第六步:新增mapper接口:ChatMemoryMessageMapper
@Mapper
public interface ChatMemoryMessageMapper {
String getByMemoryId(@Param("memoryId") String memoryId);
void insert(ChatMemoryMessage chatMemory);
void update(ChatMemoryMessage chatMemory);
void deleteByMemoryId(@Param("memoryId") String memoryId);
}
第七步:在resources/mapper下新增ChatMemoryMessageMapper.xml映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxie.chat.mapper.ChatMemoryMessageMapper">
<insert id="insert">
insert into chat_memory_message (memory_id, message) values (#{memoryId}, #{message})
</insert>
<update id="update">
update chat_memory_message set message = #{message} where memory_id = #{memoryId}
</update>
<delete id="deleteByMemoryId">
delete from chat_memory_message where memory_id = #{memoryId}
</delete>
<select id="getByMemoryId" resultType="java.lang.String">
select message from chat_memory_message where memory_id = #{memoryId}
</select>
</mapper>
第八步:新增一个manager包及存储聊天记忆的存储实现类:ChatMessageStoreManager
@Component
public class ChatMessageStoreManager implements ChatMemoryStore {
@Resource
private ChatMemoryMessageMapper chatMemoryMessageMapper;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String messages = chatMemoryMessageMapper.getByMemoryId(memoryId.toString());
List<ChatMessage> chatMessages = ChatMessageDeserializer.messagesFromJson(messages);
return chatMessages;
}
@Transactional
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String messagesJson = ChatMessageSerializer.messagesToJson(messages);
ChatMemoryMessage chatMemoryMessage = new ChatMemoryMessage();
chatMemoryMessage.setMemoryId(memoryId.toString());
chatMemoryMessage.setMessage(messagesJson);
if (chatMemoryMessageMapper.getByMemoryId(memoryId.toString()) != null) {
chatMemoryMessageMapper.update(chatMemoryMessage);
} else {
chatMemoryMessageMapper.insert(chatMemoryMessage);
}
}
@Transactional
@Override
public void deleteMessages(Object memoryId) {
chatMemoryMessageMapper.deleteByMemoryId(memoryId.toString());
}
}
注意:我们这里每个memoryIdd存储为一行记录,所以在updateMessages()方法中当不存在给定memoryId时进行新增,存在时进行更新!
第九步:新增一个constant包及公共常量类Constatns
public class Constants {
public static final Integer DEFAULT_MEMORY_LENGTH = 10; // 默认记忆长度
}
第十步:新增controller类:MemoryChatController
@Slf4j
@RestController
@RequestMapping("/memoryChat")
public class MemoryChatController {
// 注入聊天大语言模型
@Resource
private ChatLanguageModel chatLanguageModel;
@Resource
private ChatMessageStoreManager chatMessageStoreManager;
@GetMapping("/chat")
public String chat(@RequestParam("message") String message, @RequestParam("memoryId") String memoryId) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(Constants.DEFAULT_MEMORY_LENGTH)
.chatMemoryStore(chatMessageStoreManager)
.build();
chatMemory.add(UserMessage.from(message));
AiMessage content = chatLanguageModel.generate(chatMemory.messages()).content();
chatMemory.add(content);
// 清理记忆是需要手动调用的,注意,这里的清理会把指定的memoryId的memory全部清理掉
// chatMemory.clear();
return content.text();
}
}
此时我们与AI进行聊天时要根据不同的memoryId它会存储用户信息 + AI返回信息共十条记录
对于SystemMessage特殊处理
SystemMessage是一种特殊类型的消息,所以在聊天中它与其它消息处理方式不一样
- 一旦添加它之后,SystemMessage会一直保留
- 一次只能持有一个SystemMessage
- 如果添加了具有相同内容的SystemMessage,则会忽略
- 如果添加了不同内容的SystemMessage,那么会替换以前的SystemMessage
对于toolMessage的特殊处理
如果包含ToolExecutionRequests的AiMessage被逐出,下面的ToolExecutionResultMessages也会被逐出。
注意:具体关于tool工具函数的调用后续再说明