ChatMemory
无记忆

有记忆

Memory VS History
记忆(Memory)和历史记录(History)它们是相似,但不同的概念。
- 历史记录会完整的保存用户与 AI 之间的所有消息,代表了实际发生过的所有对话。
- 记忆会保留一些信息,这些信息会呈现给 LLM(生命周期管理模型),使其表现得好像“记住”了对话。记忆是短暂的,它不像历史记录那样可以永久保存,我们总是会忘记一些记忆的,就像人一样。
LangChain4j 目前仅提供 Memory 管理,不提供 History 管理。如果你需要保留完整的历史记录,请手动管理。
回收策略
因为以下的原因,指定回收策略是必要的:
- 为了适应 LLM 的上下文窗口,LLM 一次可以处理的令牌数量是有上限的。在某些情况下,对话可能会超过这个限制。在这种情况下,应该移除一些消息。通常情况下,会移除最旧的消息,但如有必要,也可以实现更复杂的算法。
- 为了控制成本。每个令牌都有成本,因此每次调用 LLM 的成本都会逐渐增加。清除不必要的消息可以降低成本。
- 为了控制延迟。发送到 LLM 的令牌越多,处理它们所需的时间就越长。
目前,LangChain4j 提供了两种实现方式:
MessageWindowChatMemory是一种简单方式,它以滑动窗口的形式运行,保留 N 条最近消息并移除超出限制的旧消息。但由于每条消息包含的 token 数量可能不同,因此该方案主要用于快速原型开发。TokenWindowChatMemory是一种更复杂的方式,它以滑动窗口的形式运行,保留 N 个最新 token,并根据需要移除旧消息。消息是不可分割的,作为不可分割的整体处理,如果某条消息无法完全容纳,则将被整体移除。TokenWindowChatMemory需要使用TokenCountEstimator来统计每条ChatMessage的 token 数量。
两种方式的演示
定义 AI 服务接口
public interface MemoryAssistant {
Flux<String> chatWithMemory(@MemoryId Long userId, @UserMessage String prompt);
}
userId标识该消息的所属人prompt提示词信息
配置模型信息,以及记忆化配置。
@Configuration
public class LLMConfig {
@Bean
public StreamingChatModel streamingChatModel() {
return OpenAiStreamingChatModel.builder()
.apiKey(System.getenv("ALI_QWEN_API_KEY"))
.modelName("qwen-plus")
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.build();
}
@Bean("chatMessageWindowChatMemory")
public MemoryAssistant chatMessageWindowChatMemory(StreamingChatModel streamingChatModel) {
return AiServices.builder(MemoryAssistant.class)
.streamingChatModel(streamingChatModel)
// 这种设置方式可以用 memoryId 作为记忆化参数
.chatMemoryProvider(memoryId -> TokenWindowChatMemory.builder().id(memoryId).build())
// 这种设置方式,并不是采用的 memoryId,而是采用的默认 ID
// .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(100))
.build();
}
@Bean("chatTokenWindowChatMemory")
public MemoryAssistant chatTokenWindowChatMemory(StreamingChatModel streamingChatModel) {
return AiServices.builder(MemoryAssistant.class)
.streamingChatModel(streamingChatModel)
// 这种设置方式可以用 memoryId 作为记忆化参数
.chatMemoryProvider(memoryId -> TokenWindowChatMemory.builder().id(memoryId).build())
.build();
}
}
MemoryController
@RestController
@RequestMapping("memory")
@Slf4j
@CrossOrigin
public class StreamController {
@Resource
private StreamingChatModel streamingChatModel;
@Resource(name = "chatMessageWindowChatMemory")
private MemoryAssistant chatMessageWindowChatMemory;
@Resource(name = "chatTokenWindowChatMemory")
private MemoryAssistant chatTokenWindowChatMemory;
// 只演示 chatMessageWindowChatMemory
// chatTokenWindowChatMemory 同理可得
@GetMapping("/qwen/chat2")
public Flux<String> chat2(@RequestParam(value = "userId") Long userId,
@RequestParam(value = "question", defaultValue = "你是谁?") String question) {
return Flux.create(e -> {
streamingChatModel.chat(question, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String s) {
e.next(s);
}
@Override
public void onCompleteResponse(ChatResponse chatResponse) {
e.complete();
}
@Override
public void onError(Throwable throwable) {
e.error(throwable);
}
});
});
}
}
补充
Assistant assistant = AiServices.builder(Assistant.class)
.chatModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
在这种情况下,所有对 AI 服务的调用都将使用同一个 ChatMemory 实例。但是,如果有多个用户,这种方法就行不通了,因为每个用户都需要自己的实例来ChatMemory 维护各自的对话。
解决此问题的方法是使用 ChatMemoryProvider。(就是我们前面使用的方式)
删除记忆
String answerToKlaus = assistant.chat(1, "Hello, my name is Klaus");
String answerToFrancine = assistant.chat(2, "Hello, my name is Francine");
// 获取 memoryId = 1 的所有消息
List<ChatMessage> messagesWithKlaus = assistant.getChatMemory(1).messages();
// 删除 memoryId = 2 的所有消息
boolean chatMemoryWithFrancineEvicted = assistant.evictChatMemory(2);
持久化
默认情况下,ChatMemory 实现会将 ChatMessage 存储在内存中。
如果需要持久化存储,可以实现自定义的 ChatMemoryStore,将 ChatMessage 存储在你选择的任意媒介中。
演示,使用 Redis 作为持久化存储方式。
@Component
public class RedisChatMemoryStore implements ChatMemoryStore {
public static final String CHAT_MEMORY_PREFIX = "CHAT_MEMORY:";
@Resource
private RedisTemplate<String, String> redisTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String value = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId);
return ChatMessageDeserializer.messagesFromJson(value);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
redisTemplate.opsForValue()
.set(CHAT_MEMORY_PREFIX + memoryId, ChatMessageSerializer.messagesToJson(messages));
}
@Override
public void deleteMessages(Object memoryId) {
redisTemplate.delete(CHAT_MEMORY_PREFIX + memoryId);
}
}
配置模型,使用自定义持久化存储。
@Configuration
public class LLMConfig {
@Resource
private RedisChatMemoryStore redisChatMemoryStore;
...
public ChatPersistenceAssistant chatPersistenceAssistant(StreamingChatModel streamingChatModel) {
ChatMemoryProvider provider = memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(1000)
.chatMemoryStore(redisChatMemoryStore) // 使用自定义持久化存储
.build();
return AiServices.builder(ChatPersistenceAssistant.class)
.streamingChatModel(streamingChatModel)
.chatMemoryProvider(provider)
.build();
}
}
自定义服务接口
public interface ChatPersistenceAssistant {
String chat(@MemoryId Long memoryId, @UserMessage String message);
}