← 返回首页

第2章: 记忆管理:让聊天助手多轮不失忆

维护对话上下文

🤔 为什么需要记忆?

❌ 没有记忆的问题

LLM本身是无状态的!每次调用都是独立的,不记得之前说过什么。

# 没有记忆的对话 llm.invoke("我叫张三") # AI: 你好张三! llm.invoke("我叫什么名字?") # AI: 抱歉,我不知道你的名字 # 👉 AI完全忘记了刚才的对话!

✅ 有记忆的效果

记忆机制让AI记住对话历史,实现真正的多轮对话。

# 有记忆的对话 conversation.predict(input="我叫张三") # AI: 你好张三! conversation.predict(input="我叫什么名字?") # AI: 你叫张三 # 👉 AI记住了之前的对话!

💡 记忆的核心作用

  • 维护上下文:让AI理解对话的来龙去脉
  • 个性化体验:记住用户信息和偏好
  • 连贯对话:实现自然的多轮交互
  • 提升体验:避免重复询问相同问题

📚 记忆类型详解

LangChain提供了多种记忆类型,适应不同的使用场景。

✅ 环境准备 & 如何运行本页代码(强烈建议先看)

本页所有示例代码都可以复制到 memory_x.py 里运行。最常见的问题不是代码写错,而是: IDE 选的 Python 解释器 ≠ 你安装依赖的 Python 环境

1) 创建并激活虚拟环境(推荐)

# 在你的项目根目录执行 python3 -m venv .venv # macOS / Linux source .venv/bin/activate # Windows PowerShell # .venv\Scripts\Activate.ps1

2) 安装依赖(用同一个解释器的 pip)

# 关键点:永远用 "python -m pip",避免 pip 装到另一个 Python 上 python -m pip install -U pip setuptools wheel # 经典 Chain API(ConversationChain / ConversationBufferMemory)建议使用 langchain-classic # 注意:langchain-classic==1.x 依赖 langchain-core>=1.0(必须一起装对版本) python -m pip install -U \ "langchain-classic==1.0.0" \ "langchain-core>=1,<2" \ "langchain-community>=0.3,<0.4" \ "langchain-text-splitters>=1,<2" \ "langsmith>=0.1.17,<1" \ dashscope \ redis \ pymysql

3) 验证导入是否正常(先验证再跑示例)

python -c "from langchain_classic.chains import ConversationChain; from langchain_classic.memory import ConversationBufferMemory; print('import OK')"

4) 运行示例(建议统一用 python3)

# 例如:运行窗口记忆示例 python3 memory_window.py

5) IDE 里怎么选解释器(非常关键)

在 VS Code / PyCharm 里把 Python Interpreter 切到你刚创建的 .venv。 如果你看到 from langchain... 下面有红线,通常就是解释器没选对。

6) Python 3.12 + pcapy 安装报错怎么办?(不降版本)

如果你遇到 Failed to build pcapy / distutils 相关错误,这是 pcapy 自身不兼容 Python 3.12。 不降 Python 版本的做法是换成维护分支 pcapy-ng
python -m pip install pcapy-ng
如果你的代码里原来写的是 import pcapy,安装 pcapy-ng 后大多数场景仍然可以继续用; 若遇到导入差异,再根据项目实际把 import 调整为对应模块名。

7) 常见报错:ConversationSummaryBufferMemory 运行时报 Token/Tokenizer 问题

如果你在跑 ConversationSummaryBufferMemory(或 SummaryBuffer 类记忆)时遇到类似报错: Could not import transformers / GPT2TokenizerFast.from_pretrained("gpt2") / httpx.ConnectTimeout, 本质原因通常是:它需要计算 token 数,而默认 token 计数会依赖 transformers,并且可能需要从 HuggingFace 下载 gpt2 的 tokenizer 文件。
✅ 解决方案 A:安装 transformers
# 必须用你正在运行脚本的那个 python 来安装 python -m pip install -U transformers
✅ 解决方案 B:国内网络配置 HF 镜像(避免 ConnectTimeout)
# 方式1:运行脚本时临时设置(推荐) HF_ENDPOINT=https://hf-mirror.com python3 memory_11.py
✅ 解决方案 C:预缓存 gpt2 tokenizer(下载一次,后面复用)
# 先把 tokenizer 下载到本地缓存目录 HF_ENDPOINT=https://hf-mirror.com python -c "from transformers import GPT2TokenizerFast; GPT2TokenizerFast.from_pretrained('gpt2'); print('gpt2 tokenizer cached')"
提示:如果你已经缓存成功,但运行脚本仍然超时,通常是因为运行时没带 HF_ENDPOINT。 记得在运行脚本时也一起设置。

1. MessagesPlaceholder + RunnableWithMessageHistory(LCEL 正统做法)💾

特点:把对话历史作为消息列表自动注入到 Prompt 中,适合 LCEL / Runnable 风格的链式调用。

📖 工作原理

通过 RunnableWithMessageHistory 按 session_id 取出历史消息列表,并填充到 MessagesPlaceholder("history") 这个占位符位置。

  • 优点:“历史注入”逻辑清晰,和 LCEL 链天然契合
  • 缺点:历史越长 token 越多(需要配合 window/summary 等策略)
  • 适用:你在用 prompt | llm 这种 Runnable 风格写链
import os from langchain_community.chat_message_histories import ChatMessageHistory from langchain_community.chat_models import ChatTongyi from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.runnables.history import RunnableWithMessageHistory # ⚠️ 注意:请使用 python3 运行(macOS 上 /usr/bin/python 可能不是你的 pip 环境) # python3 memory_placeholder.py # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # 关键点1:Prompt 里用 MessagesPlaceholder 预留“消息列表”位置 prompt = ChatPromptTemplate.from_messages( [ ("system", "你是一个有帮助的助手。"), MessagesPlaceholder("history"), ("human", "{input}"), ] ) chain = prompt | llm # 关键点2:用一个 store 存不同 session 的 ChatMessageHistory store = {} def get_history(session_id: str) -> ChatMessageHistory: if session_id not in store: store[session_id] = ChatMessageHistory() return store[session_id] conversation = RunnableWithMessageHistory( chain, get_history, input_messages_key="input", history_messages_key="history", ) session_id = "user_1" print("=== 第1轮 ===") r1 = conversation.invoke( {"input": "我叫张三,是一名Python开发者"}, config={"configurable": {"session_id": session_id}}, ) print("AI:", r1.content, "\n") print("=== 第2轮 ===") r2 = conversation.invoke( {"input": "我的职业是什么?"}, config={"configurable": {"session_id": session_id}}, ) print("AI:", r2.content, "\n") print("=== 第3轮 ===") r3 = conversation.invoke( {"input": "我叫什么名字?"}, config={"configurable": {"session_id": session_id}}, ) print("AI:", r3.content, "\n") print("=== 当前 history messages ===") print(store[session_id].messages)

🔍 MessagesPlaceholder 和 "history" 深度解析

🤔 为什么要用 MessagesPlaceholder?

核心问题:LLM 每次调用都是独立的,不记得之前说过什么。我们需要把历史对话塞进 Prompt 里。

# 没有 MessagesPlaceholder 的写法(错误❌)
prompt = "你是有帮助的助手。历史对话:[这里要手动塞历史] 用户说:{input}"

# 有 MessagesPlaceholder 的写法(正确✅)
MessagesPlaceholder("history") ← 自动处理格式和位置

MessagesPlaceholder 的作用:专门用来插入消息列表,确保格式正确。

🔑 "history" 这个名字是随便起的吗?

不是随便起的!"history" 必须在三个地方保持一致

❌ 错误示例(名字不一致)
# Prompt 中用 "history"
MessagesPlaceholder("history")

# 配置中用 "chat_history"
history_messages_key="chat_history"

# 🚨 结果:找不到数据!
✅ 正确示例(名字一致)
# Prompt 中用 "history"
MessagesPlaceholder("history")

# 配置中用 "history"
history_messages_key="history"

# ✅ 结果:数据正确注入!
� 三处统一的 "history"
第1处:Prompt 模板声明
prompt = ChatPromptTemplate.from_messages([ ("system", "你是有帮助的助手"), MessagesPlaceholder("history"), ← 第1处:占位符名称 ("human", "{input}"), ])
第2处:RunnableWithMessageHistory 配置
conversation = RunnableWithMessageHistory( chain, get_history, history_messages_key="history", ← 第2处:配置键名 )
第3处:运行时数据传递
# RunnableWithMessageHistory 自动生成: { "history": [HumanMessage(...), AIMessage(...)], ← 第3处:实际数据 "input": "我叫什么名字?" }
🔄 实际运行过程演示
用户调用:
conversation.invoke( {"input": "我叫什么名字?"}, config={"configurable": {"session_id": "user_1"}} )
🔍 config 参数详解(关键!)
❓ 为什么必须用 config={"configurable": {"session_id": ...}} ?
1) config 不是给模型的输入,是给运行时的配置
{"input": "..."} → 业务输入,会进入 Prompt
config={...} → 运行参数,由 Runnable 框架消费,不会进 Prompt
2) configurable 是 LangChain 约定的可注入字段容器
RunnableWithMessageHistory 声明了需要 session_id
• 框架约定通过 config["configurable"]["session_id"] 传递
3) session_id 的作用:会话隔离
• 告诉 RunnableWithMessageHistory "这次属于哪个会话"
• 框架调用你提供的 get_session_history(session_id)
• 返回该会话的 ChatMessageHistory 对象
• 历史被注入到 MessagesPlaceholder("history")
🔄 config 在链路中的完整流转
1️⃣ 用户调用 → config 传入整个 pipeline
2️⃣ RunnableWithMessageHistory 层 → 从 config["configurable"]["session_id"] 取值
3️⃣ 调用 get_session_history(session_id) → 得到该会话的历史对象
4️⃣ 把历史注入 MessagesPlaceholder("history") → 拼装完整 prompt
5️⃣ 执行底层 chain (prompt → model → parser)
6️⃣ 把本轮 Human/AI 消息追加回该 session 的历史
📦 config 还能放什么?(常用字段)
configurable
session_id、user_id、tenant_id 等运行时可注入参数
tags
打标签便于检索,如 ["prod", "chat", "vip"]
metadata
追踪/审计元数据,如 user_id、order_id
callbacks
接入 LangSmith/自定义回调,做 trace/token/耗时统计
💡 最佳实践:session_id 设计
单用户多会话: session_id = f"{user_id}:{conversation_id}"
多租户: session_id = f"{tenant_id}:{user_id}:{conversation_id}"
避免串号: 确保不同用户/租户的历史天然隔离
RunnableWithMessageHistory 内部处理:
# 1. 根据 session_id="user_1" 获取历史 history = get_history("user_1") # 返回 ChatMessageHistory # 2. 构建 prompt 数据 prompt_data = { "history": history.messages, ← 这里填入实际消息列表 "input": "我叫什么名字?" } # 3. 调用 prompt 模板 # MessagesPlaceholder("history") 被替换为 history.messages
最终 Prompt:
System: 你是有帮助的助手 History: Human: 我叫张三\nAI: 你好张三!\nHuman: 我是Python开发者\nAI: 很棒! Human: 我叫什么名字?
💡 可以换成其他名字吗?

可以!但必须三处统一。比如换成 "chat_history":

# 三处都改成 "chat_history"
MessagesPlaceholder("chat_history")
history_messages_key="chat_history"
# 运行时数据变成:{"chat_history": [消息列表]}

总结:MessagesPlaceholder 就是预留位置,"history" 是数据键名,两者配合实现动态历史注入。

🧩 ChatMessageHistory:所有类型与用法总结

ChatMessageHistory 可以理解为“对话消息的存储层”。它负责保存/读取消息列表(history.messages), 供 RunnableWithMessageHistory 或传统 Memory(例如 ConversationBufferMemory)在每轮调用时注入历史。

1) 它解决什么问题?
单进程内存:程序一重启历史就没了
多实例部署:多个服务实例需要共享同一份历史(例如 Redis/MySQL)
可观测/可审计:把对话落盘,方便回放、排查、统计
2) 常见实现类型(选型速查)
类型 典型类名 特点 适用
内存 ChatMessageHistory 零依赖、最快,但进程重启即丢 本地开发 / Demo
SQLite / MySQL SQLChatMessageHistory 可持久化、可查询;需要 DB 连接 生产落盘 / 审计
Redis RedisChatMessageHistory 性能高、支持 TTL;适合多实例共享 在线对话服务
文件 FileChatMessageHistory 简单直观,但并发/检索能力弱 个人项目 / 小规模

📦 ConversationBufferMemory(传统 Chain API)

这一节把 ConversationBufferMemory 单独拿出来讲:它属于 LangChain 早期的 ConversationChain(传统 Chain API) 生态,思路是“把历史拼成文本塞回 Prompt”。

📦 ConversationBufferMemory 是什么?

ConversationBufferMemory 会把所有历史对话拼成一段文本(memory.buffer),并在每次调用 ConversationChain 时自动注入到 prompt。

一句话:信息不丢,但对话越长 Token 越多。

🧩 一句话理解(BufferMemory)

把所有历史原封不动塞进 prompt。好处是"信息不丢",代价是"越聊越贵、越聊越慢"。

✅ 适用场景
用户咨询流程短(<10轮),但需要完整回看所有细节: 例如客服工单、一次性需求澄清、面试模拟。
⚠️ 不适用场景
长对话、长文本输入(例如复盘会议记录、长期陪聊)。这类场景容易出现: Token 爆炸 / 延迟上升 / 成本失控
🖼️ 图示:所有历史都进入 prompt
History(越聊越长) U: 我叫张三 A: 你好张三 U: 我做什么? A: 你是开发者 Prompt System + History(全部) + New Input ⇒ 记得全,但 Token 增长
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferMemory from langchain_community.chat_models import ChatTongyi # ⚠️ 注意:请使用 python3 运行 # python3 memory_buffer.py # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # BufferMemory:完整保存所有对话历史(对话越长,token 越多) memory = ConversationBufferMemory() conversation = ConversationChain(llm=llm, memory=memory, verbose=True) print(conversation.predict(input="我叫张三,是一名Python开发者")) print(conversation.predict(input="我的职业是什么?")) print(conversation.predict(input="我叫什么名字?")) print("\n=== memory.buffer ===") print(memory.buffer)

💾 BufferMemory 持久化:保存到磁盘/数据库

问题:默认的 ChatMessageHistory 只保存在内存里,程序重启后历史就丢了。
解决:chat_memory 换成持久化的 SQLChatMessageHistory / RedisChatMessageHistory / FileChatMessageHistory

SQLite(单机/测试)
本地文件,零配置,适合开发和小规模部署。
Redis(分布式/高性能)
内存级速度,支持过期策略,适合多实例部署。
File(简单场景)
每个 session 一个 JSON 文件,直观可控。
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferMemory from langchain_community.chat_message_histories import SQLChatMessageHistory from langchain_community.chat_models import ChatTongyi # BufferMemory + SQLite 持久化 # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # 使用 SQLChatMessageHistory 替代内存存储 memory = ConversationBufferMemory( chat_memory=SQLChatMessageHistory( session_id="user_123", # 每个用户的唯一标识 connection_string="sqlite:///chat_history.db" # 本地 SQLite 文件 ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) # 第一次运行:写入对话 print(conversation.predict(input="我叫张三,是一名Python开发者")) print(conversation.predict(input="我今年30岁,住在北京")) print("\n=== 重启程序后再次运行 ===") print(conversation.predict(input="我叫什么名字?我的职业是什么?")) # 数据已保存到 chat_history.db,程序重启后仍然存在
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferMemory from langchain_community.chat_message_histories import SQLChatMessageHistory from langchain_community.chat_models import ChatTongyi # BufferMemory + MySQL 持久化(生产环境推荐) # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # 使用本地 MySQL 数据库 # 前置准备: # 1) 安装 MySQL 驱动:python -m pip install pymysql # 2) 创建数据库:CREATE DATABASE langchain_chat; # 3) LangChain 会自动创建表结构 memory = ConversationBufferMemory( chat_memory=SQLChatMessageHistory( session_id="user_123", connection_string="mysql+pymysql://root:password@localhost:3306/langchain_chat" ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) # 第一次运行:写入对话 print(conversation.predict(input="我叫张三,是一名Python开发者")) print(conversation.predict(input="我今年30岁,住在北京")) print("\n=== 重启程序后再次运行 ===") print(conversation.predict(input="我叫什么名字?我的职业是什么?")) # 数据已保存到 MySQL 数据库,多实例部署时共享
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferMemory from langchain_community.chat_message_histories import RedisChatMessageHistory from langchain_community.chat_models import ChatTongyi # BufferMemory + Redis 持久化(适合分布式部署) # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) memory = ConversationBufferMemory( chat_memory=RedisChatMessageHistory( session_id="user_123", url="redis://localhost:6379/0", # Redis 连接地址 ttl=3600 # 1小时后自动过期(秒) ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) print(conversation.predict(input="我叫张三,请多关照")) print(conversation.predict(input="我叫什么名字?")) # 数据保存在 Redis,可被多个服务实例共享
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferMemory from langchain_community.chat_message_histories import FileChatMessageHistory from langchain_community.chat_models import ChatTongyi # BufferMemory + 文件持久化(最简单直观) # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) memory = ConversationBufferMemory( chat_memory=FileChatMessageHistory( file_path="session_user_123.json" # 每个 session 一个文件 ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) print(conversation.predict(input="我叫张三,是一名开发者")) print(conversation.predict(input="我叫什么名字?")) # 数据保存到 session_user_123.json,可随时查看和备份

2. ConversationBufferWindowMemory - 滑动窗口记忆 🪟

特点:只保留最近K轮对话,节省Token。

📖 工作原理

只保留最近的K轮对话,旧的对话会被自动丢弃。

  • 优点:Token消耗可控,不会无限增长
  • 缺点:会丢失较早的对话信息
  • 适用:长对话、只需要近期上下文的场景

🧩 一句话理解(WindowMemory)

只记住最近 K 轮。像聊天窗口一样“只看最近消息”,能把成本控制住,但早期信息可能被忘。

✅ 适用场景
用户和你聊很久,但你只需要关心最近几轮: 例如问答助手、代码修改协作、实时咨询。
⚠️ 不适用场景
用户在前面说过的身份/约束/偏好必须一直有效: 例如长期陪伴、个性化助手、医疗/法律等强约束场景。
🖼️ 图示:窗口滑动,旧消息被丢弃
History Window(只保留最近 K 轮) 旧消息(丢弃) 最近第 K-1 轮 最近第 K 轮 Prompt System + Window(最近K轮) + New Input ⇒ 成本可控,但会遗忘早期信息
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferWindowMemory from langchain_classic.prompts import PromptTemplate from langchain_community.chat_models import ChatTongyi # ⚠️ 注意:请使用 python3 运行 # python3 memory_window.py # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # WindowMemory:只保留最近 k 轮(约 2k 条 message:user+ai) memory = ConversationBufferWindowMemory(k=2) # 关键点:用一个“更严格”的 prompt,避免模型凭常识乱猜 # 只允许基于 {history} 回答;如果历史里没有,就说不知道。 prompt = PromptTemplate( input_variables=["history", "input"], template=( "你是一个严谨的助手。\n" "规则:只能根据【对话历史】回答;不要猜测、不要脑补。\n" "如果【对话历史】里没有答案,必须回答:\"我不知道\"。\n" "并且:除非用户明确提问,否则不要在回复里重复用户的姓名/职业等身份信息。\n\n" "【对话历史】\n{history}\n\n" "【用户问题】\n{input}" ), ) conversation = ConversationChain(llm=llm, memory=memory, prompt=prompt, verbose=True) print("第1轮:") print(conversation.predict(input="我叫张三,是一名Python开发者")) print("\n第2轮:") print(conversation.predict(input="我今年30岁")) print("\n第3轮:") print(conversation.predict(input="我住在北京")) print("\n第4轮:") print(conversation.predict(input="我喜欢跑步")) print("\n第5轮 - 测试记忆:") print(conversation.predict(input="我叫什么名字?我的职业是什么?")) print("\n=== memory.buffer(窗口内当前历史)===") print(memory.buffer)

✅ 运行结果

第5轮 - 测试记忆: AI: 我不知道。 === 当前记忆 === Human: 我住在北京 AI: (确认收到) Human: 我喜欢跑步 AI: (确认收到) 👉 只保留了最近2轮对话(第3、4轮),第1轮里的“张三/Python开发者”已经不在窗口里,所以第5轮必须回答“不知道”。
💡 使用官方类的优势
  • 自动管理:不需要手动实现 trim_window 逻辑
  • 内置优化:官方类已经优化了性能和内存使用
  • 标准化:使用 LangChain 标准接口,易于维护
  • 功能完整:支持 save_context、load_memory、clear 等方法

💾 WindowMemory 持久化:滑动窗口 + 磁盘存储

问题:WindowMemory 只保留最近 K 轮,但默认也在内存中,重启后丢失。
解决:同样通过 chat_memory 注入持久化的 History 实现。

关键理解:WindowMemory 的 "窗口裁剪" 是内存层面的策略,但底层历史数据可以来自磁盘/数据库。 每次加载时从持久化存储读取完整历史,然后在内存中只保留最近 K 轮。

import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferWindowMemory from langchain_community.chat_message_histories import SQLChatMessageHistory from langchain_community.chat_models import ChatTongyi # WindowMemory + SQLite 持久化 # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # WindowMemory 使用 SQL 持久化:只保留最近 k=3 轮,但数据存数据库 memory = ConversationBufferWindowMemory( k=3, # 窗口大小:只保留最近 3 轮 chat_memory=SQLChatMessageHistory( session_id="user_window_123", connection_string="sqlite:///chat_history.db" ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) # 模拟长对话(超过窗口大小) print("=== 前5轮对话 ===") for i, topic in enumerate(["Python", "Java", "Go", "Rust", "TypeScript"], 1): print(f"\n第{i}轮:") print(conversation.predict(input=f"我喜欢用{topic}编程")) print("\n=== 第6轮 - 测试窗口记忆 ===") print(conversation.predict(input="我最喜欢的前3种编程语言是什么?")) print("\n=== 重启程序后 ===") # 重新创建 memory(从数据库加载),仍然只保留最近 3 轮 memory2 = ConversationBufferWindowMemory( k=3, chat_memory=SQLChatMessageHistory( session_id="user_window_123", connection_string="sqlite:///chat_history.db" ), return_messages=True ) conversation2 = ConversationChain(llm=llm, memory=memory2, verbose=True) print(conversation2.predict(input="我最喜欢的第一种编程语言是什么?")) # 说明:虽然数据库保存了所有历史,但 WindowMemory 只加载最近 3 轮到内存
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationBufferWindowMemory from langchain_community.chat_message_histories import RedisChatMessageHistory from langchain_classic.prompts import PromptTemplate from langchain_community.chat_models import ChatTongyi # WindowMemory + Redis 持久化(适合高并发分布式场景) # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # 使用 Redis 存储,设置 1 小时过期 memory = ConversationBufferWindowMemory( k=2, # ✅ 为了演示效果,这里故意设小一点:只保留最近 2 轮 chat_memory=RedisChatMessageHistory( session_id="session_live_123", url="redis://localhost:6379/0", ttl=3600 # 1小时后自动清理过期数据 ), return_messages=True ) prompt = PromptTemplate( input_variables=["history", "input"], template=( "你是一个严谨的助手。\n" "规则:只能根据【对话历史】回答;不要猜测、不要脑补。\n" "如果【对话历史】里没有答案,必须回答:\"我不知道\"。\n\n" "【对话历史】\n{history}\n\n" "【用户问题】\n{input}" ), ) conversation = ConversationChain(llm=llm, memory=memory, prompt=prompt, verbose=True) print("=== 写入 4 轮信息(超过 k=2)===") print(conversation.predict(input="我的名字是张三")) print(conversation.predict(input="我的职业是Python开发者")) print(conversation.predict(input="我住在北京")) print(conversation.predict(input="我喜欢跑步")) print("\n=== 提问:验证窗口裁剪(应遗忘早期信息)===") print(conversation.predict(input="我叫什么名字?我的职业是什么?")) print("\n=== memory.buffer(窗口内当前历史:应该只剩最近 2 轮)===") print(memory.buffer) print("\n=== 模拟重启脚本:同 session_id 从 Redis 继续 ===") memory2 = ConversationBufferWindowMemory( k=2, chat_memory=RedisChatMessageHistory( session_id="session_live_123", url="redis://localhost:6379/0", ttl=3600, ), return_messages=True, ) conversation2 = ConversationChain(llm=llm, memory=memory2, prompt=prompt, verbose=True) print(conversation2.predict(input="我住在哪里?我喜欢什么?")) # 说明: # - Redis 保存了该 session_id 的历史,多个服务实例可共享 # - WindowMemory 每次只取最近 k 轮注入 prompt,因此早期信息会被“窗口裁剪”

3. ConversationSummaryMemory - 摘要记忆 📝

特点:使用LLM总结历史对话,压缩信息。

📖 工作原理

随着对话进行,使用LLM将历史对话总结成简短的摘要,保留关键信息。

  • 优点:保留关键信息,Token消耗可控
  • 缺点:需要额外调用LLM,可能丢失细节
  • 适用:长对话、需要保留关键信息的场景

🧩 一句话理解(SummaryMemory)

把“很长的历史”压缩成一段摘要。模型每次只读“摘要 + 当前输入”,从而把 Token 控制住。

✅ 适用场景
长期对话里只需要记住“事实”:姓名、职业、偏好、长期目标。 例如长期助手、学习教练、用户画像。
⚠️ 风险点
摘要是“二次创作”,可能把细节总结错。 生产中要配合:摘要可追溯(保留原文)、摘要版本、以及抽样回放。
🖼️ 图示:历史被压缩成 summary
Long History 很多轮对话原文… 很多轮对话原文… summarize summary 用户关键信息… Prompt System + summary + New Input ⇒ 成本可控,但细节可能丢失
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationSummaryMemory from langchain_community.chat_models import ChatTongyi # ⚠️ 注意:请使用 python3 运行 # python3 memory_summary.py # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # SummaryMemory:用 LLM 把历史压缩成一段摘要文本 memory = ConversationSummaryMemory(llm=llm) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) print(conversation.predict(input="我叫张三,是一名Python开发者,今年30岁")) print(conversation.predict(input="我住在北京,喜欢看科幻电影")) print(conversation.predict(input="我最近在学习LangChain框架")) print("\n第4轮 - 测试记忆:") print(conversation.predict(input="总结一下我的基本信息")) print("\n=== memory.buffer ===") print(memory.buffer)

✅ 运行结果

第4轮 - 测试记忆: AI: 根据我们的对话,你的基本信息如下: - 姓名:张三 - 职业:Python开发者 - 年龄:30岁 - 居住地:北京 - 爱好:看科幻电影 - 当前学习:LangChain框架 === 对话摘要 === {'history': '用户张三是一名30岁的Python开发者,居住在北京,喜欢科幻电影,目前正在学习LangChain框架。'} 👉 历史对话被总结成简短的摘要,保留了所有关键信息!

💾 SummaryMemory 持久化:摘要 + 磁盘存储

问题:SummaryMemory 生成的摘要只保存在内存中,重启后丢失,需要重新总结。
解决:通过持久化存储保存摘要,避免每次重启后重新调用 LLM 生成摘要。

关键理解:SummaryMemory 的核心是“用 LLM 把长历史压缩成短摘要”。 持久化后,重启程序可以直接从磁盘加载摘要,不需要重新调用 LLM 总结,节省 Token 和延迟。

import os import pathlib from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationSummaryMemory from langchain_community.chat_message_histories import SQLChatMessageHistory from langchain_community.chat_models import ChatTongyi # SummaryMemory + SQLite 持久化 # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # ✅ 关键:SQLite 路径一定要稳定(避免“重启后找不到记录”) # sqlite:///chat_history.db 是相对路径,取决于你运行脚本时的工作目录(cwd)。 # 用绝对路径可以保证每次都读写同一个 DB 文件。 DB_PATH = pathlib.Path("./chat_history.db").resolve() CONN_STR = f"sqlite:///{DB_PATH}" # 三个 / 表示绝对路径 session_id = "user_summary_123" # ✅ 重启后必须保持一致,否则会被当成新会话 history = SQLChatMessageHistory(session_id=session_id, connection_string=CONN_STR) # SummaryMemory 使用 SQL 持久化 memory = ConversationSummaryMemory( llm=llm, chat_memory=history, return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) # 第一轮:写入对话并生成摘要 print("=== 第一次运行:写入对话 ===") print(conversation.predict(input="我叫张三,是一名Python开发者,今年30岁")) print(conversation.predict(input="我住在北京,喜欢看科幻电影")) print("\n=== 当前摘要 ===") print(memory.buffer) print("\n=== 重启程序后 ===") # 重新创建 memory(从数据库加载已保存的 messages) # 注意:SQLChatMessageHistory 持久化的是“对话消息”(Human/AI),不是 summary 文本本身。 print(f"DB 文件:{DB_PATH}") # ✅ 使用相同的 session_id 才能读到之前的记录 history2 = SQLChatMessageHistory(session_id=session_id, connection_string=CONN_STR) print(f"DB 中已存在 messages 数量:{len(history2.messages)}") # ⚠️ 重要:ConversationSummaryMemory 不会自动加载之前的摘要! # 它只会基于 chat_memory 中的原始消息重新生成摘要 memory2 = ConversationSummaryMemory( llm=llm, chat_memory=history2, return_messages=True ) # 🔧 关键修复:如果有历史消息,先预加载摘要 if len(history2.messages) > 0: print("📝 基于 DB 中的消息预生成摘要...") # 强制让 Memory 基于已有消息生成摘要 # 通过调用 save_context 来触发摘要生成 for i, msg in enumerate(history2.messages): if i % 2 == 0: # Human message memory2.save_context( {"input": msg.content}, {"output": ""} ) else: # AI message memory2.save_context( {"input": ""}, {"output": msg.content} ) print("✅ 摘要预生成完成") # 🔧 简化方案:直接让 ConversationChain 在第一次对话时重新生成摘要 conversation2 = ConversationChain(llm=llm, memory=memory2, verbose=True) print("📝 开始对话,Memory 会自动基于 DB 中的消息生成摘要...") print(conversation2.predict(input="总结一下我的基本信息")) print("\n=== 自动生成的摘要 ===") print(memory2.buffer) # 结论: # - ✅ messages 可以通过 SQLChatMessageHistory 跨进程持久化 # - ⚠️ SummaryMemory 的摘要需要重新生成(不会自动保存到数据库) # - 🔑 session_id 是会话的唯一标识符,换了就读不到之前的数据 # - 💡 实际应用:要么接受重新生成摘要,要么额外存储摘要本身

✅ 运行结果

=== 记忆内容 === {'history': '用户张三是一名30岁的Python开发者,居住在北京,喜欢科幻电影,目前正在学习LangChain框架。'} 👉 历史对话被总结成简短的摘要,保留了所有关键信息!

💾 SummaryMemory 持久化:摘要 + 磁盘存储

问题:SummaryMemory 生成的摘要只保存在内存中,重启后丢失,需要重新总结。
解决:通过持久化存储保存摘要,避免每次重启后重新调用 LLM 生成摘要。

关键理解:SummaryMemory 的核心是“用 LLM 把长历史压缩成短摘要”。 持久化后,重启程序可以直接从磁盘加载摘要,不需要重新调用 LLM 总结,节省 Token 和延迟。

import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationSummaryMemory from langchain_community.chat_message_histories import FileChatMessageHistory from langchain_community.chat_models import ChatTongyi # SummaryMemory + 文件持久化(适合需要查看/备份摘要的场景) # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # 每个用户的摘要保存到独立的 JSON 文件 memory = ConversationSummaryMemory( llm=llm, chat_memory=FileChatMessageHistory( file_path="summary_user_123.json" ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) # 长期对话场景 print(conversation.predict(input="我是一名产品经理,负责电商APP")) print(conversation.predict(input="我在团队里带5个人,主要做用户增长")) print(conversation.predict(input="今年OKR是提升DAU 30%")) print(conversation.predict(input="我们最近在做一个新功能叫直播带货")) print("\n=== 生成的摘要 ===") print(memory.buffer) # 摘要已保存到 summary_user_123.json,可以随时查看、备份、审计 # 文件内容示例: # [ # {"type": "human", "content": "我是一名产品经理..."}, # {"type": "ai", "content": "..."}, # ... # ]

4. ConversationSummaryBufferMemory - 混合记忆 🔄

特点:结合完整记忆和摘要记忆的优点。

📖 工作原理

保留最近的完整对话 + 较早对话的摘要,平衡信息完整性和Token消耗。

  • 优点:近期信息完整,历史信息有摘要
  • 缺点:实现较复杂,需要调用LLM
  • 适用:需要平衡完整性和效率的场景

🧩 一句话理解(SummaryBufferMemory)

“最近消息保真 + 早期消息摘要”。既能保证最近几轮的细节不丢,又能把很早的历史压缩。

✅ 适用场景
你需要“长期记住用户关键信息”,同时又要“对最近几轮细节敏感”。 例如:个人助理、项目协作、长期学习规划。
⚙️ 实战建议
设定阈值(消息条数/Token),超过阈值再摘要;同时保留: 摘要 + 最近 N 条消息,并给摘要加入“可回放”的追踪 ID。
🖼️ 图示:summary + 最近消息一起进 prompt
Memory summary (早期压缩) 最近 N 条原文消息 Prompt System + summary + recent + New Input ⇒ 兼顾长期事实与近期细节

⚠️ 常见报错:transformers 下载 gpt2 tokenizer 超时

现象:运行 SummaryBufferMemory 时报 httpx.ConnectTimeout: timed out,原因是 LangChain 需要下载 gpt2 tokenizer 来计算 token 数,但国内访问 HuggingFace 不稳定。

🔧 解决方案(推荐顺序)
1) 临时解决(运行时设置镜像)
# 设置国内镜像并运行 HF_ENDPOINT=https://hf-mirror.com python3 memory_summary_buffer.py
2) 预缓存 tokenizer(只需一次)
# 先缓存一次,以后就不需要网络了 HF_ENDPOINT=https://hf-mirror.com python -c "from transformers import GPT2TokenizerFast; GPT2TokenizerFast.from_pretrained('gpt2'); print('gpt2 tokenizer cached')"
3) 永久解决(环境变量)
# 添加到 ~/.zshrc 或 ~/.bashrc echo 'export HF_ENDPOINT=https://hf-mirror.com' >> ~/.zshrc source ~/.zshrc # 之后直接运行即可 python3 memory_summary_buffer.py
✅ 验证是否成功:第一次运行后会显示 ✅ gpt2 tokenizer cached successfully,之后即使不设置镜像也能正常运行。
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationSummaryBufferMemory from langchain_community.chat_models import ChatTongyi # ⚠️ 注意:请使用 python3 运行 # python3 memory_summary_buffer.py # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # SummaryBufferMemory:超过 max_token_limit 后,把早期历史压成 summary,并保留最近消息原文 memory = ConversationSummaryBufferMemory( llm=llm, max_token_limit=100, ) conversation = ConversationChain( llm=llm, memory=memory, verbose=True, ) print("第1轮:") print(conversation.predict(input="我叫张三,是一名Python开发者")) print("\n第2轮:") print(conversation.predict(input="我今年30岁,住在北京")) print("\n第3轮:") print(conversation.predict(input="我喜欢看科幻电影,最近在学习LangChain")) print("\n第4轮:") print(conversation.predict(input="我的职业是什么?我住哪里?")) print("\n=== memory.buffer ===") print(memory.buffer)

✅ 运行结果

=== 记忆内容 === {'history': 'System: 前3轮对话的摘要...\nHuman: 这是第4轮对话\nAI: ...\nHuman: 这是第5轮对话\nAI: ...'} 👉 较早的对话被总结,最近的对话保持完整!

💾 SummaryBufferMemory 持久化:混合策略 + 磁盘存储

问题:SummaryBufferMemory 同时维护“摘要 + 最近消息”,数据量更大,更需要持久化。
解决:通过持久化存储保存混合记忆,确保重启后既能恢复摘要也能恢复最近完整消息。

关键理解:SummaryBufferMemory 是最复杂的记忆类型,它同时包含: 1)早期历史的摘要;2)最近 N 轮的完整原文。
持久化后重启程序,这两部分都能恢复,无需重新生成摘要或丢失最近消息。

import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationSummaryBufferMemory from langchain_community.chat_message_histories import SQLChatMessageHistory from langchain_community.chat_models import ChatTongyi # SummaryBufferMemory + SQLite 持久化(最完整的生产方案) # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # SummaryBufferMemory 使用 SQL 持久化 memory = ConversationSummaryBufferMemory( llm=llm, max_token_limit=100, # 超过100 tokens时,早期历史会被总结 chat_memory=SQLChatMessageHistory( session_id="user_summary_buffer_123", connection_string="sqlite:///chat_history.db" ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) # 第一阶段:写入足够多的对话触发摘要机制 print("=== 第一阶段:写入长对话(触发摘要) ===") print(conversation.predict(input="我叫张三,是一名Python开发者")) print(conversation.predict(input="我今年30岁,住在北京,工作5年了")) print(conversation.predict(input="我喜欢看科幻电影,跑步,和编程")) print(conversation.predict(input="最近在做一个AI项目,用LangChain")) print("\n=== 当前记忆状态(摘要+最近消息) ===") print(memory.buffer) print("\n=== 重启程序后 ===") # 重新创建 memory(从数据库加载已保存的摘要+消息) memory2 = ConversationSummaryBufferMemory( llm=llm, max_token_limit=100, chat_memory=SQLChatMessageHistory( session_id="user_summary_buffer_123", connection_string="sqlite:///chat_history.db" ), return_messages=True ) conversation2 = ConversationChain(llm=llm, memory=memory2, verbose=True) # 验证:既能回答早期事实(来自摘要),也能回答近期细节(来自完整消息) print(conversation2.predict(input="我叫什么名字?我的职业是什么?")) print(conversation2.predict(input="我最近在做什麼项目?")) print("\n=== 加载后的记忆状态 ===") print(memory2.buffer) # 说明:数据库同时保存了摘要和最近消息,重启后完整恢复
import os from langchain_classic.chains import ConversationChain from langchain_classic.memory import ConversationSummaryBufferMemory from langchain_community.chat_message_histories import RedisChatMessageHistory from langchain_community.chat_models import ChatTongyi # SummaryBufferMemory + Redis 持久化(高性能分布式方案) # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) # 使用 Redis 存储,设置合适的过期时间 memory = ConversationSummaryBufferMemory( llm=llm, max_token_limit=150, chat_memory=RedisChatMessageHistory( session_id="enterprise_user_001", url="redis://localhost:6379/0", ttl=86400 # 24小时过期,适合企业级会话 ), return_messages=True ) conversation = ConversationChain(llm=llm, memory=memory, verbose=True) # 企业级场景:客户咨询历史 print(conversation.predict(input="我是某科技公司的CTO,正在评估AI方案")) print(conversation.predict(input="我们有50人的技术团队")) print(conversation.predict(input="主要关注数据安全和成本控制")) print(conversation.predict(input="希望能快速落地,3个月内见效果")) # 多实例共享:同一个 session_id 可以在多个服务实例间共享记忆 # 适合:微服务架构、负载均衡、客服系统

📊 记忆类型对比

记忆类型 保存方式 Token消耗 适用场景
BufferMemory 完整保存所有对话 高(随对话增长) 短对话
WindowMemory 只保留最近K轮 低(固定大小) 长对话,只需近期上下文
SummaryMemory LLM总结历史 中(需要总结) 长对话,需保留关键信息
SummaryBufferMemory 近期完整+历史摘要 中(平衡) 需要平衡的场景

💡 实战技巧

1. 手动管理记忆

from langchain_community.chat_message_histories import ChatMessageHistory history = ChatMessageHistory() # 手动添加消息 history.add_user_message("你好") history.add_ai_message("你好!有什么可以帮你的?") # 查看消息 print(history.messages) # 清空记忆 history.clear()

2. 持久化记忆(保存到文件)

import os from dotenv import load_dotenv from langchain_community.chat_message_histories import FileChatMessageHistory from langchain_community.chat_models import ChatTongyi from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.runnables.history import RunnableWithMessageHistory # 加载环境变量(从 .env 中读取 DASHSCOPE_API_KEY) load_dotenv() api_key = os.getenv("DASHSCOPE_API_KEY") if not api_key: raise ValueError("请先在 .env 中配置 DASHSCOPE_API_KEY") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) prompt = ChatPromptTemplate.from_messages( [ ("system", "你是一个有帮助的助手。"), MessagesPlaceholder("history"), ("human", "{input}"), ] ) chain = prompt | llm def get_file_history(session_id: str) -> FileChatMessageHistory: # 每个 session_id 绑定一个历史文件 return FileChatMessageHistory(f"{session_id}_history.json") conversation = RunnableWithMessageHistory( chain, get_file_history, input_messages_key="input", history_messages_key="history", ) session_id = "demo_user" print("=== 第1次运行:写入对话并持久化到文件 ===") conversation.invoke( {"input": "我叫李雷,今年28岁,在杭州做前端工程师"}, config={"configurable": {"session_id": session_id}}, ) conversation.invoke( {"input": "我喜欢跑步和看电影"}, config={"configurable": {"session_id": session_id}}, ) print("\n=== 模拟下次启动:重新加载同一个文件 ===") r = conversation.invoke( {"input": "请总结一下我的基本信息"}, config={"configurable": {"session_id": session_id}}, ) print("AI:", r.content)

3. 获取记忆变量

import os from langchain_community.chat_message_histories import ChatMessageHistory from langchain_community.chat_models import ChatTongyi from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder # 从环境变量读取 API Key(请确保已设置 DASHSCOPE_API_KEY) os.environ["DASHSCOPE_API_KEY"] = os.environ.get("DASHSCOPE_API_KEY", "") llm = ChatTongyi(model="qwen-turbo", temperature=0.7) history = ChatMessageHistory() prompt = ChatPromptTemplate.from_messages( [ ("system", "你是一个有帮助的助手。"), MessagesPlaceholder("history"), ("human", "{input}"), ] ) def invoke(user_input: str) -> str: messages = prompt.format_messages(history=history.messages, input=user_input) ai = llm.invoke(messages) history.add_user_message(user_input) history.add_ai_message(ai.content) return ai.content invoke("我叫韩梅梅,是一名数据分析师") invoke("我擅长用Python做数据可视化") print("=== 字符串形式的 history(自己拼接)===") history_str = "\n".join([f"[{m.type}] {m.content}" for m in history.messages]) print(type(history_str)) print(history_str) print("\n=== 消息列表形式的 history ===") print(type(history.messages)) for msg in history.messages: print(f"[{msg.type}] {msg.content}")

🧪 RunnableWithMessageHistory + Window 裁剪

演示如何用 RunnableWithMessageHistory 维护会话历史,并用 K 实现只保留最近几轮(Window)效果。

import os from langchain_community.chat_models import ChatTongyi from langchain_community.chat_message_histories import ChatMessageHistory from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain_core.runnables.history import RunnableWithMessageHistory # os.environ["DASHSCOPE_API_KEY"] = "your-api-key" llm = ChatTongyi(model="qwen-turbo", temperature=0.7) prompt = ChatPromptTemplate.from_messages( [ ("system", "你是一个有帮助的助手。"), MessagesPlaceholder("history"), ("human", "{input}"), ] ) chain = prompt | llm store = {} def get_history(session_id: str) -> ChatMessageHistory: if session_id not in store: store[session_id] = ChatMessageHistory() return store[session_id] conversation = RunnableWithMessageHistory( chain, get_history, input_messages_key="input", history_messages_key="history", ) session_id = "user_2" K = 2 # 只保留最近 K 轮(约 2K 条消息:user+ai) def trim_window(history: ChatMessageHistory, k: int) -> None: keep = k * 2 if len(history.messages) > keep: history.messages = history.messages[-keep:] def ask(text: str) -> str: r = conversation.invoke( {"input": text}, config={"configurable": {"session_id": session_id}}, ) trim_window(store[session_id], K) return r.content print("第1轮:") ask("我叫张三") print("\n第2轮:") ask("我是Python开发者") print("\n第3轮:") ask("我今年30岁") print("\n第4轮:") ask("我住在北京") print("\n第5轮 - 测试记忆:") response = ask("我叫什么名字?我的职业是什么?") print("AI:", response) print("\n=== 当前记忆(只保留最近 K 轮)===") print(store[session_id].messages)

🧪 手写 SummaryBuffer(summary + 最近消息)

用一个最小可运行示例手写“摘要 + 最近消息”的混合记忆:超过阈值就把更早的消息压缩进 summary,并只保留最近 N 条原文消息。

import os from langchain_community.chat_models import ChatTongyi from langchain_community.chat_message_histories import ChatMessageHistory from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder #os.environ["DASHSCOPE_API_KEY"] = "your-api-key" llm = ChatTongyi(model="qwen-turbo", temperature=0.7) history = ChatMessageHistory() summary = "" chat_prompt = ChatPromptTemplate.from_messages( [ ("system", "你是一个有帮助的助手。历史摘要:{summary}"), MessagesPlaceholder("history"), ("human", "{input}"), ] ) summary_prompt = ChatPromptTemplate.from_messages( [ ("system", "把对话总结成不超过80字摘要,只保留关键事实:"), MessagesPlaceholder("history"), ] ) # 关键事实的作用:通过只保留最重要的信息,既节省了 token 成本,又确保了关键信息不会丢失 def maybe_summarize(max_messages: int = 8) -> None: global summary # 超过阈值:把较早消息压缩进 summary,只保留最近 max_messages 条原文 if len(history.messages) <= max_messages: return s_messages = summary_prompt.format_messages(history=history.messages) summary = llm.invoke(s_messages).content history.messages = history.messages[-max_messages:] for i in range(5): user_input = f"这是第{i+1}轮对话" messages = chat_prompt.format_messages(summary=summary, history=history.messages, input=user_input) ai = llm.invoke(messages) history.add_user_message(user_input) history.add_ai_message(ai.content) maybe_summarize(max_messages=6) print(f"第{i+1}轮: {ai.content}\n") print("=== 记忆内容(summary + 最近消息)===") print("summary:", summary) print("history messages:", history.messages)

✅ 课后习题与常见面试题

这章的目标不是“记住名字”,而是让你能在真实业务里选对记忆方案,并能把记忆做成可控、可观测、可扩展的工程能力。

📌 基础练习题(概念 + 选型)

1. Buffer / Window / Summary / SummaryBuffer 的核心差异是什么?

请从“信息完整性、成本、风险(遗忘/误总结)”三方面对比,并举 1 个你项目里的典型场景选择其一。

2. 为什么“多用户/多会话”必须引入 session_id?

如果不区分 session,会出现什么线上事故?你如何在后端接口里设计它(例如 header/cookie/body)?

💻 代码题(复制即可运行)

建议:在一个不包含本项目 static/langchain 的路径下运行示例,避免目录名遮蔽 pip 安装的包。

C1. 【编程题】实现“多会话隔离”:同一个服务进程里同时维护 2 个用户的记忆

要求:用 RunnableWithMessageHistory;user_a 记住“张三/开发者”,user_b 记住“李四/产品经理”;互相提问时不能串台。

C2. 【编程题】实现“窗口记忆”:只保留最近 K 轮(并证明它会遗忘)

要求:写一个 trim_window(),每轮对话后裁剪 history.messages;让模型在第 5 轮问“我叫什么名字?”时答不出来。

C3. 【编程题】实现“文件持久化记忆”:重启程序后还能记得之前信息

要求:用 FileChatMessageHistory;第一次运行写入两轮消息;第二次运行用同一个 session_id 继续提问并能回答。

💼 常见面试题(偏工程)

1.【工程题】你如何评估“记忆”带来的成本,并让它可观测?

请覆盖:token 使用、延迟、失败率、session 维度统计、采样回放。

2.【架构题】如果要把记忆存到 Redis / 数据库,你会怎么设计?

请覆盖:数据结构、过期策略(TTL)、并发写入、合规(隐私/脱敏)。

📝 本章小结

🎓 你学到了什么?

  • 记忆的重要性:LLM无状态,需要记忆维护上下文
  • BufferMemory:保存完整历史,适合短对话
  • WindowMemory:滑动窗口,只保留最近K轮
  • SummaryMemory:LLM总结历史,节省Token
  • SummaryBufferMemory:混合方案,平衡效率和完整性
  • 实战技巧:手动管理、持久化、获取记忆变量

🚀 下一步学习

掌握了记忆管理后,下一章我们将学习Prompt模板, 让提示词更加灵活和可复用!

← 上一章 下一章 →