LangChain Memory:让 AI 记住上下文
先说一件让我学到教训的事。
有次给一个客户做客服机器人,用的是 ConversationBufferMemory——最简单的那种,把全部对话历史都存下来,每次调用都发给模型。测试时没问题,对话十来轮,Token 消耗可以接受。
上线三周后客户来找我说"API 费用这个月涨了 8 倍"。我去看日志,有个用户跟机器人聊了 180 轮,每次调用都把前 179 轮全发给模型。到最后一轮,单次调用发了大约 40,000 个 Token。
所以讲 Memory 之前必须先说这件事:Memory 不是免费的。历史消息越长,每次 API 调用越贵。这是选 Memory 策略时最重要的约束条件。
LLM 为什么没有记忆
每次调用 LLM API,都是一次全新的请求。模型不记得上次说了什么,因为它根本没有"上次"的概念——每个 API 调用都是独立的。
Memory 系统的本质很朴素:把历史对话保存下来,每次调用时把它塞进 Prompt 里,让模型"看起来"记得你。
第 1 轮:
发给模型:[User: 我叫小明,是前端工程师]
模型回:你好小明!
第 2 轮(有 Memory):
发给模型:
[历史] User: 我叫小明,是前端工程师
[历史] AI: 你好小明!
[当前] User: 我适合学 Vue 还是 React?
→ AI 知道你叫小明、知道你是前端,能给个性化答案
关键理解:Token 消耗 = 系统 Prompt + 历史消息 + 新消息 + 模型回复。对话每多一轮,每次请求的 Token 就多那么多。这不是线性增长,是每次调用都带着所有历史。
四种 Memory 策略
不同场景对历史记忆的需求不同:
| 策略 | 保存方式 | Token 消耗 | 信息完整度 | 适合场景 | 一句话 |
|---|---|---|---|---|---|
ConversationBufferMemory | 全部历史 | 随轮次线性增长 | 100% | 测试/原型,对话 < 20 轮 | "全记,只适合测试" |
ConversationTokenBufferMemory | 最近 N Token | 固定上限 | 仅最近内容 | Token 预算严格 | "只记最近,旧的丢掉" |
ConversationSummaryMemory | LLM 生成摘要 | 相对稳定 | 摘要级别 | 超长对话但不在乎细节 | "记摘要,省 Token" |
ConversationSummaryBufferMemory | 近期全记 + 远期摘要 | 可控 | 近期精确 + 远期摘要 | 生产环境 | "近精远摘,最均衡" |
我的建议:
- 原型/demo:随便用
ConversationBufferMemory,方便 - 生产环境客服/助手:
ConversationSummaryBufferMemory,控制成本同时不丢重要上下文 - Token 预算极严格:
ConversationTokenBufferMemory
基础用法:ChatMessageHistory
最基础的存储单元,本质是一个消息列表:
from langchain_community.chat_message_histories import ChatMessageHistory
history = ChatMessageHistory()
history.add_user_message("你好,我是小明,前端工程师")
history.add_ai_message("你好小明!很高兴认识你。")
history.add_user_message("我在学 React,有什么建议?")
for msg in history.messages:
print(f"[{msg.type}]: {msg.content}")
# [human]: 你好,我是小明,前端工程师
# [ai]: 你好小明!很高兴认识你。
# [human]: 我在学 React,有什么建议?
history.clear() # 清空
在 Chain 中集成 Memory:现代写法
现代 LangChain(0.2+)推荐用 RunnableWithMessageHistory 包装 Chain,通过 session_id 管理多用户会话。每个用户有独立的历史记录,互不干扰。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o-mini")
# Prompt 里必须有历史消息占位符
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个友好的技术助手,记住用户的背景信息,给出个性化建议。"),
MessagesPlaceholder(variable_name="chat_history"),
("user", "{input}"),
])
chain = prompt | llm | StrOutputParser()
# 开发环境用内存存储(重启就没了)
store: dict = {}
def get_session_history(session_id: str) -> ChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
chain_with_memory = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
)
# 用 session_id 区分不同用户
user_a = {"configurable": {"session_id": "user_123"}}
user_b = {"configurable": {"session_id": "user_456"}}
# 用户 A 的对话
print(chain_with_memory.invoke({"input": "我叫小明,喜欢 Vue"}, config=user_a))
print(chain_with_memory.invoke({"input": "推荐一个适合我的框架"}, config=user_a))
# AI 会记得他叫小明、喜欢 Vue,给出基于此的推荐
# 用户 B 完全隔离
print(chain_with_memory.invoke({"input": "我是 Java 后端工程师"}, config=user_b))
生产环境:存储必须持久化
开发时用内存字典很方便,但服务一重启历史全没了。生产环境必须把历史存到数据库。
好消息是,只需要替换 get_session_history 函数,Chain 本身完全不用改。
Redis(推荐首选,速度快,支持自动过期)
from langchain_community.chat_message_histories import RedisChatMessageHistory
def get_session_history(session_id: str) -> RedisChatMessageHistory:
return RedisChatMessageHistory(
session_id=session_id,
url="redis://localhost:6379/0",
ttl=86400, # 24 小时自动过期,不用手动清理
key_prefix="chat:", # Redis key 前缀,方便管理
)
MongoDB(适合需要查询历史记录的场景)
from langchain_mongodb.chat_message_histories import MongoDBChatMessageHistory
def get_session_history(session_id: str):
return MongoDBChatMessageHistory(
connection_string="mongodb://localhost:27017/",
session_id=session_id,
database_name="my_app",
collection_name="chat_history",
)
SQLite(本地开发、小型项目)
from langchain_community.chat_message_histories import SQLChatMessageHistory
def get_session_history(session_id: str):
return SQLChatMessageHistory(
session_id=session_id,
connection_string="sqlite:///chat_history.db"
)
选型:高并发生产用 Redis,需要查询历史记录用 MongoDB,本地小项目用 SQLite。
摘要记忆:对话长了怎么办
回到开头那个故事——怎么避免 180 轮对话把 Token 撑爆?
ConversationSummaryBufferMemory 的思路是:最近的对话原样保留(细节重要),远处的对话自动压缩成摘要(节省 Token)。
from langchain.memory import ConversationSummaryBufferMemory
from langchain_openai import ChatOpenAI
# 最多保留最近 500 Token 的原始历史
# 超出的部分自动用 LLM 压缩成摘要
memory = ConversationSummaryBufferMemory(
llm=ChatOpenAI(model="gpt-4o-mini"),
max_token_limit=500,
return_messages=True,
)
memory.save_context(
{"input": "我在做一个电商项目,用 FastAPI + React"},
{"output": "好的,这是个很好的技术栈组合!"}
)
print(memory.load_memory_variables({}))
# {
# "history": [
# SystemMessage(content="摘要:用户正在做 FastAPI+React 电商项目..."),
# # 最近的几轮原始消息
# ]
# }
这样不管对话多长,每次请求的 Token 都在可控范围内。代价是远处的对话只保留摘要,细节会丢失。如果某些重要信息(比如用户名、项目背景)你不想让摘要压缩掉,最好在 System Prompt 里再强调一遍。
常见坑
Token 费用越来越高:大概率是用了 BufferMemory 没有上限。检查一下,换成 TokenBufferMemory 或 SummaryBufferMemory。
多用户记忆串了:session_id 设错了,两个用户共用了同一个历史。确保每个用户有唯一的 session_id,推荐用 UUID。
重启后历史消失:用了内存字典存历史。接数据库,早接早省心。
AI "忘记"了重要信息:摘要记忆压缩时把某些细节丢了。把关键信息在 System Prompt 里额外强调,不要完全依赖 Memory 来保留。
首次调用报占位符错误:get_session_history 返回了 None,或者 Prompt 里的 history_messages_key 和 RunnableWithMessageHistory 里设置的不一致。返回空的 ChatMessageHistory(),而不是 None。
动手练习
实现一个"个人学习助手",能记住用户的技术背景和学习目标,每次对话都基于此给出个性化建议:
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
# TODO 1:创建 Prompt,System 里说明助手会记住用户背景,给个性化技术建议
# TODO 2:创建 get_session_history 函数
# TODO 3:用 RunnableWithMessageHistory 包装
# 测试:
# 第 1 句:告诉助手你的背景("我是 Java 后端工程师,想学 AI 开发")
# 第 2 句:问一个技术问题
# 验证:AI 的回答是否考虑了你的 Java 背景
小结
- LLM 本身无状态,Memory 的本质是把历史对话塞进每次请求的 Prompt 里——对话越长,每次 API 调用越贵。
- 生产环境不要用
ConversationBufferMemory,没有上限,对话一长成本会爆炸。用ConversationSummaryBufferMemory。 RunnableWithMessageHistory+ 唯一的session_id是管理多用户对话的正确模式,不同用户记忆完全隔离。- 历史存储必须持久化——Redis(高并发)、MongoDB(需要查询)、SQLite(小项目)。内存字典只能开发时用。
- 需要更复杂的状态管理(多步骤、断点恢复、分支对话),可以考虑升级到 LangGraph 的 Checkpointer。
下一步:LangGraph 进阶 — 用 Checkpointer 实现更强的状态持久化,支持断点恢复和复杂工作流
相关参考:RunnableWithMessageHistory 文档 | Chat Message History 持久化方案