LangChain4j 记忆化(ChatMemory)

1 阅读4分钟

ChatMemory

无记忆

image-20260213012903165

有记忆

image-20260213020452113

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);
}