AI "失忆"怎么办?本文带你用 Spring AI 一招搞定多轮对话,让你的 AI 应用拥有超强记忆!从 ChatClient、Advisors 到实战编码,三步打造一个能记住上下文的智能历史专家。

大家好,我是程序员NEO。

你是否遇到过这样的 AI?上一秒刚告诉它你的名字,下一秒就问你是谁。这种“金鱼记忆”的 AI 简直让人抓狂!在智能客服、虚拟助手等场景,如果 AI 无法记住上下文,用户体验将大打折扣。

别担心,今天 NEO 就带你用 Spring AI 框架,彻底解决这个难题,轻松为你的 AI 应用植入“记忆芯片”!

为了方便演示,我们将一起创建一个“历史知识专家”AI。它不仅能对答如流,还能记住我们之前的对话,实现真正流畅的智能交流。

准备好了吗?让我们开始吧!

更强大的 ChatClient

要让 AI 拥有“记忆力”,首先得掌握与它高效沟通的工具。Spring AI 提供了 ChatClient API,这是我们与大模型交互的瑞士军刀。

很多同学可能习惯了直接注入 ChatModel,但 ChatClient 提供了功能更丰富、更灵活的链式调用(Fluent API),是官方更推荐的方式。

看看对比,高下立判:

// 基础用法(ChatModel)
ChatResponse response = chatModel.call(new Prompt("你好")); // 高级用法(ChatClient)
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是历史顾问")
.build(); String response = chatClient.prompt().user("你好").call().content();

ChatClient 的构建方式也很灵活,可以通过构造器注入或使用建造者模式:

// 方式1:使用构造器注入
@Service
public class ChatService {
private final ChatClient chatClient; public ChatService(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("你是历史顾问")
.build();
}
} // 方式2:使用建造者模式
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("你是历史顾问")
.build();

它还支持多种响应格式,无论是包含 Token 信息的完整响应、自动映射的 Java 对象,还是实现打字机效果的流式输出,都能轻松搞定。

// ChatClient支持多种响应格式
// 1. 返回 ChatResponse 对象(包含元数据如 token 使用量)
ChatResponse chatResponse = chatClient.prompt()
.user("Tell me a joke")
.call()
.chatResponse(); // 2. 返回实体对象(自动将 AI 输出映射为 Java 对象)
// 2.1 返回单个实体
record ActorFilms(String actor, List<String> movies) {}
ActorFilms actorFilms = chatClient.prompt()
.user("Generate the filmography for a random actor.")
.call()
.entity(ActorFilms.class); // 2.2 返回泛型集合
List<ActorFilms> multipleActors = chatClient.prompt()
.user("Generate filmography for Tom Hanks and Bill Murray.")
.call()
.entity(new ParameterizedTypeReference<List<ActorFilms>>() {}); // 3. 流式返回(适用于打字机效果)
Flux<String> streamResponse = chatClient.prompt()
.user("Tell me a story")
.stream()
.content(); // 也可以流式返回ChatResponse
Flux<ChatResponse> streamWithMetadata = chatClient.prompt()
.user("Tell me a story")
.stream()
.chatResponse();

更棒的是,你可以为 ChatClient 设置默认的“人设”(系统提示词),甚至在对话中动态替换模板变量,让 AI 的角色扮演更加生动。

// 定义默认系统提示词
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultSystem("You are a friendly chat bot that answers question in the voice of a {voice}")
.build(); // 对话时动态更改系统提示词的变量
chatClient.prompt()
.system(sp -> sp.param("voice", voice))
.user(message)
.call()
.content());

Advisors 拦截器

如果说 ChatClient 是 AI 的躯体,那 Advisors(顾问)就是给它加持的各种“外挂”和“Buff”。

你可以把 Advisors 理解为一系列可插拔的拦截器。在请求发给 AI 前或收到 AI 响应后,它们可以执行各种骚操作:

  • 前置增强:悄悄改写你的提问,让它更符合 AI 的胃口;或者进行安全检查,过滤掉危险问题。
  • 后置增强:记录调用日志,或者对 AI 的回答进行二次加工。

用法非常简单,直接在构建 ChatClient 时配置 defaultAdvisors 即可。比如,MessageChatMemoryAdvisor 就是我们实现对话记忆的关键“外挂”。

var chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory), // 对话记忆 advisor
new QuestionAnswerAdvisor(vectorStore) // RAG 检索增强 advisor
)
.build(); String response = this.chatClient.prompt()
// 对话时动态设定拦截器参数,比如指定对话记忆的 id 和长度
.advisors(advisor -> advisor.param("chat_memory_conversation_id", "678")
.param("chat_memory_response_size", 100))
.user(userText)
.call()
.content();

Advisors 的工作原理就像一条精密的流水线(责任链模式):

流水线流程解读:

  1. 用户的请求进来,被包装成一个 AdvisedRequest
  2. 请求在 Advisor 链上依次传递,每个 Advisor 都可以对它进行处理或修改。
  3. 最终,请求被发送给 ChatModel
  4. 模型的响应再沿着流水线反向传回,每个 Advisor 也可以处理响应。
  5. 最后,客户端收到经过层层“加持”的最终结果。

注意Advisor 的执行顺序由其 getOrder() 方法决定,值越小,优先级越高,跟代码书写顺序无关哦!

Chat Memory Advisor

要实现对话记忆,ChatMemoryAdvisor 是我们的不二之选。它有几种实现方式,最常用的是 MessageChatMemoryAdvisor

  • MessageChatMemoryAdvisor:将历史对话作为完整的消息列表(包含用户和 AI 的角色)添加到提示中。这是最符合现代大模型交互方式的选择。
  • PromptChatMemoryAdvisor:将历史对话拼接成一段文本,塞进系统提示词里。
  • VectorStoreChatMemoryAdvisor:使用向量数据库来存储和检索历史对话,适用于更复杂的场景。

MessageChatMemoryAdvisor 保留了对话的原始结构,能让 AI 更好地理解上下文,因此 强烈推荐使用

Chat Memory

ChatMemoryAdvisor 只是“搬运工”,真正存储对话历史的是 Chat Memory。Spring AI 提供了多种“记忆仓库”:

  • InMemoryChatMemory:内存存储,简单快捷,适合测试(我们今天就用它)。
  • JdbcChatMemory, CassandraChatMemory, Neo4jChatMemory:持久化存储,可将对话历史保存在数据库中,适合生产环境。

打造一个“历史学家”AI

理论讲完了,上代码!

初始化 ChatClient

我们通过构造器注入 ChatModel,然后构建 ChatClient。在构建时,设定好“历史学家”的人设(SYSTEM_PROMPT),并装上我们的记忆“外挂”——MessageChatMemoryAdvisor

/**
* @author 程序员NEO
* @version 1.0
* @description 历史知识专家应用
* @since 2025-07-07
**/
@Component
@Slf4j
public class HistoryExpertApp { private final ChatClient chatClient; private static final String SYSTEM_PROMPT = "你是一位风趣幽默的历史知识专家,学识渊博。" +
"你需要根据用户的提问,生动、清晰地回答相关的历史知识。" +
"如果用户的问题不清晰,你需要引导用户提供更多信息。"; public HistoryExpertApp(ChatModel chatModel) {
// 初始化基于内存的对话记忆
ChatMemory chatMemory = new InMemoryChatMemory();
chatClient = ChatClient.builder(chatModel)
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
// ... doChat 方法
}

这里我们使用了 InMemoryChatMemory,它将对话历史存在内存里。对于生产环境,记得换成 Redis 或数据库等持久化方案。

编写对话方法

核心的 doChat 方法接收用户消息(message)和会话 ID(chatId)。chatId 是区分不同对话的关键,确保每个用户的聊天记录相互独立。

/**
* 执行聊天操作,处理用户消息并返回 AI 的响应。
*
* @param message 用户发送的消息
* @param chatId 对话 ID,用于标识当前会话
* @return AI 的响应内容
*/
public String doChat(String message, String chatId) {
ChatResponse chatResponse = chatClient
.prompt()
.user(message)
.advisors(spec -> spec
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) // 设置对话 ID
.param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10)) // 设置记忆容量
.call()
.chatResponse(); String content = chatResponse.getResult().getOutput().getContent();
log.info("AI Response: {}", content);
return content;
}

.advisors() 方法中,我们传入了两个关键参数:

  • CHAT_MEMORY_CONVERSATION_ID_KEY: 会话 ID,确保每个用户的对话历史是隔离的。
  • CHAT_MEMORY_RETRIEVE_SIZE_KEY: 对话记忆检索大小。设置为 10 表示 AI 在回答时,会参考最近的 10 条消息(5 轮对话)。

见证奇迹的时刻!

我们用一个单元测试来验证 AI 是否真的拥有了记忆。

@SpringBootTest
public class HistoryExpertAppTest { @Resource
private HistoryExpertApp historyExpertApp; @Test
void testChat() {
String chatId = UUID.randomUUID().toString(); // 第一轮对话
System.out.println("--- 第一轮对话 ---");
String message1 = "我叫NEO,我最喜欢的数字是7。";
System.out.println("我: " + message1);
String answer1 = historyExpertApp.doChat(message1, chatId);
Assertions.assertNotNull(answer1);
System.out.println("AI: " + answer1); // 第二轮对话
System.out.println("\n--- 第二轮对话 ---");
String message2 = "我叫什么名字?我最喜欢的数字是几?";
System.out.println("我: " + message2);
String answer2 = historyExpertApp.doChat(message2, chatId);
Assertions.assertNotNull(answer2);
System.out.println("AI: " + answer2);
}
}

场景一:拥有完整记忆

CHAT_MEMORY_RETRIEVE_SIZE_KEY 设置为 10 时,AI 能轻松记住我们在第一轮对话中提供的信息。

测试结果

--- 第一轮对话 ---
我: 我叫NEO,我最喜欢的数字是7。
AI: 哈哈,Neo!很高兴认识你!7确实是一个神奇的数字——不仅是上帝创造世界的天数,也是彩虹的颜色数、一周的天数,甚至还是詹姆斯·邦德的代号!看来你和神秘事物很投缘啊! 既然你喜欢7,那我考考你:你知道人类历史上有哪些著名的"七"吗?比如七大奇迹、七星瓢虫,或者...《七龙珠》? 说说看,你是更喜欢历史中的神秘"七",还是生活里有趣的"七"呢?我可以从任何方向展开聊聊!
--- 第二轮对话 ---
我: 我叫什么名字?我最喜欢的数字是几?
AI: 哎呀!这像是在考考我这个"博学多才"的历史专家了是不是? 让我想想...上一秒钟我还记得清清楚楚来着...哦对了!你叫NEO,跟我分享过你最喜欢数字7的奥秘。就像《黑客帝国》里的主角一样充满传奇色彩的名字,配上神秘的7,简直完美搭配! 不过说真的,能告诉我你是从哪部《黑客帝国》开始认识NEO这个角色的呢?是第一部经典之作,还是后来的续集?还有,为什么偏偏是7这个数字吸引了你?我觉得这里面一定有段有趣的故事要听!

成功了!AI 准确地记住了我们的信息。

场景二:秒变“金鱼记忆”

现在,我们将记忆容量改为 1,这意味着 AI 只能看到当前用户的提问,完全不记得之前的对话。

.param(MessageChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 1)

再次运行测试,看看会发生什么。

测试结果(失忆版)

--- 第一轮对话 ---
我: 我叫NEO,我最喜欢的数字是7。
AI: 哎呀,欢迎Neo!让我猜猜...你喜欢数字7是不是因为"Matrix"里的第七号巧合?不过既然你提到了数字7,让我告诉你一个有趣的历史故事吧! 你知道吗?在古希腊人眼中,7可是个神奇的数字组合 - 就像我这个历史专家平时吃着橄榄,看着帕特农神庙时想的那样。他们发现了七个智慧贤人,观察到天空中有七颗可见的行星(当然那是没有望远镜的时代),甚至连他们的神话里都有七重天的说法。 嘿,作为一个喜欢解密历史的人,我发现你这个名字和数字7的组合让我想起了一些有意思的东西。比如,柏拉图在他的著作里经常提到"理想国",而那个国家正好有七个等级制度。或者亚历山大图书馆,它可是古代世界七大奇迹之一! 说起来,你是更喜欢探索数字7在数学上的独特性质,还是它在不同文化中的神秘象征?让我们一起揭开这个数字背后的更多秘密吧!
--- 第二轮对话 ---
我: 我叫什么名字?我最喜欢的数字是几?
AI: 哎呀,这可真是个让人摸不着头脑的问题!让我这个历史专家翻遍了古希腊的羊皮卷、巴比伦的泥板和中国的甲骨文都没找到答案。难道你是想让我展示特洛伊木马式的惊喜?还是像斯芬克斯谜语那样考考我? 不过作为一个精通历史的学者,我可以告诉你一些有趣的背景:在古代埃及,人们会给新生儿起非常特别的名字,有时候是根据他们出生的日子来取的。而说到数字,毕达哥拉斯可是坚信万物皆数呢!

看到了吗?仅仅是一个参数的差别,AI 就从“智能”变成了“智障”。这个对比鲜明地展示了对话记忆的重要性。

希望这篇文章对你有帮助!如果你觉得内容不错,点个赞分享给更多朋友吧!有任何问题,欢迎在评论区留言交流。

关注公众号【程序员NEO】,第一时间获取更多 AIGC 实战干货!

Spring AI 玩转多轮对话的更多相关文章

  1. 游戏AI玩伴,是“神队友”还是“猪队友”?

    “一代英豪”暴雪迎来了自己的暴风雪. 2月13日,动视暴雪公布了2018年全年财报.财报显示,暴雪第四季度营业收入仅为28.4亿美元,低于华尔街分析师预期的30.4亿美元.在公布了财报业绩后,该公司又 ...

  2. AI 玩法整理

    随着信息技术的火热发展,人工智能已经成为IT全行业的风口爆发点,既然风口来了,作为技术人人员也都毫不犹豫的分一杯羹,怎么玩呢? 接下来的博客就会带领大家一起玩玩AI 认识AI--略,如果有需要的可以再 ...

  3. 增强学习训练AI玩游戏

    1.游戏简介 符号A为 AI Agent. 符号@为金币,AI Agent需要尽可能的接取. 符号* 为炸弹,AI Agent需要尽可能的躲避. 游戏下方一组数字含义如下: Bomb hit: 代表目 ...

  4. AIUI开放平台:多轮对话返回前几轮语槽数据

    编写云函数: AIUI.create("v2", function(aiui, err){ // 获取 response response = aiui.getResponse() ...

  5. Web测试要点 做移动端的测试,也做web端的测试,甚至后面桌面端的测试和后台的测试也做了,基本上把我们产品各个端都玩了一轮

    Web测试要点 一.功能测试 1.链接测试 (1).测试所有链接是否按指示的那样确实链接到了该链接的页面:  (2).测试所链接的页面是否存在:  (3).保证Web应用系统上没有孤立的页面(所谓孤立 ...

  6. 在 Spring 生态中玩转 RocketMQ

    本文作者:饶子昊 - Spring Cloud Alibaba Committer,阿里云智能开发工程师. 01 Spring 生态介绍 根据 JVM EcoSystem Report 2021 最新 ...

  7. AI中台——智能聊天机器人平台的架构与应用(分享实录)

    内容来源:宜信技术学院第3期技术沙龙-线上直播|AI中台——智能聊天机器人平台 主讲人:宜信科技中心AI中台团队负责人王东 导读:随着“中台”战略的提出,目前宜信中台建设在思想理念及架构设计上都已经取 ...

  8. 人工智能头条(公开课笔记)+AI科技大本营——一拨微信公众号文章

    不错的 Tutorial: 从零到一学习计算机视觉:朋友圈爆款背后的计算机视觉技术与应用 | 公开课笔记 分享人 | 叶聪(腾讯云 AI 和大数据中心高级研发工程师) 整    理 | Leo 出   ...

  9. GAME AI Pro 1 第1章

    和钱康来合作翻译的AI PRO 1和2 系列,计划是一周一篇,先捡着有意思的翻,对那篇有兴趣也可以留言给我优先翻译,希望都翻译好后有机会成书吧,有兴趣一起翻译的也可以联系我. 游戏人工智能是什么( W ...

  10. 24分钟让AI跑起飞车类游戏

    本文由云+社区发表 作者:WeTest小编 WeTest 导读 本文主要介绍如何让AI在24分钟内学会玩飞车类游戏.我们使用Distributed PPO训练AI,在短时间内可以取得不错的训练效果. ...

随机推荐

  1. X86-64位简易系统开发 - 从BIOS阶段开始

    最近回顾之前写的代码的时候, 发现了以前本科时还开发过一个64位的操作系统, 不过最终也只是开发到进程切换部分 这是一个涉及到汇编和C语言的一个偏底层偏硬核的项目, 而且为了能够学到更多东西, 使用的 ...

  2. PMP学习记录

    本人在2020年12月已经顺利拿到PMP证书. 第一次听说PMP证书是2016年,一个同事说考试通过拿到了PMP证书,当时对PMP不是很了解.也未作深入了解,当时认为俺是做技术的,这个证书没啥用.O( ...

  3. Lua中获取第二天凌晨的剩余时间

    在时间这个问题上,lua提供两大方法来供开发者使用,一个是os.time(),一个是os.date(),这两大方法可以满足日常开发的需求. 那么我们如何准确运用这两大方法呢. 在这一文章中我们先讲os ...

  4. MCP开发应用,使用python部署sse模式

    一.概述 MCP服务端当前支持两种与客户端的数据通信方式:标准输入输出(stdio)  和 基于Http的服务器推送事件(http sse) 1.1 标准输入输出(stdio) 原理:  标准输入输出 ...

  5. mybatis——分页插件PageHelper的使用

    项目开发中涉及列表查询时,经常会需要对查询结果进行分页处理:常用的一个插件--PageHelper,是国内非常优秀的一款开源的mybatis分页插件,它支持基本主流与常用的数据库,一致支持mysql. ...

  6. Robot Framework自定义关键字

    需求分析: 如下图,诸多步骤中可能共用某些共同的步骤,比如都需要登录会员 此,可以把登录的操作写成模块化,插入其他脚本供其他脚本调用,如此可以节省不少脚本量 上图为会员登录的操作. 具体实施如下: 1 ...

  7. pytorch 实战教程之 SPP(SPPNet---Spatial Pyramid Pooling)空间金字塔池化网络代码实现 和 SPPF (Spatial Pyramid Pooling Fast)详解​​

    原文作者:aircraft 原文链接:pytorch 实战教程之 SPP(SPPNet---Spatial Pyramid Pooling)空间金字塔池化网络代码实现 和 SPPF (Spatial ...

  8. 2025dsfz-KMP学习笔记

    KMP 前言:这把高端局 关于KMP 时间复杂度为 \(O(n+m)\) 的优秀字符串查找算法. 适用于在句子/文章中查找一段文字(词语). KMP实现 关于共同前后缀数组(PMT) 说人话就是 \( ...

  9. Java编程--简单的Factory程序(工厂设计模式)

    Factory类不是接口.抽象类,就是普通的类. Factory就像一个工厂一样,可以返回很多对象. 子类在继承.实现抽象类和接口后由Factory类处理,由于子类可能会有多个,Factory根据客户 ...

  10. 【记录】MATLAB矩阵的批量元素修改方式,与Python的NumPy对比

    文章目录 二维矩阵 操作 1. 将数组大于0的数全部加1 2. 删除元素 ①删除单个元素 ②删除一列元素 3. 添加一行或多行 ①添加一行 ②添加多行 4. 获取行/列数 5. 格式化输出数组 结构数 ...