图片

让Java开发者像写Spring Boot一样开发AI应用——第二阶段第三篇

写在前面

前两篇,我们让Agent学会了“动手”——从单个工具到多工具协同,Agent已经能调用外部API、查询天气、推荐景点。

但还有一个致命问题:Agent记不住。

你刚告诉它“我叫张三”,下一轮对话它就问“你叫什么名字?”;你刚说完“我在杭州”,它转头就问“你在哪个城市?”

这就像一个患有严重失忆症的实习生——能力很强,但每次对话都要从头认识你。

今天,我们给Agent装上“大脑皮层”——短时记忆系统。

📌 本文是第二阶段“核心能力实战篇”的第三篇。前两篇我们解决了“动手”问题,今天解决“记忆”问题——让Agent从“一次性对话”走向“持续对话”。

一、为什么Agent需要“记忆”?

大模型的“无状态”本质

在LLM的底层原理中,模型本身是无状态(Stateless)的。每一次API调用对模型来说都是全新的开始,它并不知道你3秒前问了什么。

课堂演示:无状态对话的问题

<span leaf="">第一次调用:</span>

所谓的“多轮对话”,本质上是在每次请求时,将历史消息列表连同当前用户输入一起打包发送给模型:

<span leaf=""><span>[系统提示词]</span>&nbsp;+&nbsp;<span>[历史消息1]</span>&nbsp;+&nbsp;<span>[历史消息2]</span>&nbsp;+ ... +&nbsp;<span>[用户最新提问]</span>&nbsp;→ LLM → 回复</span>

💡 大模型就像一个“健忘的天才”——你问什么他都能答,但你刚说完他就忘了。所谓的“多轮对话”,全靠应用程序在背后帮他把历史消息重新塞回去。

二、Agent记忆体系全景图

在Spring AI Alibaba中,记忆体系分为三个层次:

| 记忆类型

|

定义

|

存储介质

|

生命周期

|

典型实现

短时记忆

当前会话窗口内的上下文

|

内存 / Redis

|

会话期间,随会话结束而消失

|

MemorySaver / RedisSaver

| | 长时记忆 |

跨多个会话的持久化知识

|

向量数据库

|

永久(除非主动删除)

|

Vector Store + RAG

| | 用户画像记忆 |

用户偏好、身份、习惯

|

结构化存储

|

永久(除非用户修改)

|

自定义存储

|

💡 今天聚焦短时记忆——它是实现多轮对话连贯性的基础。长时记忆和用户画像将在后续课程中专门讲解。

三者关系示意图:

<span leaf=""><span>┌─────────────────────────────────────────────────────────────┐</span></span>

三、ChatMemory核心概念

ChatMemory vs ChatHistory

在使用之前,先区分两个容易混淆的概念:

| 概念

|

定义

|

用途

ChatMemory(聊天记忆)

大模型保留并用于保持上下文感知的信息

|

用于当前对话的推理

| | ChatHistory(聊天历史) |

整个对话历史的完整记录

|

用于审计、分析、归档

|

💡 ChatMemory是“模型正在用的上下文”,ChatHistory是“完整的聊天记录归档” 。两者是不同的概念——前者用于推理,后者用于审计或分析。

滑动窗口策略

短时记忆面临的核心矛盾:

  • 对话越长,上下文越大

  • 上下文越大,越可能撑爆模型的上下文窗口

  • Token越多,调用成本越高

解决方案:滑动窗口(Message Window)

MessageWindowChatMemory维护一个最大指定大小的消息窗口。当消息数量超过最大值时,会删除较旧的消息,同时保留系统消息。

<span leaf=""><span>// 配置滑动窗口:保留最近10条消息</span></span>

窗口大小的权衡:

| 窗口大小

|

优点

|

缺点

大(如50条)

上下文完整,信息丢失少

|

Token消耗大,成本高,可能超限

| | 小(如5条) |

Token消耗小,成本低

|

可能丢失关键上下文信息

| | 适中(如20条) |

平衡成本和上下文完整性

|

框架默认值

|

四、短时记忆的两种实现方式

在Spring AI Alibaba中,短时记忆有两种核心实现:

| 方式

|

核心组件

|

特点

|

适用场景

MemorySaver

内存存储

|

简单快速,服务重启即丢失

|

开发测试

| | RedisSaver |

Redis持久化

|

持久化,多实例共享

|

生产环境

|

方式一:MemorySaver——内存存储

MemorySaver是会话级缓存,它的粒度是threadId,不是userId。

<span leaf=""><span>// 最简单的配置:使用内存存储</span></span>

MemorySaver的特点:

| 特点

|

说明

✅ 简单

|

无需额外配置,开箱即用

| |

✅ 快速

|

内存读写,速度最快

| |

❌ 易失

|

服务一重启,记忆就没了

| |

❌ 单机

|

不适合多实例部署

|

💡 MemorySaver就像白板上的笔记——写上去快、看得清楚,但一擦就没了。

方式二:RedisSaver——Redis持久化

RedisSaver将Agent状态持久化到Redis中:

<span leaf=""><span>// 配置RedisSaver</span></span>

RedisSaver的优势:

| 优势

|

说明

✅ 持久化

|

服务重启,对话历史还在

| |

✅ 多实例共享

|

多个服务实例可以共享同一个Redis

| |

✅ 高性能

|

Redis读取速度很快

|

实现原理:

<span leaf="">┌──────────────────────────────────────────────────────────────┐</span>

五、threadId与会话管理

什么是threadId?

threadId是会话的唯一标识符,用于实现对话状态的隔离和持久化。

threadId的两大作用:

  1. 作为多轮对话上下文管理器的键(key)

  2. 实现不同会话的状态隔离

💡 threadId就像聊天窗口的ID——每个聊天窗口都有自己的threadId,不同窗口的对话互不干扰。

关键理解:threadId的粒度是会话,不是用户。同一个用户可以拥有多个threadId(多个会话),不同用户也可以拥有各自的threadId。

如何使用threadId?

<span leaf=""><span>// 为不同用户/会话指定不同的threadId</span></span>

使用threadId的关键要点:

  • threadId用于关联同一对话的上下文

  • 无需每次执行都生成新的threadId(否则无法保留多轮对话状态)

多会话隔离管理

核心原则:不同threadId的对话互不干扰。

<span leaf=""><span>// 用户A的会话</span></span>

六、实战:构建带记忆的Agent

实战一:用MemorySaver实现基础对话记忆

<span leaf=""><span>@Configuration</span></span>

测试验证:

<span leaf=""><span># 第一次对话</span></span>

实战二:用RedisSaver实现会话持久化

Step 1:添加依赖

<span leaf=""><span>&lt;!-- Redis客户端 --&gt;</span></span>

Step 2:配置RedisSaver

<span leaf=""><span>@Configuration</span></span>

测试持久化:

<span leaf=""><span># 第一轮对话</span></span>

💡 课堂演示:在课堂上演示“重启服务后记忆仍然存在”——让学员亲眼见证RedisSaver的持久化能力。

七、短时记忆的最佳实践

开发 vs 生产环境选型

| 环境

|

推荐存储

|

理由

开发/测试

MemorySaver

|

简单快速,无需额外依赖

| | 生产环境(单机) |

RedisSaver

|

持久化,服务重启不丢数据

| | 生产环境(集群) |

RedisSaver

|

多实例共享,会话状态一致

|

⚠️ 生产环境绝对不要用MemorySaver——服务重启、滚动更新都会导致用户对话历史丢失,用户体验极差。

滑动窗口大小的选择

| 场景

|

推荐窗口大小

|

理由

客服机器人

|

10-15条

|

对话通常较短,过多历史增加延迟

| |

编程助手

|

20-30条

|

需要记住代码上下文

| |

长文本创作

|

5-10条

|

每次输入较长,窗口不宜过大

|

常见问题与避坑指南

| 问题

|

原因

|

解决方案

服务重启后记忆丢失

使用了MemorySaver

|

切换到RedisSaver

| | 不同用户对话互相干扰 |

使用了相同的threadId

|

每个用户使用独立的threadId

| | 上下文超限 |

对话太长,窗口太大

|

减小滑动窗口大小

| | 记忆“串台” |

threadId复用或生成策略不当

|

确保threadId唯一且稳定

|

八、课后挑战

任务:实现一个支持多用户、多会话的聊天Agent

要求:

  1. 基于RedisSaver实现对话记忆的持久化

  2. 支持多用户(不同userId)的会话隔离

  3. 支持同一用户的多个会话(通过不同的threadId)

  4. 实现以下API:

  • POST /chat:发送消息(需传入userId和sessionId)

  • GET /history:查看某个会话的历史

  • DELETE /clear:清除某个会话

验收标准:

  • Redis已正确配置并启动

  • 服务重启后,对话历史仍然存在

  • 不同用户的对话互不干扰

  • 同一用户的不同会话互不干扰

九、本日核心收获

  1. 大模型是无状态的——每次API调用都是全新的开始,需要应用程序管理对话历史

  2. 记忆体系分三层——短时记忆(今天)、长时记忆(后续)、用户画像(后续)

  3. 两种实现方式——MemorySaver(内存,适合开发)和RedisSaver(Redis,适合生产)

  4. threadId是会话的身份证——不同threadId的对话互不干扰,实现多会话隔离

  5. 滑动窗口控制成本——默认保留最近20条消息,可根据场景调整

  6. 生产环境用RedisSaver——服务重启不丢数据,多实例共享状态

📌 本文是第二阶段“核心能力实战篇”的第三篇。前两篇我们让Agent学会了“动手”,今天让Agent学会了“记忆”——从“一次性对话”走向“持续对话”。下一篇,我们将进入长时记忆与RAG(检索增强生成),让Agent能够访问私有知识库,回答专属问题。

有任何问题,欢迎在评论区留言交流!


作者:Java老兵搞AI,专注Java生态下的AI应用开发

如果觉得有用,点个「在看」支持一下吧,下期见!