当前位置: 首页 > article >正文

* LangChain4j中的会话记忆ChatMemory

在构建 AI 志愿填报顾问时一个很自然的期望是它能记住我们之前聊过什么而不是每次都像第一次见面一样。大模型本身是无状态的每次调用都是独立的要实现“记忆”唯一的方法就是把聊天历史连同新问题一起发给模型。LangChain4j 用一套简洁的抽象帮我们搞定了这件事从最基本的会话记忆到多用户隔离再到持久化存储让记忆能力变得健壮且透明。一、会话记忆的原理先搞清楚 LangChain4j 是怎么让大模型“记住”的用户发送消息“西北大学是211吗”给后端。后端将这条消息存入一个会话记忆存储对象然后把存储对象里的所有历史消息一起发给大模型。大模型根据完整的上下文生成回答比如“是的西北大学是211高校”。后端收到回答后再把这条回答也存进记忆存储对象然后才返回给用户。用户接着问“那它是985吗”这条消息再次存入记忆对象此时记忆对象里已有三轮对话全部发给大模型它就能根据之前的上下文推断出用户还在问西北大学。整个过程 LangChain4j 自动完成我们几乎不用写额外的代码。二、基础实现MessageWindowChatMemoryLangChain4j 提供了ChatMemory接口来抽象记忆存储核心方法就三个add(ChatMessage)添加一条消息messages()获取所有消息clear()清空记忆同时提供了MessageWindowChatMemory实现类它用一个固定大小的滑动窗口来保留最近的消息防止上下文过长。2.1 定义会话记忆 Bean注册一个MessageWindowChatMemory的Bean设置最大保留条数建议20条Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); }为什么限制数量一是大模型有上下文窗口限制二是按 token 计费发得越多越贵。窗口大小是一个实用的折中。2.2 把记忆挂载到 AiService 上在AiService注解中通过chatMemory属性关联刚定义的 BeanAiService( wiringMode AiServiceWiringMode.EXPLICIT, chatModel openAiChatModel, streamingChatModel openAiStreamingChatModel, chatMemory chatMemory ) public interface ConsultantService { SystemMessage(fromResource system.txt) FluxString chat(String message); }启动后测试多轮对话就能看到上下文关联效果。但一个问题马上暴露出来所有用户共用同一个 ChatMemory张三问的学校会被李四的对话干扰这显然不行。补充如果使用AiService的默认 wiring 模式AUTO且容器中只有一个ChatMemoryBean部分版本可能会自动注入本文的版本就可以。但题中指定了wiringMode EXPLICIT并且需要严谨控制因此必须显式配置 2.2 才能生效。建议始终显式关联避免歧义。三、会话记忆隔离一个用户一个记忆隔离的核心思路是给每个会话一个唯一标识memoryIdLangChain4j 会为每个memoryId维护独立的ChatMemory实例。3.1 原理用户请求时携带memoryId比如session-123。LangChain4j 在一个内部容器中查找id为session-123的ChatMemory。如果没找到就通过ChatMemoryProvider创建一个新的ChatMemory并关联该 ID如果找到了直接复用。3.2 定义 ChatMemoryProvider不再需要单一的chatMemoryBean而是提供一个ChatMemoryProvider它负责按需创建新的ChatMemory对象并设置 ID。Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId - MessageWindowChatMemory.builder() .id(memoryId) // 关键绑定唯一ID .maxMessages(20) .build(); }3.3 接口增加 MemoryId 参数在ConsultantService的chat方法上新增一个MemoryId参数同时因为现在有多个参数需用UserMessage明确哪个是用户输入public interface ConsultantService { SystemMessage(fromResource system.txt) FluxString streamChat(MemoryId String memoryId, UserMessage String message); }3.4 Controller 配合前端在调用/chat接口时带上memoryId一般由前端生成并维持Controller 直接透传RequestMapping(value /streamChat, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chat(RequestParam(memoryId) String memoryId, RequestParam(message) String message) { return consultantService.streamChat(memoryId, message); }现在不同memoryId的对话完全隔离互不干扰。四、持久化重启也不失忆目前我们用MessageWindowChatMemory默认内置的SingleSlotChatMemoryStore它的底层是一个内存ArrayList服务重启记忆全丢。要解决这个问题需要把会话记录存到外部存储如 Redis。4.1 原理MessageWindowChatMemory的内部其实委托给一个ChatMemoryStore接口做实际的增删查public interface ChatMemoryStore { ListChatMessage getMessages(Object memoryId); void updateMessages(Object memoryId, ListChatMessage messages); void deleteMessages(Object memoryId); }我们只需要提供一个实现了该接口的 Redis 版本然后注入给MessageWindowChatMemory。4.2 准备 Redis 环境安装并启动 Redis本地用 Docker 一行命令docker run --name redis -d -p 6379:6379 redis引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency配置连接spring: data: redis: host: localhost port: 63794.3配置Redis配置类package com.langchan4jSpringBoot.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.cache.RedisCacheWriter; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.*; import java.time.Duration; /** * Redis配置类 * 负责配置RedisTemplate序列化方式、缓存管理器以及自定义key前缀策略 */ Configuration EnableCaching public class RedisConfig { // 从配置文件中读取Redis key的统一前缀 Value(${spring.data.redis.key-prefix}) private String keyPrefix; /** * 配置RedisTemplate * - key使用带前缀的String序列化 * - value使用Jackson JSON序列化 */ Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // key采用带统一前缀的key序列化器 template.setKeySerializer(prefixRedisSerializer()); // hash的key也采用带统一前缀的key序列化器 template.setHashKeySerializer(prefixRedisSerializer()); // value序列化方式采用jackson template.setValueSerializer(valueSerializer()); // hash的value序列化方式采用jackson template.setHashValueSerializer(valueSerializer()); template.afterPropertiesSet(); return template; } /** * 配置Spring Cache缓存管理器 * - 使用非锁定的RedisCacheWriter * - 自定义缓存key前缀拼接规则 * - value采用Jackson JSON序列化 * - 默认永不过期 */ Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheWriter redisCacheWriter RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory); RedisCacheConfiguration redisCacheConfiguration RedisCacheConfiguration.defaultCacheConfig() .computePrefixWith(name - { if (name.endsWith(:)) { return keyPrefix.concat(:).concat(name); } return keyPrefix.concat(:).concat(name).concat(:); }) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer())); redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); //过期时间设置为 Duration#ZERO永远不过期 redisCacheConfiguration.entryTtl(Duration.ZERO); return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration); } /** * 创建带统一前缀的key序列化器 */ private RedisSerializerString prefixRedisSerializer() { return new PrefixRedisSerializer(keyPrefix :); } /** * 自定义前缀序列化器 * 在序列化时自动为key添加前缀实现多应用共用Redis时的命名空间隔离 */ static class PrefixRedisSerializer implements RedisSerializerString { /** * 委托给默认的String序列化器处理实际的字节转换 */ private final RedisSerializerString delegate RedisSerializer.string(); /** * key前缀如 app: */ private final String prefix; public PrefixRedisSerializer(String prefix) { this.prefix prefix; } Override public byte[] serialize(String s) throws SerializationException { return delegate.serialize(prefix s); } Override public String deserialize(byte[] bytes) throws SerializationException { return delegate.deserialize(bytes); } } /** * 使用Jackson序列化器 * * return */ private RedisSerializerObject valueSerializer() { ObjectMapper objectMapper new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return new GenericJackson2JsonRedisSerializer(objectMapper); } }4.4实现 RedisChatMemoryStorepackage com.langchan4jSpringBoot.repository; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.ChatMessageDeserializer; import dev.langchain4j.data.message.ChatMessageSerializer; import dev.langchain4j.store.memory.chat.ChatMemoryStore; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Repository; import java.util.List; import java.util.concurrent.TimeUnit; Repository public class RedisChatMemoryStore implements ChatMemoryStore { /** * Redis key前缀完整key格式为 chat:memory:{conversationId} */ private static final String KEY_PREFIX chat:memory:; /** * 会话记录过期天数 */ private static final int TTL_DAYS 3; Autowired private StringRedisTemplate stringRedisTemplate; Override public ListChatMessage getMessages(Object memoryId) { String json stringRedisTemplate.opsForValue().get(KEY_PREFIX memoryId); return ChatMessageDeserializer.messagesFromJson(json); } Override public void updateMessages(Object memoryId, ListChatMessage list) { String json ChatMessageSerializer.messagesToJson(list); stringRedisTemplate.opsForValue().set(KEY_PREFIX memoryId, json, TTL_DAYS, TimeUnit.DAYS); } Override public void deleteMessages(Object memoryId) { stringRedisTemplate.delete(KEY_PREFIX memoryId); } }序列化/反序列化直接使用 LangChain4j 自带的ChatMessageSerializer/ChatMessageDeserializer非常方便。4.5 将 Redis Store 注入 ChatMemoryProviderAutowired private ChatMemoryStore redisChatMemoryStore; Bean public ChatMemoryProvider chatMemoryProvider() { return memoryId - MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .chatMemoryStore(redisChatMemoryStore) // 使用 Redis 持久化 .build(); }现在即使服务重启只要 Redis 还在对话记忆就不会丢失。还可以通过设置过期时间如Duration.ofDays(1)实现自动清理。五、总结从无记忆到带隔离的持久化记忆我们层层递进地解决了会话记忆的核心问题需求实现方式关键配置基础记忆MessageWindowChatMemorychatMemory chatMemory多用户隔离ChatMemoryProviderMemoryIdchatMemoryProvider chatMemoryProvider持久化存储自定义ChatMemoryStoreRedis.chatMemoryStore(redisChatMemoryStore)LangChain4j 把记忆管理彻底从业务代码中抽离我们只需要在接口层面做声明就能获得完整的多轮对话能力。接下来再结合 RAG 知识库我们的 AI 志愿填报顾问就能既“记得住”又“懂得新”真正成为一个靠谱的助手。

相关文章:

* LangChain4j中的会话记忆ChatMemory

在构建 AI 志愿填报顾问时,一个很自然的期望是它能记住我们之前聊过什么,而不是每次都像第一次见面一样。大模型本身是无状态的,每次调用都是独立的,要实现“记忆”,唯一的方法就是把聊天历史连同新问题一起发给模型。…...

2026年吃油腻重口后的脾虚湿热腹泻辨证用药与中成药选购参考

日常饮食中,若长期或一次性摄入过多油腻、辛辣、重口味食物,可能会引发肠胃不适的一种常见类型。这类情况的相关知识、公开产品信息整理如下,本文仅做日常健康科普,不构成诊断、治疗或用药建议。一、公开提到的该类型肠胃不适的常…...

如何快速掌握串口数据可视化:SerialPlot终极完整教程

如何快速掌握串口数据可视化:SerialPlot终极完整教程 【免费下载链接】serialplot Small and simple software for plotting data from serial port in realtime. 项目地址: https://gitcode.com/gh_mirrors/se/serialplot 想象一下,你正在调试一…...

HTR6916 共阴极16x9阵列LED驱动器 聚能芯半导体禾润电子一级代理

概述HTR6916 是一款功能卓越的 LED 驱动芯片。它可通过 2 线串行接口进行编程,能够完美支持 169 阵列的 LED 布局。芯片中的每一颗 LED 均能借助 8 位 PWM 数据实现独立调光,为用户提供了高度灵活的调光方案。此外,用户还能通过 ISET 端的外部…...

行业内热门的饲料颗粒机厂家哪家靠谱

在饲料生产链条中,颗粒机作为核心成型设备,其性能直接关系到饲料品质、能耗水平以及综合运营成本。然而,当前行业内部分产品仍面临显著的技术瓶颈,制约着生产效率的进一步提升。本文将深入剖析行业痛点,并以荥阳市光辉…...

Healthy Care辅酶Q10怎么选?

当代社会,心脏健康养护早已不是中老年人的专属需求。长期熬夜的年轻人、高压职场人群、作息紊乱的轮班从业者、体力消耗偏大的服务行业工作者,都容易出现心脏能量不足的信号:爬楼容易气喘、安静状态下莫名心慌、睡眠充足却依旧浑身疲惫。这类…...

苏州晟雅泰电子:关于长鑫存储与兆易创新的关系

长鑫存储(及其母公司长鑫科技)与兆易创新的关系极为紧密,是由一位核心人物——董事长朱一明联结而成的深度战略联盟。这两家公司在股权、人事和业务等多个层面相互绑定,形成了“一个核心、两个支点”的独特格局。以下是其关系的具…...

PowerToys中文汉化:3分钟让微软效率工具变身中文版

PowerToys中文汉化:3分钟让微软效率工具变身中文版 【免费下载链接】PowerToys-CN PowerToys Simplified Chinese Translation 微软增强工具箱 自制汉化 项目地址: https://gitcode.com/gh_mirrors/po/PowerToys-CN 还在为PowerToys的英文界面发愁吗&#xf…...

【NotebookLM关键词提取实战指南】:20年AI工程师亲授3步精准提取法,90%用户忽略的隐藏参数曝光

更多请点击: https://kaifayun.com 第一章:NotebookLM关键词提取的核心原理与适用场景 NotebookLM 是 Google 推出的面向研究者与知识工作者的 AI 助手,其关键词提取能力并非依赖传统 TF-IDF 或 TextRank 等静态统计方法,而是深度…...

ESP32蓝牙音频终极指南:3个实用技巧轻松实现A2DP音乐传输 [特殊字符]

ESP32蓝牙音频终极指南:3个实用技巧轻松实现A2DP音乐传输 🎵 【免费下载链接】ESP32-A2DP A Simple ESP32 Bluetooth A2DP Library (to implement a Music Receiver or Sender) that supports Arduino, PlatformIO and Espressif IDF 项目地址: https:…...

智慧巡检-基于深度学习的指针式压力表读数识别【YOLO+OpenCv+TensorRT+ROS+Python】

智慧巡检-基于深度学习的指针式压力表读数识别【YOLOOpenCvTensorRTROSPython】 1指针式压力表读数识别系统(YOLOOpenCVTensorRTROS)一、系统整体架构 ┌──────────────────────────────────────────────…...

Global Mapper 26空间数据转换安装教程 Windows版:自定义路径+Crack替换指南

它以出色的格式兼容性(支持数百种矢量和栅格格式,如 CAD、SHP、GeoTIFF 等)和高效的数据处理能力著称,广泛应用于测绘、林业、矿业及土地规划领域。 一、准备工作 安装包下载:https://pan.quark.cn/s/a361bbddd854&a…...

C++中的函数知识点大全

函数的定义不能嵌套但调用可以嵌套在函数调用时,如某一默认参数要指明一个特定值,则有其之前所有参数都必须赋值赋默认实参时 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值,因为设置默认参数的顺序是自右向左&…...

AI Agent 上线后,别只看成功率:你需要一套可观测性指标

很多团队做 AI Agent,上线前会问一个问题: “成功率多少?” 这当然要看。 但只看成功率,很容易误判。 因为 AI Agent 的问题不是简单的成功或失败。 它可能成功调用了工具,但参数是错的。 它可能生成了回复&#xff0c…...

构造函数、this指向和原型链机制

今天在刷力扣 [146. LRU 缓存](https://leetcode.cn/problems/lru-cache/) 的时候,遇到了原型链的写法,想想这个写法我正式开发中从来都没有用过,到底是个什么玩意?遂将各个节点和变量都定义在外面,但是代码居然报错啦…...

【Coze工作流】零代码做AI自动化,小白也能5分钟上手

一、问题背景:手工做重复AI任务太累,想自动化但不会写代码在日常办公或者内容创作中,很多人都有过这样的痛点:每天要重复打开各种AI工具。比如你要写一篇爆款文章,先要找AI找选题,再让AI写大纲,…...

PowerToys汉化指南:3步让英文效率工具变成你的中文助手

PowerToys汉化指南:3步让英文效率工具变成你的中文助手 【免费下载链接】PowerToys-CN PowerToys Simplified Chinese Translation 微软增强工具箱 自制汉化 项目地址: https://gitcode.com/gh_mirrors/po/PowerToys-CN 你是不是曾经因为PowerToys的英文界面…...

通过Taotoken CLI工具一键配置多开发环境接入参数

🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 通过Taotoken CLI工具一键配置多开发环境接入参数 在接入大模型服务时,开发者常常需要为不同的开发工具(如…...

我用AI一周做了个口播视频平台,现在开源了

做独立开发这两年,我一直在想一个问题:一个人到底能做到什么程度? 上周我给出了自己的答案——我用 DeepSeek 定义需求 CodeBuddy 辅助编码,一个人从零搞了一个 AI 口播视频生成平台,取名智播坊。输入文案&#xff0…...

Java类与对象:编程核心解密

好的,我们来详细解释一下Java中的类和对象这两个核心概念。1. 类 (Class)定义:类是一个模板或蓝图。它定义了某一类“事物”的共同特征(属性)和行为(方法)。作用:类描述了该种“事物”具有哪些信…...

git reset 怎么用?2026年最完整操作指南,撤销提交不再手足无措

代码提交了才发现写错了,或者本地 commit 堆了一堆想整理——你是直接新建一个"撤回"commit,还是对着搜索结果一脸茫然不敢乱动? 如果你还没搞清楚 git reset 的三种模式,随时可能把代码撤没了。学完本文,你…...

easyPoi使用

一、核心定位区别 EasyPoi:全能型,支持 Excel、Word、PDF 导出,注解极简,适合小数据、快速开发EasyExcel:高性能型,只专注 Excel,主打低内存、大数据量,适合海量数据导出 二、Easy…...

My-TODOs:跨平台桌面待办清单,解放您的生产力

My-TODOs:跨平台桌面待办清单,解放您的生产力 【免费下载链接】My-TODOs A cross-platform desktop To-Do list. 跨平台桌面待办小工具 项目地址: https://gitcode.com/gh_mirrors/my/My-TODOs 在信息过载的今天,如何高效管理日常任务…...

2026 年 AI 工具聚合站:从模型入口到开发基础设施的进化之路

在 2026 年的 AI 开发生态中,开发者正面临一个矛盾的现状:一方面是 GPT-5.5、Claude 4.7、Gemini 3.1 等大模型的能力持续突破,带来了前所未有的技术红利;另一方面,模型碎片化、接口异构化、成本高企等问题&#xff0c…...

咖啡一杯,Token 无限,Real-Time Cafe 深圳站来了!新增「硬件晒晒桌」与「AI 桌游试玩桌」

咖啡一杯,Token 无限——「Real-Time Cafe」是一个让开发者聚在一起实时 coding、实时 debug、实时互动的咖啡馆快闪计划。 Real-Time Cafe 深圳站来了!就在本周日 5 月 24 日下午。 本站特设「硬件晒晒桌」与「AI 桌游试玩桌」——带上你的电子宠物、…...

3分钟掌握R3nzSkin:英雄联盟国服免费全皮肤终极方案

3分钟掌握R3nzSkin:英雄联盟国服免费全皮肤终极方案 【免费下载链接】R3nzSkin-For-China-Server Skin changer for League of Legends (LOL) 项目地址: https://gitcode.com/gh_mirrors/r3/R3nzSkin-For-China-Server 想在英雄联盟国服免费体验所有皮肤吗&a…...

3个关键技术方案:如何系统化解决Navicat Premium试用期限制

3个关键技术方案:如何系统化解决Navicat Premium试用期限制 【免费下载链接】navicat_reset_mac navicat mac版无限重置试用期脚本 Navicat Mac Version Unlimited Trial Reset Script 项目地址: https://gitcode.com/gh_mirrors/na/navicat_reset_mac 本文旨…...

终极KMS激活解决方案:Windows与Office一体化智能激活工具

终极KMS激活解决方案:Windows与Office一体化智能激活工具 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO KMS_VL_ALL_AIO是一款功能强大的Windows系统和Microsoft Office套件的智能激…...

历史性突破,OpenAI模型搞定人类科学家80年未破难题,能发顶刊了

OpenAI 7个月前曾因虚假数学突破被同行嘲笑。 智东西5月21日报道,今日,OpenAI宣布,其一款未对外发布的内部通用推理模型,独立完成了一份原创数学证明。该证明推翻了匈牙利数学家保罗埃尔德什(Paul Erdős)…...

显卡驱动清理终极指南:Display Driver Uninstaller专业使用教程

显卡驱动清理终极指南:Display Driver Uninstaller专业使用教程 【免费下载链接】display-drivers-uninstaller Display Driver Uninstaller (DDU) a driver removal utility / cleaner utility 项目地址: https://gitcode.com/gh_mirrors/di/display-drivers-uni…...