LangChain4j 对话记忆:让模型记住聊过什么,三种方式实测对比 LangChain4j 对话记忆让模型记住聊过什么三种方式实测对比作者鱼宵 LangChain4j实战精通营 · 第 4 篇你有没有遇过这种尴尬跟客服机器人说我叫小明它回好的小明过两句你问那我叫什么“它一本正经地说你还没告诉我”。你气得想摔手机——它刚才明明还叫你小明这不是机器人笨是大模型本身就是个失忆的实习生。这一课我用三个对照实验把这事彻底讲透带记忆、无记忆、窗口溢出。同三句话、同一个模型只有有没有把历史拼进请求这一个变量结果天差地别。代码全在仓库lesson-04/clone 下来跑一遍你会亲眼看到它怎么从一口一个小明变成你没告诉我——末尾挑战题改个窗口大小遗忘现象当场反转。一、核心原理模型什么都记不住记忆是外挂的一句话大模型两次调用之间不保留任何上下文。所谓记忆其实是框架每次都把历史对话重新发给模型一遍。1. 无状态每次打电话都得重新自我介绍打客服电话对面是个刚换班、完全不记得上一通电话的实习生。你上一句说过我叫小明这通电话他压根不知道。要让他记得你只能每次都把前情提要重新念一遍。HTTP 本身就是无状态的网站登录态靠 Cookie/Session 这些外挂实现——大模型也一样。模型是个无状态的 HTTP 服务服务端不存你会话。2. ChatMemory旁边放个聊天记录本ChatMemory是 LangChain4j 提供的对话记忆抽象本质就是一个保存历史消息列表的容器。工作流程是这样用户说我叫小明 ↓ 框架把这条 UserMessage 存进记忆本 ↓ 把【记忆本里的全部消息】打包发给模型 ↓ 模型回答好的小明 ↓ 框架把这条 AiMessage 也存进记忆本 用户又说我喜欢打篮球 ↓ 这条 【记忆本里之前的全部消息】一起发给模型 ← 关键 ↓ 模型这次看到了上面的小明看出来了吗模型一次都没真记住是你框架每次都在复读前情提要。3. MessageWindowChatMemory小白板只留最近 N 条上下文窗口有上限、token 要钱本子不能无限写。窗口记忆只保留最近 N 条消息更早的自动擦掉。这里有个面试高频坑withMaxMessages(N)的 N 是消息条数不是轮数。一轮对话 1 条用户消息 1 条 AI 消息 2 条。所以容量实际保留10约最近 5 轮2只留最近 1 轮用户 1 AI 1再多聊一句最早的就被挤掉本课实验三就是把容量设成 2故意让它忘光早期信息。4. AiServices 织记忆就加一行上一课见过的 AiServices多挂一个.chatMemory(memory)就自动帮你做存历史 发历史全套AssistantassistantAiServices.builder(Assistant.class).chatModel(chatModel)// 用哪个模型.chatMemory(memory)// 挂哪本记忆本 ← 多轮记忆的全部秘密.build();之后你只管assistant.chat(你好)历史怎么拼、怎么存框架全包。注意必须复用同一个 Assistant 实例——记忆本绑在实例上每轮 new 一个新的记忆永远是空的。二、动手一次跑完三个实验环境沿用前几课JDK 17、DEEPSEEK_API_KEY已配好。这课是控制台课程序不读键盘跑完自动退出。cd langchain4j-journey\lesson-04$env:JAVA_HOMEC:\Program Files\Java\jdk-17mvn clean install-DskipTests java-Dfile.encodingUTF-8-jar target\lesson-04-1.0.0.jar想把输出存下来对照用cmd /c java -Dfile.encodingUTF-8 -jar target\lesson-04-1.0.0.jar out.txt 21。三、关键代码三个实验一个主类本课模型配置LlmConfig和第 1 课一样阻塞式OpenAiChatModelmaxTokens(200)temperature(0.7)不重复贴。重点是这个主类——三个实验并排对照着看packagecom.langchain4j.lesson04;importdev.langchain4j.memory.ChatMemory;importdev.langchain4j.memory.chat.MessageWindowChatMemory;importdev.langchain4j.model.chat.ChatModel;importdev.langchain4j.service.AiServices;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/** * 第 4 课主程序一次跑完三个对照实验。 * demoWithMemory —— 带记忆报名字/爱好回头问它记得住 * demoWithoutMemory—— 无记忆裸调同样三句话它失忆 * demoWindowForget —— 窗口溢出容量设 2多聊几轮早期信息被自动擦掉 */SpringBootApplicationpublicclassChatWithMemoryApplicationimplementsCommandLineRunner{privatefinalChatModelchatModel;// 阻塞式模型本身无状态publicChatWithMemoryApplication(ChatModelchatModel){this.chatModelchatModel;}publicstaticvoidmain(String[]args){SpringApplication.run(ChatWithMemoryApplication.class,args);}Overridepublicvoidrun(String...args){demoWithMemory();demoWithoutMemory();demoWindowForget();System.out.println( 第 4 课全部实验结束 );}/** 实验一带记忆。三件套 窗口记忆 AiServices 织记忆 同一个实例反复聊。 */privatevoiddemoWithMemory(){System.out.println( 实验一带记忆窗口容量 10约等于保留最近 5 轮 );// 容量 10 10 条消息 ≈ 5 轮一轮用户1条AI1条ChatMemorymemoryMessageWindowChatMemory.withMaxMessages(10);// 把接口、模型、记忆本三者织在一起AssistantassistantAiServices.builder(Assistant.class).chatModel(chatModel).chatMemory(memory).build();ask(assistant,我叫小明请帮我记住我的名字。);ask(assistant,我最喜欢打篮球也请帮我记住。);// 这一问一个字上下文都没提但模型该从记忆本翻出前两轮ask(assistant,那我叫什么名字我喜欢什么运动只用一句话回答。);}/** 实验二无记忆对照。直接拿模型裸调每次都是独立请求。 */privatevoiddemoWithoutMemory(){System.out.println( 实验二无记忆裸调 chatModel.chat每次都不带历史 );System.out.println(用户第1句我叫小明请帮我记住我的名字。);System.out.println(AI 第1答chatModel.chat(我叫小明请帮我记住我的名字。));System.out.println(用户第2句我最喜欢打篮球也请帮我记住。);System.out.println(AI 第2答chatModel.chat(我最喜欢打篮球也请帮我记住。));System.out.println(用户第3句那我叫什么名字我喜欢什么运动只用一句话回答。);// 这句话里模型根本没见过小明/篮球那是上两个请求的事System.out.println(AI 第3答chatModel.chat(那我叫什么名字我喜欢什么运动只用一句话回答。));}/** 实验三窗口溢出。容量 2 只留最近 1 轮中间插闲聊把前两轮挤出去。 */privatevoiddemoWindowForget(){System.out.println( 实验三窗口溢出容量 2 只留最近 1 轮多聊会自动忘 );ChatMemorytinyMemoryMessageWindowChatMemory.withMaxMessages(2);// 只留 1 轮AssistantforgetfulAiServices.builder(Assistant.class).chatModel(chatModel).chatMemory(tinyMemory).build();ask(forgetful,我叫小明请帮我记住我的名字。);// 第1轮写入名字ask(forgetful,我最喜欢打篮球也请帮我记住。);// 第2轮名字这条开始被挤ask(forgetful,1 加 1 等于几一句话回答。);// 第3轮闲聊把前两轮彻底挤出窗口ask(forgetful,那我叫什么名字只用一句话回答。);// 此时记忆本里只剩11那一来一回}/** 小工具打印用户的话调一次助手打印回答。 */privatevoidask(Assistantassistant,StringuserMessage){System.out.println(用户说userMessage);System.out.println(AI 回答assistant.chat(userMessage));System.out.println(---- 一轮结束 ----);}}Assistant接口本身极简——就声明一个String chat(String userMessage)AiServices 给它生成实现配了记忆后这个方法自动带上下文。四、实测输出三组实验教科书级对照以下为 2026-10-05 本机真实运行DeepSeek 实测已省略启动 banner。这节是本课最值钱的证据三组输出挨着看。实验一带记忆窗口容量 10。 实验一带记忆窗口容量 10约等于保留最近 5 轮 用户说我叫小明请帮我记住我的名字。 AI 回答好的小明我已经记住你的名字了。 以后你再来找我聊天我会知道你是小明。有什么我可以帮你的吗 ---- 一轮结束 ---- 用户说我最喜欢打篮球也请帮我记住。 AI 回答好的小明我已经记住了你叫**小明**你最喜欢**打篮球** 。以后聊天的时候我会记得这些信息。 ---- 一轮结束 ---- 用户说那我叫什么名字我喜欢什么运动只用一句话回答。 AI 回答你叫小明最喜欢打篮球。 ---- 一轮结束 ----关键证据第三问里我们一个字都没提小明/篮球模型却答出了你叫小明最喜欢打篮球——因为框架自动把前两轮塞进了请求。记忆生效了。实验二无记忆裸调每次不带历史。 实验二无记忆裸调 chatModel.chat每次都不带历史 用户第1句我叫小明请帮我记住我的名字。 AI 第1答好的小明我记住你的名字了以后有什么需要帮忙的随时告诉我。 用户第2句我最喜欢打篮球也请帮我记住。 AI 第2答好的我记住了你最喜欢打篮球。 …… 用户第3句那我叫什么名字我喜欢什么运动只用一句话回答。 AI 第3答我不知道你的名字也不知道你喜欢什么运动因为你还没有告诉我这些信息。关键证据第 1、2 答明明一口一个小明“打篮球”第 3 答却矢口否认——因为这是三次互不相干的独立请求模型对前两次毫无印象。和实验一形成教科书级对照。实验三窗口溢出容量 2多聊自动遗忘。 实验三窗口溢出容量 2 只留最近 1 轮多聊会自动忘 用户说我叫小明请帮我记住我的名字。 AI 回答好的小明我记住你的名字了…… ---- 一轮结束 ---- 用户说我最喜欢打篮球也请帮我记住。 AI 回答好的小明我也记住你最喜欢打篮球了 …… ---- 一轮结束 ---- 用户说1 加 1 等于几一句话回答。 AI 回答1 加 1 等于 2。 ---- 一轮结束 ---- 用户说那我叫什么名字只用一句话回答。 AI 回答我不知道你的名字因为你没有告诉我。 ---- 一轮结束 ----关键证据一开始它一口一个小明闲聊一句11之后再问名字它就说你没告诉我——容量 2 只留最近 1 轮小明那条早被挤出窗口擦掉了。窗口遗忘肉眼可见。排查提示实验三它居然还记得名字DeepSeek 偶尔聪明过头在11那轮后多插两轮闲聊把窗口挤得更彻底遗忘会更稳定。五、挑战题改参数看看会怎样⭐把窗口从 2 改成 10把实验三的withMaxMessages(2)改成withMaxMessages(10)再跑一遍报名字→闲聊→问名字看这次它还记不记得名字解释为什么。答案就在demoWindowForget那行withMaxMessages。⭐多挤两轮在实验三11后面再加两轮无关问题如今天星期几“讲个笑话”再问名字确认遗忘更彻底。答案就在demoWindowForget那串ask(...)调用。⭐⭐每轮 new 一个新 Assistant把实验一改成循环里每轮AiServices.builder()...build()新建实例再问我叫什么看它是不是又答不上来——体会必须复用同一实例。答案在demoWithMemory的 builder 用法。六、生产环境进阶三个加分项1. 对话越长越贵窗口是成本闸。“记忆”每次都把历史重新发一遍聊了 20 轮第 21 轮请求里就塞了 40 条历史全按输入 token 计费。窗口记忆只留最近几轮就是为了控成本。2. 多用户要做记忆隔离。现在记忆本是全局单例所有用户共用一本——张三跟机器人聊的李四全看得见。生产用ChatMemoryProvider按用户 ID 返回不同记忆本互不串。3. 长对话上摘要记忆。窗口记忆简单但会丢早期信息摘要记忆把老对话压成一段小摘要塞进去省 token、可能丢细节。生产常混用。七、面试回答模板面试官为什么说大模型是无状态的每次调用之间发生了什么一句话模型两次调用之间不保留任何上下文它每次都像第一次见你。展开它是个无状态 HTTP 服务服务端不存你的会话所谓多轮记忆是应用层框架每次把历史消息重新拼进请求实现的模型本身一个字没记住。指向本课第一节 实验二裸调实测追问ChatMemory 怎么让模型记住的历史消息在请求里怎么拼它本质是个消息列表容器。AiServices 配了.chatMemory()后自动做三件事把用户消息存进列表 → 发请求时把列表里全部历史一起带上 → 把模型回答也存回去。你看到的它记得上文其实是每次都把前情提要重发一遍。指向本课第 2 小节 实验一追问MessageWindowChatMemory 的窗口大小记的是轮数还是消息数是消息条数不是轮数。一轮 用户 1 条 AI 1 条 2 条所以withMaxMessages(10)只留约 5 轮。超窗口自动丢最老的消息。本课实验三把容量设 2闲聊一句就把小明挤出去了模型立马失忆。指向本课第 3 小节 实验三实测追问为什么对话越长越费钱因为记忆 每次都把历史重发一遍多聊一轮之后每轮请求都多塞 2 条历史输入 token 线性涨。这就是长对话要做窗口记忆/摘要记忆的根本原因。指向本课第六节八、总结表坑现象解法裸调chatModel.chat()问完就忘答不上上文记忆是 AiServices 层加的ChatModel 本身无状态要织.chatMemory()容量 N 理解成轮数窗口演示对不上现象N 是消息条数一轮 2 条withMaxMessages(2)只留 1 轮每轮 new 新 Assistant记忆永远是空的记忆本绑实例多轮必须复用同一个 AssistantChatMemory包名写错import 报错接口在dev.langchain4j.memory实现在...memory.chat子包多用户共用一本记忆用户之间串话用ChatMemoryProvider按用户 ID 隔离记忆本九、关于这个系列本文是「Java 后端实战精通营」系列第 4 篇原则实战驱动、由浅到深、面试向每篇文章的结论都可以亲手验证。LangChain4j 实战精通营10 课https://gitee.com/j67mk2/langchain4j-journey本文对应源码位置lesson-04/控制台工程主类ChatWithMemoryApplication内含带记忆 / 无记忆 / 窗口溢出三个对照实验系列文章一览按发布顺序篇主题1LangChain4j 第一个对话程序手写 LLM 调用链路看清模型怎么开口2LangChain4j 提示词工程模板 few-shot 温度提示词从写死到复用3LangChain4j 结构化输出让模型吐出 Java 对象JSON 不再手写解析4LangChain4j 对话记忆让模型记住聊过什么三种方式实测对比5LangChain4j 流式输出打字机效果 SSE回答不再干等三秒6LangChain4j 工具调用Function Calling 让模型动手查数据多工具连调实测7LangChain4j 向量检索文本怎么变成可搜索的坐标8LangChain4j RAG 流水线手写问答全流程给 AI 开卷考试9LangChain4j Agent多步推理自主调用放手让实习生自己安排工作10LangChain4j 企业智能客服RAG 工具 记忆 流式 兜底验收 5 项全过下一篇预告《LangChain4j 流式输出打字机效果 SSE回答不再干等三秒》——本课你每次都是等半天整句话一次性蹦出来。下一课换 StreamingChatModel TokenStream让回答一个字一个字往外蹦再用 SSE 把这股字流推到浏览器网页上。跑完有任何报错把终端输出发评论区一起排查。标签建议LangChain4j、对话记忆、ChatMemory摘要建议≤256 字模型为什么聊两句就失忆因为大模型本身无状态所谓记忆是框架每次把历史重新发一遍。本文用三个对照实验实测带记忆窗口 10第三问它准确答出你叫小明最喜欢打篮球无记忆裸调三次独立请求它矢口否认窗口容量设 2闲聊一句它立马忘了你是谁。讲清 withMaxMessages 的 N 是消息条数不是轮数附 3 道改参数挑战题与面试回答模板源码在 lesson-04 可 clone 直接跑。