1. 为什么内存版会话记忆一定会翻车Agent 会话记忆持久化说白了就是让 Agent 记住「你们聊过什么、干到哪一步了」并且把这些上下文写进数据库做到会话中断后能恢复、换台设备还能接着聊。它适合正在做 Java Agent 的开发者、需要多端同步的智能助手团队以及被「重启就失忆」折磨过的同学。我见过太多 Demo 阶段的 Agent上下文全塞在一个ConcurrentHashMapString, ListMessage里本地跑得飞起一上线就出问题。最典型的三类翻车现场第一类是进程重启即失忆。用户让 Agent 写一个文件操作工具类写到一半服务发了个版重启后 Agent 一脸茫然用户只能把需求重新描述一遍。第二类是跨设备断片。用户在电脑端发起了一个复杂编码任务出门想用手机继续追问进度结果手机端是全新的空会话历史记录一条都读不到。第三类更隐蔽——多实例部署时负载均衡把同一个会话的两次请求打到了不同节点A 节点有记忆、B 节点没有Agent 的回答前后矛盾用户以为它「精神分裂」了。这三个问题的根因是同一个会话记忆的生命周期被绑定在了进程内存上而不是绑定在会话 ID 上。工业级 Agent 的做法是把记忆从内存里「搬出来」落到 Redis MySQL 这类外部存储用 SessionManager 统一调度。这样进程只是无状态的计算单元谁挂了都不影响记忆换设备只要带上同一个 sessionId 就能续上。这节课我就按「表结构 → SessionManager 骨架 → 接入通道 → 验证 → 排障」的顺序把一套可复制的持久化方案拆开讲。核心结论先放这儿分层存储、会话隔离、高效读写——短期记忆进 Redis 扛高频读写长期记忆进 MySQL 保不丢用 sessionId 串起「用户-会话-记忆」三层关系。2. 前置准备TaoToken 统一 Key 与依赖清单在写 SessionManager 之前先把模型调用通道和工程依赖准备好。Agent 的每一轮推理都要调模型如果 Key 管理散落在各个类里后面做记忆回放和跨设备同步时会非常难排查。我建议统一走 TaoToken 的 API 通道一个 Key 管所有模型调用接入地址是https://taotoken.net/api控制台里可以创建和管理 API Keys。具体动作分三步。第一步到官网https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册后进入控制台在 API Keys 页面生成一个 Key建议按环境区分dev / prod 各一个方便出问题时快速定位是哪套环境在刷量。第二步把 Key 写进配置中心或环境变量不要硬编码进代码后面 SessionManager 读取配置时统一从agent.llm.api-key取。第三步确认你的模型对话通道可用可以先用模型对话页面手动发一条消息验证 Key 有效再去写代码。工程依赖这块除了 Spring Boot 基础包需要额外引入 Redis、MySQL 驱动和 Spring JDBCdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.43/version /dependency配置文件里把三样东西配好MySQL 连接、Redis 连接、TaoToken 的 API Key 与 base URL。这里有个细节spring.redis在新版 Spring Boot 里已经改名为spring.data.redis如果你用的是 2.7 以上版本配置项要跟着改否则启动时 Redis 相关 Bean 注入会失败这个坑我在排障章节还会再提。spring.datasource.urljdbc:mysql://localhost:3306/agent_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.passwordyour_password spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver spring.data.redis.hostlocalhost spring.data.redis.port6379 spring.data.redis.database0 agent.llm.base-urlhttps://taotoken.net/api agent.llm.api-key${TAOTOKEN_API_KEY}注意API Key 通过环境变量注入不要提交到 Git 仓库。团队协作时用配置中心下发避免 Key 泄露后被滥用。3. 可复制配置表结构与 SessionManager 骨架3.1 记忆表结构设计先把 MySQL 表建好。这张表要同时承载长期记忆和会话元信息字段设计上我加了memory_type区分短期/长期加了expire_time支持过期清理索引建在session_id和user_id上因为这两个字段是查询的主入口。CREATE TABLE agent_memory ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键, session_id varchar(128) NOT NULL COMMENT 会话ID, user_id varchar(64) NOT NULL COMMENT 用户ID, memory_type tinyint(4) NOT NULL COMMENT 1短期 2长期, role varchar(16) NOT NULL COMMENT user/assistant/tool, content mediumtext NOT NULL COMMENT 记忆内容JSON, token_count int(11) DEFAULT 0 COMMENT token估算, create_time datetime NOT NULL, update_time datetime NOT NULL, expire_time datetime DEFAULT NULL COMMENT 过期时间, PRIMARY KEY (id), KEY idx_session (session_id, create_time), KEY idx_user (user_id, memory_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTAgent会话记忆表;这里role字段很关键。Agent 的上下文不只是用户说的话还包括 assistant 的回复和 tool 的调用结果三者要按时间顺序拼回一个完整的消息列表才能喂给模型。如果只存 content 不存 role恢复出来的上下文顺序和角色就乱了模型会答非所问。3.2 SessionManager 配置骨架SessionManager 是整个持久化系统的调度中心职责有四块创建会话、写入记忆、恢复会话、跨设备查询。下面是一个可运行的骨架我把它拆成接口和实现两部分方便你替换存储方案。Component public class SessionManager { Resource private StringRedisTemplate redisTemplate; Resource private JdbcTemplate jdbcTemplate; private static final String SHORT_KEY_PREFIX agent:mem:short:; private static final Duration SHORT_TTL Duration.ofHours(2); /** 创建会话返回全局唯一 sessionId */ public String createSession(String userId) { String sessionId s- userId - UUID.randomUUID().toString().replace(-, ); // 初始化一条空的短期记忆占位标记会话已激活 redisTemplate.opsForValue().set(SHORT_KEY_PREFIX sessionId, [], SHORT_TTL); return sessionId; } /** 写入记忆短期进Redis长期进MySQL */ public void appendMemory(String sessionId, String userId, String role, String content, boolean longTerm) { MemoryRecord record new MemoryRecord(sessionId, userId, role, content); if (longTerm) { jdbcTemplate.update( INSERT INTO agent_memory(session_id,user_id,memory_type,role,content, create_time,update_time) VALUES(?,?,?,?,?,?,?), sessionId, userId, 2, role, content, LocalDateTime.now(), LocalDateTime.now()); } else { String key SHORT_KEY_PREFIX sessionId; String existing redisTemplate.opsForValue().get(key); ListMemoryRecord list JSON.parseArray(existing, MemoryRecord.class); list.add(record); redisTemplate.opsForValue().set(key, JSON.toJSONString(list), SHORT_TTL); } } /** 恢复会话短期 长期合并按时间排序 */ public ListMemoryRecord restoreSession(String sessionId) { ListMemoryRecord result new ArrayList(); String shortJson redisTemplate.opsForValue().get(SHORT_KEY_PREFIX sessionId); if (shortJson ! null) { result.addAll(JSON.parseArray(shortJson, MemoryRecord.class)); } ListMemoryRecord longList jdbcTemplate.query( SELECT session_id AS sessionId, user_id AS userId, role, content, create_time AS createTime FROM agent_memory WHERE session_id? AND memory_type2 ORDER BY create_time ASC, new BeanPropertyRowMapper(MemoryRecord.class), sessionId); result.addAll(longList); result.sort(Comparator.comparing(MemoryRecord::getCreateTime)); return result; } /** 跨设备同步查用户所有会话 */ public ListString listUserSessions(String userId) { return jdbcTemplate.queryForList( SELECT DISTINCT session_id FROM agent_memory WHERE user_id? AND memory_type2, String.class, userId); } }MemoryRecord就是一个普通 POJO字段和表结构对应createTime用LocalDateTime。注意短期记忆我用了「读-改-写」的方式追加这在单会话低并发下没问题但如果同一个会话有并发写入需要换成 Redis 的 List 结构用RPUSH原子追加避免覆盖丢失。这个点后面排障会展开。3.3 接入 TaoToken 通道记忆恢复出来后要把上下文拼成消息列表发给模型。这里统一走 TaoToken 的 API 通道base URL 用https://taotoken.net/apiKey 从配置读。下面是一个最小调用示例把恢复的记忆转成 messages 数组public String chatWithMemory(String sessionId, String userId, String userInput) { // 1. 先落库用户输入 appendMemory(sessionId, userId, user, userInput, true); // 2. 恢复完整上下文 ListMemoryRecord history restoreSession(sessionId); ListMapString, String messages history.stream() .map(r - Map.of(role, r.getRole(), content, r.getContent())) .collect(Collectors.toList()); // 3. 调用模型 MapString, Object body Map.of( model, claude-sonnet-4-5, messages, messages, max_tokens, 2048 ); String resp webClient.post() .uri(baseUrl /v1/messages) .header(x-api-key, apiKey) .header(anthropic-version, 2023-06-01) .bodyValue(body) .retrieve() .bodyToMono(String.class) .block(); // 4. 落库模型回复 String reply extractText(resp); appendMemory(sessionId, userId, assistant, reply, true); return reply; }这样一轮对话结束后用户输入和模型回复都进了 MySQL短期记忆在 Redis 里也有副本。下次无论从哪台设备带同一个 sessionId 进来restoreSession都能把完整上下文拼回来。4. 验证请求中断恢复与跨设备读取配置写完必须验证不然你不知道记忆到底有没有落库。我一般分三步测。第一步验证写入。启动应用后创建一个会话发一条消息然后直接查数据库SELECT session_id, role, LEFT(content, 50) AS preview, create_time FROM agent_memory WHERE session_id s-dev001-xxxx ORDER BY create_time;正常应该看到两条记录一条 roleuser一条 roleassistant时间戳递增。如果只有一条说明模型回复没落库检查appendMemory的调用位置是不是在异常分支里被跳过了。第二步验证中断恢复。手动重启应用模拟服务宕机然后用同一个 sessionId 调restoreSession打印返回的消息条数。预期条数和重启前一致且顺序正确。这一步能过说明记忆真的持久化了不是靠内存撑着。第三步验证跨设备同步。换一个「设备」——其实就是换一个 HTTP 客户端或另一台机器带上同一个 userId 调listUserSessions应该能列出该用户的所有 sessionId。再挑其中一个 sessionId 调restoreSession能读到完整历史就说明跨设备读取通了。# 用 curl 模拟另一台设备读取会话列表 curl -X GET http://your-host:8080/agent/sessions?userIddev-001 \ -H Authorization: Bearer your-token返回结果类似{ userId: dev-001, sessions: [s-dev001-a1b2c3, s-dev001-d4e5f6], count: 2 }拿到 sessionId 后再请求/agent/session/{sessionId}/history返回的消息列表如果和电脑端一致跨设备同步就算验证通过。实测下来这套流程在单机 Redis MySQL 环境下恢复 100 条记忆的耗时在 20ms 以内对交互体验基本无感。5. 本篇常见错排查5.1 恢复出来的上下文顺序错乱最常见的原因是短期记忆和长期记忆合并时没排序。Redis 里的短期记忆是追加的MySQL 里的长期记忆是按插入时间查的两边拼起来如果不按createTime统一排序模型收到的消息顺序就是乱的。解决办法就是在restoreSession最后加一次sort用createTime升序。另外要确保createTime在写入时就固定下来不要用查询时的当前时间。5.2 并发写入导致短期记忆丢失前面提过短期记忆用「读-改-写」在并发下会丢数据。假设同一会话两个请求同时进来都读到[msg1]各自追加后写回后写的会覆盖先写的结果只剩一条。修复方式有两种一是改用 Redis List用RPUSH原子追加读取时用LRANGE二是给会话加分布式锁串行化写入。生产环境我推荐第一种性能更好。// 改用 List 结构原子追加 redisTemplate.opsForList().rightPush(SHORT_KEY_PREFIX sessionId, JSON.toJSONString(record)); redisTemplate.expire(SHORT_KEY_PREFIX sessionId, SHORT_TTL);5.3 Redis 配置项不生效如果你用的是 Spring Boot 2.7 及以上spring.redis.*已经废弃要改成spring.data.redis.*。配置没生效时应用启动不会报错但连接的是默认 localhost:6379如果你 Redis 在别的机器上就会一直连不上。排查方法是启动时看日志里 Redis 的连接地址或者直接在代码里打印redisTemplate.getConnectionFactory()的配置。5.4 长期记忆无限增长拖慢查询agent_memory表只增不删跑几个月后单表几百万行restoreSession的查询会越来越慢。解决办法是加定期清理任务把超过 N 天的长期记忆归档或删除同时给session_id create_time建联合索引前面表结构里已经建了。清理任务可以用 Spring 的Scheduled每天凌晨跑一次Scheduled(cron 0 0 3 * * ?) public void cleanExpired() { jdbcTemplate.update( DELETE FROM agent_memory WHERE memory_type2 AND create_time ?, LocalDateTime.now().minusDays(90)); }5.5 模型调用失败导致记忆只写了一半如果用户输入已经落库但模型调用抛异常assistant 的回复就不会写入恢复出来的上下文里 user 消息后面直接跟下一条 user 消息模型会困惑。建议在chatWithMemory里用 try-catch 包住模型调用失败时写入一条 roleassistant、content 为错误提示的占位记录保证消息序列完整。同时把失败请求记到日志方便后续重放。6. 下一步把记忆接进你的 Agent 主循环到这里一套可运行的会话记忆持久化方案就搭完了表结构管长期记忆Redis 管短期记忆SessionManager 负责调度TaoToken 统一通道负责模型调用。你可以先把restoreSession接进 Agent 的主循环在每轮推理前恢复上下文、推理后追加记忆跑通「中断-恢复」这条链路。如果你打算把这套方案用到长期编码或 Agent 场景建议顺手把 Coding Plan 配上让模型调用和记忆管理解耦后续换模型或加工具都不用动存储层。接入过程中遇到 Key 或通道问题直接翻接入文档对照排查比在代码里猜要快得多。记忆持久化这层做扎实了后面加多轮工具调用、任务断点续跑都会顺很多。
企业数字化 ERP 产品动态
相关推荐
Hermes Agent深度探索:必知!读懂 AIAgent,解锁 Hermes 会话循环真正主轴价值 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 22:27:07
建个网站费用怎么算?5条注意事项避坑指南 建个网站费用怎么算?5条注意事项避坑指南 找建站公司最怕什么?不是技术不行,而是报价单拿在手里心里没底,怕被坑高价。很多老板花了几万块建了个站,结果连搜索引擎都搜不到,钱白花了一大半。… · 2026/9/27 22:27:01
2026年亲测AI写作辅助网站合集:TaoToken统一Key接入DeepSeek与Claude的合规高效版 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/27 22:27:01
3DGS 端侧重建结果发糊不是算法玄学:采集覆盖率、模糊帧与轨迹回环怎么做门禁 3DGS 端侧重建结果发糊不是算法玄学:采集覆盖率、模糊帧与轨迹回环怎么做门禁
同一台设备拍同一个物体,有时模型完整,有时背面塌掉、纹理发糊。把问题全部归到重建算法,通常会错过真正能控制的变量:输入帧是否清晰、视… · 2026/9/27 23:02:03
Java面试被问烂的JVM,这样答直接加分 别背“堆栈方法区”,画一张内存图面试官问内存模型,不是考你记忆力,是考你脑子里有没有一幅图。你可以说:“我习惯把JVM内存想象成一栋楼。程序计数器是每层楼的门牌号,记录线程执行到哪一行;虚拟机栈是每个… · 2026/9/27 23:01:57
视频通话弱网测试笔记:用网络损伤仪把上行限到800kbps 接着前面的选型记录,这篇把网准通 NetAccura ChaosBridge 网络损伤仪的使用方法写具体一点:怎么接线,怎么把视频通话的上行限到800kbps,以及画面卡住以后去哪里找原因。这一轮适合用DPDK引擎,重点是上下行分开设置、队… · 2026/9/27 23:01:57
ChromaPanel 与其他 React 颜色选择器对比:功能、包体积、可访问性等 选择一个 React 颜色选择器,听起来很简单,直到你开始认真考虑自己的应用到底需要什么。
也许你只需要一个很小的 HEX 颜色选择器。
也许你需要 RGB 和 HSL 控制、预设的调色板、一个吸管工具、从图片中取色、渐变功能、可访问性、表单支持,… · 2026/9/27 23:01:57
告别改需求拖一周,这份做网站计划是保姆级建站教程 告别改需求拖一周,这份做网站计划是保姆级建站教程 改个按钮颜色,建站公司说要排期一周?这种憋屈事,谁干谁心累。 很多设计师转前端的朋友,手里有图,心里有底,但一旦涉及【做网站计划】,就容易卡壳。 今天不整虚的,直接上一份 保姆级建站教程… · 2026/9/27 23:01:57
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01
MATLAB雷达信号脉冲压缩仿真:LFM线性调频、匹配滤波与距离分辨率实现 简介:这套Matlab仿真工具完整呈现雷达信号脉冲压缩过程,从线性调频(LFM)信号生成、目标回波仿真到匹配滤波压缩处理均有可运行代码支撑,面向电子信息工程、计算机、数学等专业学生,适用于课程设计、期末大作… · 2026/9/27 0:00:01
汕头网站建设制作厂家避坑指南:5大注意事项救急 汕头网站建设制作厂家避坑指南:5大注意事项救急 改个需求建站公司拖一周,这种憋屈事我见得太多了。 很多汕头老板找本地建站团队,签合同前看着方案挺美,一上线就变脸。 今天不聊虚的,直接拆解找 汕头网站建设制作厂家 时的5个核心 注意事项… · 2026/9/27 0:00:01
多模态虚假新闻检测实战:BERT+ResNet双塔与对比学习 简介:基于PyTorch的多模态虚假新闻检测项目完整代码包,面向自然语言处理与计算机视觉交叉方向的开发者、科研人员及毕业设计选题者,解决社交媒体中文本与图像联合识别虚假新闻的问题。系统以BERT预训练模型提取文本语义特征,以Res… · 2026/9/27 0:00:01