logo

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 预算严格"只记最近,旧的丢掉"
ConversationSummaryMemoryLLM 生成摘要相对稳定摘要级别超长对话但不在乎细节"记摘要,省 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_keyRunnableWithMessageHistory 里设置的不一致。返回空的 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 背景

小结

  1. LLM 本身无状态,Memory 的本质是把历史对话塞进每次请求的 Prompt 里——对话越长,每次 API 调用越贵。
  2. 生产环境不要用 ConversationBufferMemory,没有上限,对话一长成本会爆炸。用 ConversationSummaryBufferMemory
  3. RunnableWithMessageHistory + 唯一的 session_id 是管理多用户对话的正确模式,不同用户记忆完全隔离。
  4. 历史存储必须持久化——Redis(高并发)、MongoDB(需要查询)、SQLite(小项目)。内存字典只能开发时用。
  5. 需要更复杂的状态管理(多步骤、断点恢复、分支对话),可以考虑升级到 LangGraph 的 Checkpointer。

下一步LangGraph 进阶 — 用 Checkpointer 实现更强的状态持久化,支持断点恢复和复杂工作流

相关参考RunnableWithMessageHistory 文档 | Chat Message History 持久化方案

LangChain 框架指南
AI Engineer

LangChain 框架指南

LangChain 是构建 LLM 应用的流行框架,提供了链式调用、Agent、RAG 等丰富的功能模块。

LangChain 框架指南Memory 记忆

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 预算严格"只记最近,旧的丢掉"
ConversationSummaryMemoryLLM 生成摘要相对稳定摘要级别超长对话但不在乎细节"记摘要,省 Token"
ConversationSummaryBufferMemory近期全记 + 远期摘要可控近期精确 + 远期摘要生产环境"近精远摘,最均衡"

我的建议:

  • 原型/demo:随便用 ConversationBufferMemory,方便
  • 生产环境客服/助手:ConversationSummaryBufferMemory,控制成本同时不丢重要上下文
  • Token 预算极严格:ConversationTokenBufferMemory

#基础用法:ChatMessageHistory

最基础的存储单元,本质是一个消息列表:

python
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 管理多用户会话。每个用户有独立的历史记录,互不干扰。

python
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(推荐首选,速度快,支持自动过期)

python
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(适合需要查询历史记录的场景)

python
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(本地开发、小型项目)

python
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)

python
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_keyRunnableWithMessageHistory 里设置的不一致。返回空的 ChatMessageHistory(),而不是 None。


#动手练习

实现一个"个人学习助手",能记住用户的技术背景和学习目标,每次对话都基于此给出个性化建议:

python
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 背景

#小结

  1. LLM 本身无状态,Memory 的本质是把历史对话塞进每次请求的 Prompt 里——对话越长,每次 API 调用越贵。
  2. 生产环境不要用 ConversationBufferMemory,没有上限,对话一长成本会爆炸。用 ConversationSummaryBufferMemory
  3. RunnableWithMessageHistory + 唯一的 session_id 是管理多用户对话的正确模式,不同用户记忆完全隔离。
  4. 历史存储必须持久化——Redis(高并发)、MongoDB(需要查询)、SQLite(小项目)。内存字典只能开发时用。
  5. 需要更复杂的状态管理(多步骤、断点恢复、分支对话),可以考虑升级到 LangGraph 的 Checkpointer。

下一步LangGraph 进阶 — 用 Checkpointer 实现更强的状态持久化,支持断点恢复和复杂工作流

相关参考RunnableWithMessageHistory 文档 | Chat Message History 持久化方案

System Design

系统设计必备:核心概念 + 经典案例

快速掌握取舍与设计套路,备战系统设计面试。

进入 System Design →

相关路线图