LangChain4j 记忆化(ChatMemory)

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 服务接口

java 复制代码
public interface MemoryAssistant {
    Flux<String> chatWithMemory(@MemoryId Long userId, @UserMessage String prompt);
}
  • userId 标识该消息的所属人
  • prompt 提示词信息

配置模型信息,以及记忆化配置。

java 复制代码
@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

java 复制代码
@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);
                }
            });
        });
    }
}

补充

java 复制代码
Assistant assistant = AiServices.builder(Assistant.class)
    .chatModel(model)
    .chatMemory(MessageWindowChatMemory.withMaxMessages(10))
    .build();

在这种情况下,所有对 AI 服务的调用都将使用同一个 ChatMemory 实例。但是,如果有多个用户,这种方法就行不通了,因为每个用户都需要自己的实例来ChatMemory 维护各自的对话。

解决此问题的方法是使用 ChatMemoryProvider。(就是我们前面使用的方式)

删除记忆

java 复制代码
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 作为持久化存储方式。

java 复制代码
@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);
    }
}

配置模型,使用自定义持久化存储。

java 复制代码
@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();
    }
}

自定义服务接口

java 复制代码
public interface ChatPersistenceAssistant {
    String chat(@MemoryId Long memoryId, @UserMessage String message);
}
相关推荐
葫芦和十三6 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp6 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑7 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯7 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan10 小时前
多Agent之间的区别
后端
青石路11 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充12 小时前
1.面向对象设计思想
后端
IT_陈寒12 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro13 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗13 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端