LLM-Cookbook 学习—— 使用 LangChain 开发应用程序>Memory——让大语言模型拥有对话记忆 📅 2026/8/26 18:17:04 一、为什么大语言模型需要 Memory在使用 ChatGPT 这类聊天机器人时我们很容易产生一种感觉AI 好像能够记住我们前面聊过的内容。例如我你好我叫皮皮鲁。 AI你好皮皮鲁 我11等于多少 AI2。 我我叫什么名字 AI你叫皮皮鲁。从结果来看AI 明显“记住”了第一轮对话。但这里需要先理解一个非常重要的概念LLM 本身并不会自动永久保存前几次 API 调用中的聊天记录。在典型的 LLM 应用中要想让模型知道之前发生了什么需要应用程序保存历史消息并在之后调用模型时把相关历史上下文再次提供给模型。因此可以简单理解为第一轮 用户输入 ↓ LLM ↓ 模型回答 ↓ 保存到 Memory第二轮历史 Memory 新的用户输入 ↓ 一起发送给 LLM ↓ 模型回答 ↓ 再次更新 Memory所以所谓模型“记得”之前的内容其实很多时候是程序保存了历史记录 ↓ 下一次调用时重新发送给模型 ↓ 模型根据这些历史信息生成回答这就是 LangChain 中Memory要解决的核心问题。教程将 Memory 定位为帮助保存、组织和跟踪对话历史从而为多轮交互提供连续上下文。二、Memory 可以理解成什么我认为可以把 Memory 理解成聊天机器人旁边的一个聊天记录本例如第一次对话Human: 我叫皮皮鲁 AI: 你好皮皮鲁Memory 把它记下来Memory Human: 我叫皮皮鲁 AI: 你好皮皮鲁第二轮用户问我叫什么名字模型看到的内容可能变成Human: 我叫皮皮鲁 AI: 你好皮皮鲁 Human: 11等于多少 AI: 2 Human: 我叫什么名字 AI:模型因此能够回答你叫皮皮鲁。所以需要注意Memory 并不是修改了模型参数而是在管理模型调用时使用的上下文。三、短期记忆和长期记忆教程将这里介绍的 LangChain Memory 理解成 LLM 的短期记忆。例如用户我叫小明 AI你好小明 用户我今年22岁 AI好的 用户我叫什么 AI小明这些都是当前对话中的上下文信息。可以简单理解成短期记忆 ↓ 当前这场对话中发生了什么而当前 LangChain 官方文档进一步把 Memory 分成Short-term Memory 短期记忆 ↓ 同一个 thread / conversation 内保存信息和Long-term Memory 长期记忆 ↓ 跨 conversation / thread 保存用户信息例如用户今天告诉 AI我喜欢喝美式咖啡。一个月后重新打开一个新对话AI 仍然知道用户喜欢美式咖啡。这更加接近长期记忆。当前 LangChain 官方文档将短期记忆定义为单个 thread 内的历史状态而长期记忆则可以跨 thread 持久保存。不过这一章主要学习的是对话级短期 Memory四、本章介绍的四种 Memory教程主要介绍以下四种 MemoryMemory核心思想ConversationBufferMemory所有历史对话全部保存ConversationBufferWindowMemory只保存最近 k 轮对话ConversationTokenBufferMemory按 Token 数量限制历史记录ConversationSummaryBufferMemory太长的历史对话用 LLM 总结成摘要这四种 Memory 本质上都是在解决同一个问题到底应该给 LLM 保留多少历史信息只是采取了不同策略。五、ConversationBufferMemory完整保存历史记录首先学习最容易理解的ConversationBufferMemoryBuffer 可以翻译成缓冲区因此ConversationBufferMemory可以理解为对话历史缓冲区。它采用的策略非常简单你说过什么 AI 回答过什么 ↓ 全部保存六、创建 ConversationChain教程中的代码from langchain.chains import ConversationChain from langchain.chat_models import ChatOpenAI from langchain.memory import ConversationBufferMemory llm ChatOpenAI(temperature0.0) memory ConversationBufferMemory() conversation ConversationChain( llmllm, memorymemory, verboseTrue )这里其实创建了三个核心对象llm memory conversation关系是ChatOpenAI ↓ llm ConversationBufferMemory ↓ memory llm memory ↓ ConversationChain ↓ conversation也就是说conversation不仅知道使用哪个 LLM还知道使用哪个 Memory因此它可以在每次调用模型时把历史聊天记录一起组织进去。七、ConversationChain 是什么前面已经学习过 Chain 的基本思想。这里ConversationChain可以简单理解成专门用于多轮对话的一条 Chain。它把Prompt LLM Memory连接了起来。可以表示成用户输入 ↓ ConversationChain │ ├── 读取 Memory │ ├── 拼接 Prompt │ ├── 调用 LLM │ └── 更新 Memory ↓ 模型回答这也是 LangChain 名字中Chain的体现。八、verboseTrue 是什么意思代码中conversation ConversationChain( llmllm, memorymemory, verboseTrue )这里verboseTrue表示输出更加详细的执行过程。例如运行conversation.predict( input你好我叫皮皮鲁 )除了模型回答以外还可能看到Entering new chain... Prompt after formatting: The following is a friendly conversation... Current conversation: Human: 你好我叫皮皮鲁 AI: Finished chain.这些内容可以帮助我们观察LangChain 到底给模型发送了什么因此学习和调试时verboseTrue非常有帮助。而正式运行、不希望输出大量调试信息时可以verboseFalse教程也通过verboseTrue展示了格式化后的 Prompt 和历史对话。九、第一轮对话发生了什么运行conversation.predict( input你好, 我叫皮皮鲁 )此时 Memory 一开始是空的Memory 空所以 Prompt 大致类似Current conversation: Human: 你好我叫皮皮鲁 AI:模型回答之后你好皮皮鲁Memory 就会被更新Human: 你好我叫皮皮鲁 AI: 你好皮皮鲁十、第二轮对话发生了什么接着conversation.predict( input11等于多少 )这次就不是只把11等于多少交给模型。因为 Memory 中已经有第一轮记录所以 Prompt 中还会出现之前的对话Current conversation: Human: 你好我叫皮皮鲁 AI: 你好皮皮鲁 Human: 11等于多少 AI:模型回答11等于2。Memory 再次更新Human: 你好我叫皮皮鲁 AI: 你好皮皮鲁 Human: 11等于多少 AI: 11等于2。十一、第三轮测试 Memory接下来故意问conversation.predict( input我叫什么名字 )此时模型能够看到Human: 你好我叫皮皮鲁所以回答你叫皮皮鲁。这就证明Memory ↓ 保存第一轮信息 ↓ 第三轮重新提供给 LLM ↓ LLM 能根据历史信息回答教程正是通过“我叫什么名字”这一问题验证ConversationBufferMemory是否成功保留前面的聊天内容。十二、memory.buffer 是什么如果想直接查看 Memory 保存的内容可以print(memory.buffer)例如Human: 你好, 我叫皮皮鲁 AI: 你好皮皮鲁 Human: 11等于多少 AI: 11等于2。 Human: 我叫什么名字 AI: 你叫皮皮鲁。所以memory.buffer可以理解成Memory 当前保存的聊天历史内容。这里的buffer就是缓冲区十三、load_memory_variables({}) 是什么还可以memory.load_memory_variables({})得到类似{ history: Human: ...\nAI: ... }这里需要特别注意memory.buffer和memory.load_memory_variables({})虽然都可以查看 Memory但是形式不同。memory.buffer更接近直接查看缓存内容Human: ... AI: ...load_memory_variables({})返回类似{ history: Human: ... AI: ... }也就是dict其中history是 Memory 对应的历史变量名称。所以可以理解成Memory ↓ load_memory_variables({}) ↓ 把 Memory 转换成 Chain / Prompt 能使用的变量教程演示了这两种查看缓存内容的方式。十四、为什么 load_memory_variables() 里面要传 {}代码memory.load_memory_variables({})这里{}表示空字典方法本身预留了一个inputs参数用于一些需要根据当前输入决定返回哪些 Memory 信息的场景。但是ConversationBufferMemory这个简单例子中并不需要额外输入因此直接{}即可。所以不要把{}理解成Memory 是空的它只是当前调用load_memory_variables()时没有额外输入参数。十五、save_context()手动往 Memory 里面添加聊天记录除了让ConversationChain自动记录历史我们还可以手工加入聊天记录。首先memory ConversationBufferMemory()这时候Memory 空然后memory.save_context( {input: 你好我叫皮皮鲁}, {output: 你好啊我叫鲁西西} )这里{input: ...}代表Human 输入而{output: ...}代表AI 输出执行以后 Memory 变成Human: 你好我叫皮皮鲁 AI: 你好啊我叫鲁西西再添加memory.save_context( {input: 很高兴和你成为朋友}, {output: 是的让我们一起去冒险吧} )Memory 就变成Human: 你好我叫皮皮鲁 AI: 你好啊我叫鲁西西 Human: 很高兴和你成为朋友 AI: 是的让我们一起去冒险吧教程通过save_context()演示了如何不经过模型调用直接向 Memory 写入输入和输出。十六、save_context() 可以怎么理解我认为可以理解为conversation.predict()是自动模式即用户输入 ↓ LLM生成回答 ↓ 自动写进Memory而memory.save_context()是手动模式即我直接告诉 Memory 这一句是用户说的 这一句是 AI 说的两者最终都可以修改Memory 中保存的聊天记录十七、ConversationBufferMemory 的问题ConversationBufferMemory的优点非常明显所有信息都保留但它也有一个非常明显的问题对话越长历史记录越长。例如第1轮 第2轮 第3轮 ... 第100轮全部保存以后Memory ↓ 越来越长 ↓ Prompt越来越长 ↓ 发送给LLM的Token越来越多这可能带来更高 Token 消耗 更高 API 成本 更慢响应 超过 Context Window 无关旧信息干扰模型当前 LangChain 官方文档也强调聊天历史会随着对话增长过长上下文不仅可能超过上下文窗口还可能导致成本和延迟增加并使模型被陈旧或无关信息干扰。因此需要一种不要全部保存的 Memory。这就是ConversationBufferWindowMemory十八、ConversationBufferWindowMemory滑动窗口记忆导入from langchain.memory import ConversationBufferWindowMemory创建memory ConversationBufferWindowMemory(k1)这里最重要的是k1表示只保留最近一定数量的对话交互。教程中设置k1因此只保存最近一轮聊天记录。十九、什么叫“滑动窗口”假设现在有第1轮 第2轮 第3轮 第4轮如果k2则 Memory 可以理解为只关注最近第3轮 第4轮当第5轮出现第1轮 第2轮 第3轮 第4轮 第5轮窗口向后移动第4轮 第5轮所以叫Sliding Window 滑动窗口可以画成1 2 [3 4] ↓ 新对话 1 2 3 [4 5]窗口不断往后移动。二十、测试 ConversationBufferWindowMemory创建memory ConversationBufferWindowMemory(k1)加入memory.save_context( {input: 你好我叫皮皮鲁}, {output: 你好啊我叫鲁西西} )再加入memory.save_context( {input: 很高兴和你成为朋友}, {output: 是的让我们一起去冒险吧} )此时查看memory.load_memory_variables({})只剩Human: 很高兴和你成为朋友 AI: 是的让我们一起去冒险吧第一轮你好我叫皮皮鲁已经不在当前窗口中了。二十一、为什么 WindowMemory 会“失忆”教程进一步执行conversation.predict( input你好我叫皮皮鲁 ) conversation.predict( input11等于多少 ) conversation.predict( input我叫什么名字 )如果k1第三轮时 Memory 只保留上一轮 Human: 11等于多少 AI: 11等于2。但是我叫皮皮鲁是在第一轮说的。已经超出了窗口范围。因此第三轮问我叫什么名字模型无法通过当前 Memory 获取“皮皮鲁”这个信息。这不是模型突然变笨而是第一轮信息 ↓ 已经被窗口 Memory 丢掉 ↓ 没有重新发送给 LLM ↓ LLM 自然不知道教程的实验结果正是如此。二十二、BufferMemory 和 WindowMemory 对比特点BufferMemoryBufferWindowMemory保存历史全部最近 k 轮是否容易丢旧信息否是Token 消耗越来越大相对稳定适合短对话是是适合很长对话容易膨胀更合适实现逻辑全保存滑动窗口简单理解ConversationBufferMemory 我全都记而ConversationBufferWindowMemory 太早的事情我忘掉只记最近的二十三、ConversationTokenBufferMemory根据 Token 控制 Memory接下来是ConversationTokenBufferMemory导入from langchain.memory import ConversationTokenBufferMemory创建memory ConversationTokenBufferMemory( llmllm, max_token_limit30 )这里最关键的是max_token_limit30即Memory 中历史内容最多控制在指定 Token 数量范围内。教程通过 30 个 Token 的较小限制演示了早期对话被裁剪掉的效果。max_token_limit控制的是模型意义上的 Token 数量而不是 Pythonlen(text)得到的字符数量。教程还提到了 OpenAI 的tiktoken库用于进行 Tokenization 和 Token 数量计算。二十六、为什么按照 Token 限制比按照“轮数”更合理假设两轮对话第一轮你好第二轮请你详细分析一下 Transformer 架构中的 Self-Attention、Multi-Head Attention、 Position Encoding……虽然都是一轮对话但 Token 数量完全不同。所以ConversationBufferWindowMemory(k5)控制的是对话轮数而ConversationTokenBufferMemory( max_token_limit1000 )控制的是实际 Token 规模从模型上下文窗口和 API Token 消耗角度看Token 往往是更加直接的控制指标。二十七、TokenBufferMemory 工作方式假设限制max_token_limit 100Memory 当前第一轮30 tokens 第二轮30 tokens 第三轮30 tokens总共90 tokens还可以保存。如果再增加第四轮30 tokens总量120 tokens超过100那么较早的历史就需要被裁掉使历史记录重新落入 Token 限制范围。可以理解成旧信息 新信息 ████ ████ ████ ████ ████ ████ ← 从较旧的信息开始裁剪因此TokenBufferMemory核心思想就是优先保留近期对话同时限制历史上下文的 Token 数量。二十八、ConversationSummaryBufferMemory用摘要保存历史信息前面的策略都会出现一个问题直接删除旧信息例如ConversationBufferWindowMemory窗口之外的信息直接消失。但是有些旧信息可能很重要比如用户姓名 用户的重要要求 项目背景 之前做出的决定如果全部删掉模型可能失去重要上下文。于是另外一种思路出现了不保存所有原文但把旧内容总结成摘要。这就是ConversationSummaryBufferMemory教程将其描述为利用 LLM 对已有历史进行自动总结并保存摘要。二十九、创建 ConversationSummaryBufferMemory教程中from langchain.memory import ConversationSummaryBufferMemory memory ConversationSummaryBufferMemory( llmllm, max_token_limit100 )这里需要特别注意llmllm为什么 Summary Memory 还需要一个 LLM因为“总结历史对话”这件事情本身就需要模型完成。所以工作过程类似历史对话太长 ↓ 调用 LLM ↓ 总结旧对话 ↓ 保存摘要 ↓ 继续加入最近对话三十、SummaryBufferMemory 和 TokenBufferMemory 最大的区别假设有第一轮 第二轮 第三轮 第四轮 第五轮如果超出限制。ConversationTokenBufferMemory更接近第一轮 ❌ 删除 第二轮 ❌ 删除 第三轮 第四轮 第五轮而ConversationSummaryBufferMemory则更接近第一、二轮 ↓ 总结 ↓ 摘要用户介绍了自己的姓名并说明了项目背景。 第三轮 第四轮 第五轮也就是原始长历史 ↓ 压缩 ↓ 保留重要语义三十一、教程中的日程案例教程创建了一个比较长的schedule其中包含8点产品团队会议 需要制作 PPT 9点到12点处理 LangChain 项目 中午与客户吃饭 需要带笔记本电脑 展示最新 LLM 示例然后memory.save_context( {input: 今天的日程安排是什么}, {output: schedule} )由于对话已经比较长Summary Memory 会生成一段历史摘要。随后问conversation.predict( input展示什么样的样例最好呢 )模型能够理解这里的样例指的是前面日程中给客户展示的 LLM 样例说明历史摘要依然为新一轮对话提供了相关上下文。三十二、摘要也会随着对话继续更新第一次摘要可能是用户今天有一个会议需要准备 PPT 上午处理 LangChain 项目 中午和客户吃饭并需要展示 LLM 示例。接下来用户问展示什么样的样例最好AI 回答以后Summary Memory 可以把新内容继续加入摘要用户今天有一个会议…… 中午要向客户展示 LLM 示例。 用户询问应该展示什么样的示例 AI 建议展示具有多样性和实际应用价值的案例。也就是说旧摘要 新对话 ↓ 新摘要教程通过两次输出load_memory_variables()展示了摘要会随着新的交互更新。三十四、一张表彻底搞清楚四种 MemoryMemory保留方式是否会丢旧信息Token 控制是否需要 LLM 总结BufferMemory全部保留基本不会差否BufferWindowMemory最近 k 轮会较好否TokenBufferMemory最近一定 Token会好否SummaryBufferMemory历史摘要 近期内容可能丢细节好是三十七、Memory 实际上属于 Context Engineering学习完这一章以后我认为可以进一步把 Memory 理解为Context Engineering 上下文工程我们真正需要解决的问题并不是怎样让模型神奇地“拥有记忆”而是每一次调用 LLM 时应该把哪些历史信息作为 Context 提供给它例如用户过去100轮对话 ↓ 哪些应该保留 ↓ 哪些可以删除 ↓ 哪些应该总结 ↓ 哪些最相关 ↓ 最终组成本次Prompt这才是 Memory 的核心价值。三十九、本章几个重要类和方法总结代码作用ConversationChain将对话 Prompt、LLM 和 Memory 连接起来ConversationBufferMemory()保存完整历史ConversationBufferWindowMemory(k...)保存最近 k 轮ConversationTokenBufferMemory(...)根据 Token 数量限制历史ConversationSummaryBufferMemory(...)将较长历史总结保存conversation.predict()输入新消息并执行对话 Chainmemory.buffer查看当前缓存memory.load_memory_variables({})读取 Memory 变量memory.save_context()手动写入一次输入与输出max_token_limit设置 Memory 的 Token 上限k设置窗口保留的最近交互数量四十一、完整执行流程假设执行conversation.predict( input我叫什么名字 )整个过程可以理解成1. 接收 input 我叫什么名字 ↓ 2. ConversationChain 读取 Memory Human: 我叫皮皮鲁 AI: 你好皮皮鲁 ↓ 3. 将历史记录 新问题组合成 Prompt Human: 我叫皮皮鲁 AI: 你好皮皮鲁 Human: 我叫什么名字 AI: ↓ 4. 调用 LLM ↓ 5. LLM 回答 你叫皮皮鲁。 ↓ 6. 将本轮输入输出保存到 Memory ↓ 7. 返回结果四十二、教程代码的版本问题这里需要特别注意。Datawhale 教程使用的是较早期 LangChain 写法例如from langchain.chains import ConversationChain from langchain.chat_models import ChatOpenAI from langchain.memory import ConversationBufferMemory但是 LangChain 当前已经进入 v1 API 体系。官方迁移文档明确说明LLMChain ConversationChain等 legacy chains 已经移入langchain-classic如果需要继续运行旧代码需要根据当前版本安装和调整相应包。因此如果现在直接按照教程安装最新 LangChain 后出现ModuleNotFoundError ImportError DeprecationWarning不一定代表自己代码写错了很可能是教程版本 ≠ 当前 LangChain v1四十三、新版 LangChain 如何理解 Memory当前 LangChain 官方文档仍然非常强调Short-term Memory但是实现思路已经更加偏向Thread State Checkpointer即某个会话 thread ↓ 保存 message state ↓ 通过 checkpointer 持久化 ↓ 下一次调用恢复同一 thread 状态而对于很长的历史记录新版 LangChain 也强调Trim Messages 删除旧消息或者Summarization 历史摘要等上下文管理策略。所以虽然代码 API 已经发生变化但这一章学习的Buffer Window Token Limit Summary这些设计思想仍然非常有价值。四十五、和上一章 Prompt 的关系上一章学习Prompt Template Model Output Parser这一章增加Memory于是整个 LLM 应用流程逐渐变成Memory │ ▼ 用户输入 ──→ Prompt Template │ ▼ LLM │ ▼ Model Response │ ▼ Output Parser │ ▼ 程序结果 同时 Model Response │ └──────────→ 更新 Memory所以 Memory 解决的是当前这一次Prompt 如何利用之前的对话信息四十六、本章最终总结这一章最核心的一句话就是LLM 本身不会凭空记住此前独立调用中的所有内容Memory 的作用是保存和管理历史上下文并在后续调用模型时把需要的信息重新提供给模型。“当前这一轮调用模型时 哪些历史信息值得重新提供给模型”这也是后面继续学习Chain、RAG、Agent 以及更加复杂的智能体 Memory 系统时非常重要的基础。本章知识结构Memory │ ├── 为什么需要 Memory │ └── LLM 调用需要历史上下文 │ ├── ConversationBufferMemory │ ├── 保存全部历史 │ ├── memory.buffer │ ├── load_memory_variables() │ └── save_context() │ ├── ConversationBufferWindowMemory │ ├── k │ └── 滑动窗口 │ ├── ConversationTokenBufferMemory │ ├── max_token_limit │ └── Token 数量控制 │ ├── ConversationSummaryBufferMemory │ ├── LLM 自动摘要 │ ├── 历史压缩 │ └── 摘要动态更新 │ └── 本质 └── Context Engineering