LangChain 持久化对话记忆:从入门到生产级实践
大语言模型本身是无状态的,每次调用都是全新对话。本文带你从零实现 LangChain 对话记忆的持久化,覆盖 JSON、SQLite、Redis 三种方案;深入讲解
ConversationSummaryMemory多轮对话摘要压缩;并介绍新版 LCEL 写法,帮你在实际项目中落地多轮对话。
一、为什么需要持久化对话记忆?
LLM 的无状态性是一把双刃剑。在单轮问答中没有问题,但在以下真实场景中,缺少记忆会让用户体验大打折扣:
- 客服机器人:用户说了订单号,下一句就忘了
- 代码助手:每轮都要重新描述上下文
- 个人 AI 助手:重启应用后对话从零开始
LangChain 的 Memory 模块解决的正是这个问题------在每次调用前自动将历史消息注入 Prompt,在每次响应后自动保存新消息。
但默认的 Memory 是内存存储 ,进程重启即消失。本文的核心就是:如何让它真正持久化。
📌 环境说明:本文使用 LangChain
0.2.x+,Python3.10+,以 OpenAI API 作为示例 LLM。
二、Memory 核心类型速览
LangChain 内置了多种 Memory 策略,先建立认知:
| 类型 | 策略 | 适用场景 | Token 消耗 |
|---|---|---|---|
ConversationBufferMemory |
保存全量历史 | 短对话 / 调试 | 高 |
ConversationBufferWindowMemory |
保留最近 k 轮 | 一般对话 | 中 |
ConversationSummaryMemory |
LLM 摘要压缩 | 长对话 / 省 token | 低 |
ConversationSummaryBufferMemory |
摘要 + 近期缓冲 | 兼顾细节与长度 | 中 |
本文重点不在于类型本身,而在于将这些 Memory 与持久化存储后端结合。
三、快速上手:内存记忆(跑通基础)
先用最简单的方式跑通,建立直觉。
安装依赖
pip install langchain langchain-openai openai
最小可运行示例
ini
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
import os
os.environ["OPENAI_API_KEY"] = "sk-..."
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
memory = ConversationBufferMemory(return_messages=True)
chain = ConversationChain(llm=llm, memory=memory)
# 第一轮
resp1 = chain.invoke({"input": "我叫小明,今年 25 岁"})
print(resp1["response"])
# 第二轮 ------ 模型能记住上文
resp2 = chain.invoke({"input": "我叫什么名字?"})
print(resp2["response"])
# 输出:你叫小明,今年 25 岁。
问题所在 :memory 是一个普通 Python 对象,程序一旦重启,对话历史全部丢失。
四、持久化方案一:JSON 文件存储
最轻量的方案,不依赖任何外部服务,将消息历史序列化到本地 JSON 文件。适合本地脚本、个人项目。
自定义 BaseChatMessageHistory
python
import json
from pathlib import Path
from typing import List
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import BaseMessage, messages_from_dict, messages_to_dict
class JSONChatHistory(BaseChatMessageHistory):
"""将对话历史持久化到本地 JSON 文件"""
def __init__(self, session_id: str, save_dir: str = "./chat_history"):
self.path = Path(save_dir) / f"{session_id}.json"
self.path.parent.mkdir(parents=True, exist_ok=True)
@property
def messages(self) -> List[BaseMessage]:
if not self.path.exists():
return []
with open(self.path, "r", encoding="utf-8") as f:
data = json.load(f)
return messages_from_dict(data)
def add_messages(self, messages: List[BaseMessage]) -> None:
existing = messages_to_dict(self.messages)
existing.extend(messages_to_dict(messages))
with open(self.path, "w", encoding="utf-8") as f:
json.dump(existing, f, ensure_ascii=False, indent=2)
def clear(self) -> None:
if self.path.exists():
self.path.unlink()
接入 ConversationChain
ini
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 指定 session_id,不同用户用不同 id 隔离
history = JSONChatHistory(session_id="user_001")
memory = ConversationBufferMemory(
chat_memory=history,
return_messages=True
)
chain = ConversationChain(llm=llm, memory=memory)
resp = chain.invoke({"input": "我叫小明,今年 25 岁"})
print(resp["response"])
# 重启程序后重新执行:
# history 从 user_001.json 加载,模型依然记得你是小明
✅ 现在对话记录会保存在
./chat_history/user_001.json,程序重启后自动恢复。
五、持久化方案二:SQLite 存储
比 JSON 更结构化,支持多会话查询,适合需要查历史记录的场景。LangChain 社区包已内置支持。
安装依赖
pip install langchain-community
使用 SQLiteChatMessageHistory
ini
from langchain_community.chat_message_histories import SQLChatMessageHistory
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
history = SQLChatMessageHistory(
session_id="user_001",
connection_string="sqlite:///chat_history.db" # 本地 SQLite 文件
)
memory = ConversationBufferMemory(
chat_memory=history,
return_messages=True
)
chain = ConversationChain(llm=llm, memory=memory)
resp = chain.invoke({"input": "帮我记住:我最喜欢的颜色是蓝色"})
print(resp["response"])
# 程序重启后,记忆依然存在
resp2 = chain.invoke({"input": "我最喜欢什么颜色?"})
print(resp2["response"])
# 输出:你最喜欢的颜色是蓝色。
数据库文件
chat_history.db会自动创建,可用 DB Browser for SQLite 等工具查看历史消息。
六、持久化方案三:Redis 存储(生产推荐)
Redis 是生产环境下最常见的选择,支持:
- 多用户 session 隔离(不同 session_id 对应不同 key)
- TTL 过期控制(对话记录自动清理)
- 高并发读写(内存数据库,毫秒级响应)
安装依赖
pip install langchain-community redis
使用 RedisChatMessageHistory
ini
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
history = RedisChatMessageHistory(
session_id="user_001",
url="redis://localhost:6379/0",
ttl=3600 # 1 小时后自动过期,单位秒
)
memory = ConversationBufferMemory(
chat_memory=history,
return_messages=True
)
chain = ConversationChain(llm=llm, memory=memory)
resp = chain.invoke({"input": "我在北京工作,做后端开发"})
print(resp["response"])
多用户隔离示例
ini
def get_chain(user_id: str) -> ConversationChain:
"""为每个用户创建独立的对话链"""
history = RedisChatMessageHistory(
session_id=f"chat:{user_id}", # key 格式:chat:user_001
url="redis://localhost:6379/0",
ttl=86400 # 24 小时过期
)
memory = ConversationBufferMemory(
chat_memory=history,
return_messages=True
)
return ConversationChain(
llm=ChatOpenAI(model="gpt-4o-mini", temperature=0),
memory=memory
)
# 用户 A 和用户 B 完全隔离
chain_a = get_chain("user_a")
chain_b = get_chain("user_b")
chain_a.invoke({"input": "我叫 Alice"})
chain_b.invoke({"input": "我叫 Bob"})
# Alice 的链不会知道 Bob 的信息
print(chain_a.invoke({"input": "我叫什么?"})["response"])
# 输出:你叫 Alice。
七、多轮对话总结:用 ConversationSummaryMemory 压缩历史
为什么需要对话总结?
随着对话轮次增多,全量历史消息会迅速消耗大量 Token,导致:
- 调用成本指数级上升
- 超出模型上下文窗口(如 GPT-3.5 的 4K / 16K 限制)
- 响应速度变慢
解决思路 :不再保存每一条原始消息,而是用 LLM 将历史对话实时压缩为摘要,只保留摘要 + 最近几轮原文。
ConversationSummaryMemory:纯摘要模式
每轮对话结束后,LLM 自动更新一份滚动摘要,不保留原始消息。
python
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationSummaryMemory
from langchain.chains import ConversationChain
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
memory = ConversationSummaryMemory(
llm=llm, # 用于生成摘要的 LLM
return_messages=True
)
chain = ConversationChain(llm=llm, memory=memory)
chain.invoke({"input": "我叫小明,是一名前端工程师,在深圳工作"})
chain.invoke({"input": "我最近在学习 React 和 TypeScript"})
chain.invoke({"input": "我的目标是今年内转型全栈"})
# 查看当前摘要内容(不是原始消息列表)
print(memory.buffer)
# 输出类似:
# 小明是一名前端工程师,在深圳工作。
# 他正在学习 React 和 TypeScript,目标是今年内转型全栈开发。
# 第四轮:模型基于摘要回答,而非原始对话
resp = chain.invoke({"input": "根据我的情况,给我推荐一个学习路线"})
print(resp["response"])
✅ 优点:Token 消耗恒定(摘要长度固定),适合超长对话。
⚠️ 缺点:摘要有信息损失,细节问题(如"我刚才说的第二点是什么")可能无法准确回答。
ConversationSummaryBufferMemory:摘要 + 缓冲混合模式(推荐)
这是更实用的方案:近期消息保持原文,超出 Token 阈值的旧消息自动压缩为摘要。兼顾细节与长度控制。
python
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationSummaryBufferMemory
from langchain.chains import ConversationChain
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=300, # 超过 300 token 的旧消息才会被摘要
return_messages=True
)
chain = ConversationChain(llm=llm, memory=memory)
# 模拟多轮对话
topics = [
"我叫小红,是产品经理",
"我们公司在做一款 AI 写作工具",
"目标用户是内容创作者和自媒体博主",
"核心功能是自动生成大纲和润色文章",
"我们计划下个月上线 Beta 版",
]
for msg in topics:
chain.invoke({"input": msg})
# 查看当前 memory 状态
print("=== 摘要部分 ===")
print(memory.moving_summary_buffer)
print("\n=== 近期原文消息 ===")
for m in memory.chat_memory.messages:
print(f"[{m.type}] {m.content[:50]}...")
# 继续对话------模型同时拥有摘要上下文 + 近期原文
resp = chain.invoke({"input": "我们产品的目标用户是哪些人?"})
print("\n=== 模型回复 ===")
print(resp["response"])
将摘要记忆与持久化后端结合
ConversationSummaryBufferMemory 同样支持自定义 chat_memory,可以接入前几节的 JSON / SQLite / Redis 后端:
ini
from langchain_openai import ChatOpenAI
from langchain.memory import ConversationSummaryBufferMemory
from langchain.chains import ConversationChain
from langchain_community.chat_message_histories import RedisChatMessageHistory
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 使用 Redis 作为持久化后端
history = RedisChatMessageHistory(
session_id="summary_user_001",
url="redis://localhost:6379/0",
ttl=86400
)
memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=500,
chat_memory=history, # 持久化后端
return_messages=True
)
chain = ConversationChain(llm=llm, memory=memory)
resp = chain.invoke({"input": "我在做一个电商项目,使用 Django + Vue 技术栈"})
print(resp["response"])
# 程序重启后,摘要和近期消息均从 Redis 恢复
📌 注意 :摘要本身也会以消息形式存入
chat_memory,重启后ConversationSummaryBufferMemory会自动加载并继续维护滚动摘要,无需额外处理。
三种记忆策略对比
| 策略 | 保存内容 | Token 上限风险 | 细节保真度 | 推荐场景 |
|---|---|---|---|---|
ConversationBufferMemory |
全量原文 | 高 | 高 | 短对话 / 调试 |
ConversationSummaryMemory |
仅摘要 | 低 | 低 | 超长对话 / 省成本 |
ConversationSummaryBufferMemory |
摘要 + 近期原文 | 中(可控) | 中高 | 生产场景推荐 |
八、进阶:LCEL + RunnableWithMessageHistory(新版推荐写法)
LangChain 0.2.x 开始推荐使用 LCEL(LangChain Expression Language)替代 ConversationChain。新写法更灵活、更易于组合。
旧写法 vs 新写法
| 对比项 | ConversationChain(旧) | LCEL(新) |
|---|---|---|
| 灵活性 | 低,接口固定 | 高,可自由组合 |
| 流式输出 | 不友好 | 原生支持 |
| 维护状态 | 自动 | 显式传入 |
| 官方推荐 | 逐步废弃 | ✅ 推荐 |
完整 LCEL 示例(搭配 Redis)
ini
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_community.chat_message_histories import RedisChatMessageHistory
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# 1. 定义 Prompt,留出历史消息占位符
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个友好的 AI 助手,能记住用户的信息。"),
MessagesPlaceholder(variable_name="history"), # 历史消息注入位置
("human", "{input}"),
])
# 2. 构建基础链
base_chain = prompt | llm
# 3. 定义 history 工厂函数
def get_history(session_id: str) -> RedisChatMessageHistory:
return RedisChatMessageHistory(
session_id=session_id,
url="redis://localhost:6379/0",
ttl=3600
)
# 4. 包装为带记忆的链
chain_with_history = RunnableWithMessageHistory(
base_chain,
get_session_history=get_history,
input_messages_key="input",
history_messages_key="history",
)
# 5. 调用时传入 session_id(支持多用户)
config = {"configurable": {"session_id": "user_001"}}
resp1 = chain_with_history.invoke(
{"input": "我是一名 Python 工程师,在上海工作"},
config=config
)
print(resp1.content)
resp2 = chain_with_history.invoke(
{"input": "我在哪个城市工作?"},
config=config
)
print(resp2.content)
# 输出:你在上海工作。
流式输出(LCEL 的优势)
lua
# 只需将 invoke 改为 stream
for chunk in chain_with_history.stream(
{"input": "用 3 句话介绍一下 Python 的优势"},
config=config
):
print(chunk.content, end="", flush=True)
九、四种方案横向对比
| 方案 | 依赖 | 持久化 | 多用户隔离 | TTL 支持 | 推荐场景 |
|---|---|---|---|---|---|
| 内存(默认) | 无 | ❌ | ❌ | ❌ | 本地调试 |
| JSON 文件 | 无 | ✅ | ✅(文件名) | ❌ | 个人工具 / 脚本 |
| SQLite | 无 | ✅ | ✅(session_id) | ❌ | 小型应用 |
| Redis | Redis 服务 | ✅ | ✅ | ✅ | 生产环境 |
选型建议:
- 本地开发 / 快速验证 → JSON 文件
- 单机部署的小型应用 → SQLite
- 多用户 / 高并发 / 需要自动清理 → Redis
- 追求灵活性和流式响应 → 搭配 LCEL 新写法
十、总结
本文从 LangChain Memory 的基础概念出发,完整覆盖了以下内容:
- JSON 文件:零依赖,适合本地项目
- SQLite:结构化存储,无需额外服务
- Redis:生产首选,支持 TTL 和高并发
- 多轮对话总结 :
ConversationSummaryBufferMemory控制 Token 消耗,兼顾细节与长度 - LCEL 新写法 :
RunnableWithMessageHistory更灵活,原生支持流式输出
延伸阅读
如果这篇文章对你有帮助,欢迎点赞收藏 🙌 有问题欢迎在评论区交流!