第9章:Spring AI 2.0对话记忆入门

发布时间:2026/10/13 15:49:04
第9章:Spring AI 2.0对话记忆入门 上章思考题答案入库用请求参数里的订单号。模型回写的另一个号码记在审计字段modelOrderId不作为工单主键。两边不一致时工单标记为待人工核对不自动退款。重试日志记录 Schema 路径例如refundable缺失不记录模型原文。原文可能含用户隐私也可能把内部字段说明再次写进下一轮提示词。1. 项目背景1.1 业务场景用户在 App 里先问「订单 LC202610060018 还没发货。」客服回答了发货状态。下一句用户只说「那单能退吗」句子里没有订单号。若每一轮都是全新的Prompt模型看不见上一句只能反问「请问订单号是多少」。邻里仓的体验要求是同一会话里第二轮仍能提到LC202610060018。本章用MessageWindowChatMemory按会话编号保存最近若干条消息用MessageChatMemoryAdvisor在调用前把历史拼进提示词调用后再写回。邻里仓要求同一会话里第二轮仍能提到LC202610060018。这个号码只出现在第一轮用户话里不写进系统提示词。否则第二轮「记住了」只是店规在复述记忆测试会假通过。会话编号用渠道加会话不用全站共享的一个 key避免张三的订单出现在李四的回答里。1.2 痛点有人把全部历史拼进一个静态字符串两个用户共用一个字段张三的订单号出现在李四的回答里。有人把历史放进单例List重启或扩成两个实例之后第二轮又失忆而且测试之间互相污染。窗口若只留一条系统提示词里的店规会被挤掉或者反过来窗口留得太长店规还在但提示词 token 被闲聊占满模型开始忽略「未发货可退」。没有会话记忆时的路径每轮只发当前句 → 第二轮丢失订单号 → 客服反复追问或全局 List → 串会话。反复追问在退款场景里尤其糟。用户已经报过单号再问一次会被当成机器人。全局 List 则更糟后一个用户的「那单」会继承前一个用户的订单号直接退错单。窗口记忆把这两件事分开编号负责隔离条数上限负责不让闲聊占满上下文。重启后记忆消失是本章接受的边界。进程内窗口只保证同一次运行里的多轮对话不要接数据库来掩盖重启丢数据。conversationIdMessageWindowChatMemoryMessageChatMemoryAdvisor本次 Prompt 含历史DeepSeek2. 项目设计小胖说「这不就是食堂记熟客吗上次要少盐这次只说『跟上次一样』师傅就该懂。把聊天记录全塞进去不就完了」小白问「两个窗口同时喊『跟上次一样』师傅听的是哪一桌你那个 List 是贴在食堂墙上的还是贴在每一桌的号牌上」小胖又说「号牌我懂用用户 id 当 key。窗口大小默认 20够用了别改。」小白追问「20 条里如果前 15 条都是在问营业时间店规和订单号被顶到上下文外面模型还看得到吗系统提示词算不算在这 20 条里面」大师先定号牌「号牌是conversationId不是用户 id。同一个用户可以有 App 会话和门店会话混用用户 id 会把两边的订单串在一起。常量名是ChatMemory.CONVERSATION_ID值是chat_memory_conversation_id。调用时必须advisors(a - a.param(ChatMemory.CONVERSATION_ID, id))。不传这个参数BaseChatMemoryAdvisor.getConversationId会断言失败因为 context 里的会话编号不允许是 null。记忆本体是MessageWindowChatMemory。建造时不传仓库默认就是InMemoryChatMemoryRepository数据在本进程的内存里。maxMessages本章设为 6足够放下店规、两轮问答又不会把闲聊无限留下。」技术映射桌牌是conversationId墙上的公共黑板是错误的全局 List每桌自己的小本子是按编号分开的ChatMemory。小白问写回时机「是我自己在业务里add还是顾问帮我写我若两边都写历史会不会出现两份用户原话」小胖说「我先手动 add 最稳顾问以后再说。」小白反对「手动 add 和顾问各写一次第二轮提示词里同一句话出现两遍token 翻倍测试还会以为模型自己复读。」大师说明顾问的两段动作「MessageChatMemoryAdvisor.before用会话编号取出已有消息拼到本次Prompt前面并把系统消息挪到列表开头。用户原话在before里就add进记忆发生在模型返回之前。after只把助手消息写回。业务代码不要再add同一轮。顾问发现提示词里已经包含同样的记忆消息时会跳过重复拼接但你手动再写一份新对象内容相等才会被认作同一条。」技术映射点单前把小本子抄到新菜单上是before用户原话在点单时就记下助手的话要等师傅说完才补上那是after。小白再问失败「模型超时的时候用户原话已经在before里写下了。下一轮重试会不会看见一句没有回答的用户话我要不要在捕获异常后把这一条删掉」小胖说「留下也没事反正用户确实说过。」小白说「留下一句没有回答的话重试时提示词里连续两条用户消息模型会以为用户说了两遍。测试若不断言消息条数只看第二轮有没有订单号发现不了这个重复。」大师把失败路径说完「用户原话在before写入助手消息在after写入。调用抛错时after不会走记忆里会留下一条没有回答的用户消息。业务若立即用同一句重试应先看这个会话的消息列表。本章的成功路径不覆盖这个清理但测试至少要知道条数会多一。不要在异常处理里再add一次用户话。」小胖问重启「小本子在口袋里下班回家是不是就没了」小白问「测试里怎么证明『重启会丢』而不必真的杀掉进程窗口只留很短时第一轮的订单号会不会在第二轮之前就被裁掉系统消息会被裁掉吗」大师把两条边界分开「新建一个MessageWindowChatMemory再按同一编号去get得到空列表这就相当于进程重启后的新仓库。真正的重启行为与此一致因为默认仓库没有落盘。持久化是第 21 章的事本章不要接数据库。裁剪规则在MessageWindowChatMemory.process新的系统消息会替换旧的系统消息条数超过maxMessages时系统消息尽量保留从更早的非系统消息开始删并且尽量从一条用户消息处下刀避免留下没有问题的孤立回答。所以窗口太小会丢掉含订单号的那句用户话第二轮就又失忆。窗口太大则把闲聊一并送给模型店规仍在但上下文被占满。6 条是本章的实验值不是生产公式。」技术映射口袋里的小本子是InMemoryChatMemoryRepository。只留最近几行是maxMessages。店规那一行尽量不擦是系统消息的保留规则。3. 项目实战3.1 环境准备沿用第 6 章的ChatClient建造方式。不新增中间件不引入 JDBC。会话编号用固定字符串app-user-42-aftersale方便测试复现。订单号仍是LC202610060018。系统提示词要求若历史里出现订单号回答中必须原样写出该订单号。系统提示词本身不包含这个号码。验收记忆之前先做两件本地检查都不需要密钥。第一新建MessageWindowChatMemory后按编号get结果为空。第二手动add一条用户消息再get列表里能看到原文。这两步证明仓库能存再挂顾问做两轮对话。两轮对话的第二句用户参数必须是「那单能退吗」测试里用assertThat(userText).doesNotContain(LC202610060018)锁住输入。窗口先用 6。改成 2 的裁剪实验单独建一个记忆实例不要改掉正在跑两轮对话的那个否则成功用例和裁剪用例会互相影响。3.2 分步实现步骤 1建造窗口记忆和顾问步骤目标有一个只保留 6 条消息的本进程记忆。packagecom.linlicang.aftersale;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.memory.MessageWindowChatMemory;importorg.springframework.ai.chat.model.ChatModel;publicclassRememberingRefundService{privatefinalChatClientchatClient;privatefinalMessageWindowChatMemorymemory;publicRememberingRefundService(ChatModelchatModel){this.memoryMessageWindowChatMemory.builder().maxMessages(6).build();varadvisorMessageChatMemoryAdvisor.builder(this.memory).build();this.chatClientChatClient.builder(chatModel).defaultAdvisors(advisor).build();}publicMessageWindowChatMemorymemory(){returnthis.memory;}}运行结果文字描述启动时没有连接数据库。memory.get(app-user-42-aftersale)在第一轮之前是空列表。可能的坑maxMessages(0)会在build()时断言失败因为实现要求大于 0。另一个坑是把记忆声明成 Spring 单例测试不clear下一条用例看见上一条用例的订单号。步骤 2两轮对话第二轮不写订单号步骤目标第二轮回答仍包含LC202610060018。publicStringask(StringconversationId,StringuserText){Stringsystem你是邻里仓售后。未发货可退。制度版本 policy-2026.10。若对话历史中出现订单号回答里必须原样写出该订单号。不要发明优惠券。;returnthis.chatClient.prompt().system(system).user(userText).advisors(a-a.param(ChatMemory.CONVERSATION_ID,conversationId)).call().content();}第一轮用户话是「我的订单是 LC202610060018还没发货。」第二轮只说「那单能退吗」运行结果文字描述第二轮正文包含LC202610060018。memory.get(conversationId)至少有两轮的用户消息和助手消息。第一轮若忘记传advisors参数调用直接失败异常信息里能看到conversationId cannot be null。可能的坑只在第一轮传了会话编号第二轮忘了传于是第二轮落到断言失败或者若你捕获异常后换了一个新编号模型又看不见历史。每一轮都要带同一个编号。系统提示词里不要写LC202610060018。上面的system只要求引用历史中的订单号。若把号码写进店规第二轮回答含有号码也证明不了记忆。验收时打印第一轮之后的memory.get确认用户原文里有这个号码再发第二轮。第二轮的用户参数本身不含号码这是测试要锁住的输入。两轮之间不要clear。clear会把刚刚记住的第一轮删掉第二轮必然失忆测试会误判成顾问失效。用例开始时clear一次是为了去掉上一则测试的残留不是为了放在两轮中间。步骤 3模拟重启步骤目标新的记忆实例按同一编号取不到旧消息。MessageWindowChatMemoryrestartedMessageWindowChatMemory.builder().maxMessages(6).build();assertrestarted.get(app-user-42-aftersale).isEmpty();运行结果文字描述新实例为空。旧实例里的消息还在直到进程退出。这就是「重启会丢」在单测里的对应物。可能的坑以为clear等于重启。clear只删一个编号别的会话还在。重启是整个仓库消失。新实例为空只说明默认仓库没有落盘。旧实例里的消息仍在直到进程退出。生产若开了两个应用实例用户第一轮打到实例 A第二轮打到实例 BB 上的新仓库是空的表现和这个测试一样。本章不引入 Redis 或 JDBC 来「修好」它。先把失忆的行为写进发布说明第 21 章再换仓库。测试可以同时持有两个MessageWindowChatMemory对同一个编号分别get一个有消息一个为空。步骤 4把窗口收成 2观察订单号被裁掉步骤目标看见窗口过小的失忆而不是只听结论。用maxMessages(2)再建一个服务。系统消息会占住名额。连续两轮之后用get打印消息类型和文本。若第一轮含订单号的用户消息已经不在列表里第二轮回答允许不再包含订单号。测试断言的是列表里已经没有那条用户原文而不是断言模型一定失忆因为模型仍可能从系统提示词里看到订单号。所以这一步同样要求系统提示词不含订单号。运行结果文字描述控制台打印出的历史里最早的用户消息消失系统消息还在。这验证了「系统消息尽量保留、更早的对话被裁掉」。可能的坑窗口设成 100把三个月闲聊都送进提示词退款规则被淹没回答开始谈优惠券。处理按业务保留最近几轮店规始终放在系统消息不靠闲聊复述店规。窗口为 2 时系统消息会占住名额更早的用户消息被裁掉。打印时看消息类型不要只看条数。条数仍可能包含系统消息和最近一条用户消息订单号那一句已经不在了。这个用例不断言模型一定失忆因为模型仍可能猜。它断言的是列表里没有第一轮原文。系统消息还在用来对照「店规尽量保留、闲聊先丢」。若你看到系统消息先消失、用户闲聊还在说明用的不是MessageWindowChatMemory的这套裁剪先核对类名。裁剪从一条用户消息处下刀是为了避免留下没有问题的孤立回答。打印时若看到助手消息打头、前面没有用户消息把窗口再调大一条重试确认不是测试自己把列表截断了。3.3 完整代码清单RememberingRefundService与测试。源码对照spring-ai-model/.../chat/memory/MessageWindowChatMemory.javaspring-ai-model/.../chat/memory/InMemoryChatMemoryRepository.javaspring-ai-client-chat/.../advisor/MessageChatMemoryAdvisor.javaspring-ai-client-chat/.../advisor/api/BaseChatMemoryAdvisor.java示例继续放在samples/linlicang-aftersale-boot。目录落地前以本章代码块为准。3.4 测试验证SpringBootTestEnabledIfEnvironmentVariable(namedDEEPSEEK_API_KEY,matches.)classRememberingRefundServiceTest{AutowiredprivateRememberingRefundServiceservice;TestvoidsecondTurnStillNamesTheOrder(){Stringidapp-user-42-aftersale;this.service.memory().clear(id);this.service.ask(id,我的订单是 LC202610060018还没发货。);Stringsecondthis.service.ask(id,那单能退吗);System.out.println(second);assertThat(second).contains(LC202610060018);}}无密钥时跳过。另有一个不连网的测试对空的新MessageWindowChatMemory调用get断言为空。这个测试不需要DEEPSEEK_API_KEY。mvn -DtestRememberingRefundServiceTest test4. 项目总结4.1 优点与缺点对比项每轮全新 PromptMessageWindowChatMemory第二轮指代丢失订单号能引用上一轮会话隔离天然隔离靠 conversationId重启无状态内存数据消失店规每次都在系统消息系统消息尽量保留成本提示词短历史越长 token 越多优点第二轮可以说「那单」不必重复订单号。不同conversationId互不看见。顾问负责读和写业务不必手拼历史。系统消息在裁剪时优先保留店规不容易被闲聊挤掉。不引入数据库本地就能演示。缺点进程重启或第二个实例上看不到这份记忆。窗口太小会丢掉订单号那一句。窗口太大增加 token店规仍可能被忽略。忘传会话编号会直接断言失败。系统提示词里若写死订单号会让记忆测试变成假阳性。4.2 适用场景适用同一 App 会话里的连续退款追问。需要演示「指代上一轮」而不接 Redis。测试要断言第二轮原文里没有订单号、回答里却有。窗口需要一个很小的上限来保护上下文。为第 21 章的持久化做行为对照。不适用多实例部署还指望记忆一致。内存仓库做不到不要在生产假装它做得到。要把三个月工单当聊天记录塞进窗口。那是检索不是窗口记忆。4.3 注意事项每一轮都传同一个ChatMemory.CONVERSATION_ID。验收记忆时系统提示词不要包含被断言的订单号。测试结束clear避免单例记忆污染下一条用例。不要同时手动add和挂顾问。4.4 常见踩坑李四看到了张三的订单号。根因是两个用户共用一个conversationId或用了用户无关的常量。处理会话编号包含渠道和会话而不是全站一个 key。发布后第二轮全部失忆。根因是滚动重启清空了InMemoryChatMemoryRepository。处理本章接受这个行为生产改用第 21 章的持久仓库。记忆测试偶发失败回答里仍有订单号但历史是空的。根因是店规模板写死了LC202610060018。处理模板只保留规则订单号只来自第一轮用户话。4.5 思考题系统消息在裁剪时被保留若每一轮都传入一句略有不同的系统提示词旧店规会不会被换掉你希望保留哪一版顾问的after在模型调用失败时还会不会写入用户原话写入了的话下一轮重试会不会把失败的那句又送给模型答案在第 10 章开头。延伸阅读与资源10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询