LangChain Memory解析:让AI记住对话上下文

前言

上三篇文章跟着宋红康老师的教程学了 LangChain 的入门、Model I/O 和 Chains 模块,这次继续学习第四章 Memory(记忆)模块。

你肯定体验过这样的场景:和 ChatGPT 聊天时,它能记住你之前说过的话,甚至记住你上次提到的名字、偏好。但如果你直接调用 OpenAI 的 API,每次请求都是独立的,AI 根本不记得你是谁。这就是为什么我们需要 Memory 模块——让 AI 像人一样,能记住对话的上下文

本文会从"没有 Memory 的痛点"开始,循序渐进地介绍 LangChain 提供的 10 种 Memory 类型,从最简单的全量存储,到智能的摘要压缩,再到专业的实体追踪和向量检索。内容比较长,但每个类型都配有代码示例,力求讲透每种 Memory 的适用场景和技术细节。


目录


一、为什么需要 Memory?

先看个没有 Memory 的例子:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt_template = ChatPromptTemplate.from_messages([
    ("system", "你是一个与人类对话的机器人。"),
    ("human", "问题:{question}")
])

# 第一轮对话
messages = prompt_template.invoke({"question": "北京有什么好吃的?"})
response = llm.invoke(messages)
print(response.content)

# 手动添加历史消息到 prompt
from langchain_core.messages import AIMessage
prompt_template.messages.append(AIMessage(content=response.content))

# 第二轮对话
messages = prompt_template.invoke({"question": "上海呢?"})
response = llm.invoke(messages)
print(response.content)

痛点显而易见:

  1. 手动管理历史消息:每次对话后都要 append(AIMessage(...)),非常繁琐
  2. 无限增长的上下文:对话越长,prompt 越长,Token 消耗越大,成本越高
  3. 缺乏灵活性:想要滑动窗口、摘要压缩、实体追踪?自己写代码吧

这就是 Memory 模块要解决的问题:自动管理对话历史,提供灵活的存储策略,平衡上下文完整性和成本效率。


二、基础 Memory 模块

2.1 ChatMessageHistory:最底层的消息存储

ChatMessageHistory 是最基础的消息存储容器,就像一个 list,用来存放用户消息和 AI 消息:

from langchain.memory import ChatMessageHistory

history = ChatMessageHistory()
history.add_user_message("你好")
history.add_ai_message("你好!有什么我可以帮助你的吗?")

print(history.messages)
# 输出:
# [HumanMessage(content='你好'),
#  AIMessage(content='你好!有什么我可以帮助你的吗?')]

核心方法:

  • add_user_message(text): 添加用户消息
  • add_ai_message(text): 添加 AI 消息
  • messages: 返回所有消息列表(HumanMessageAIMessage 对象)

适用场景: 当你需要完全自定义消息管理逻辑时使用,但通常我们会用更高级的封装。


2.2 ConversationBufferMemory:完整对话记忆

ConversationBufferMemory 是最常用的 Memory,它会完整保存所有对话历史

示例 1:字符串格式输出
from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory()
memory.save_context(inputs={"input": "你好"}, outputs={"output": "你好!有什么我可以帮助你的吗?"})
memory.save_context(inputs={"input": "我想了解 LangChain"}, outputs={"output": "LangChain 是一个..."})

print(memory.load_memory_variables({}))
# 输出:{'history': 'Human: 你好\nAI: 你好!有什么我可以帮助你的吗?\nHuman: 我想了解 LangChain\nAI: LangChain 是一个...'}

关键 API:

  • save_context(inputs, outputs): 保存一轮对话
  • load_memory_variables({}): 加载记忆内容,默认返回字符串格式
示例 2:消息列表格式输出
memory = ConversationBufferMemory(return_messages=True)
memory.save_context({"input": "你好"}, {"output": "你好!"})

print(memory.load_memory_variables({}))
# 输出:{'history': [HumanMessage(content='你好'), AIMessage(content='你好!')]}

参数说明:

  • return_messages=True: 返回消息对象列表,适合与 ChatPromptTemplate 配合
示例 3:与 LLMChain 集成(记住名字)
from langchain.chains import LLMChain
from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate(
    input_variables=['history', 'question'],
    template="以下是历史对话:\n{history}\n问题:{question}"
)

memory = ConversationBufferMemory()
chain = LLMChain(llm=llm, prompt=prompt, memory=memory)

# 第一轮:告诉名字
response1 = chain.invoke({"question": "我叫小明"})
print(response1)  # 输出:你好,小明!

# 第二轮:询问名字
response2 = chain.invoke({"question": "我叫什么名字?"})
print(response2)  # 输出:你叫小明

核心机制: Memory 会在每次调用 chain.invoke() 时:

  1. 调用前:通过 load_memory_variables() 加载历史对话到 {history} 变量
  2. 调用后:通过 save_context() 自动保存本轮对话
示例 4:自定义 memory_key
memory = ConversationBufferMemory(memory_key="chat_history")

prompt = PromptTemplate(
    input_variables=['chat_history', 'question'],
    template="历史:\n{chat_history}\n问题:{question}"
)
# 注意:prompt 中的变量名要和 memory_key 对应
示例 5:与 ChatPromptTemplate 集成
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个与人类对话的机器人。"),
    MessagesPlaceholder(variable_name='history'),  # 占位符,用于插入历史消息
    ("human", "问题:{question}")
])

memory = ConversationBufferMemory(memory_key='history', return_messages=True)
chain = LLMChain(llm=llm, prompt=prompt, memory=memory)

response = chain.invoke({"question": "你好"})

重要提示: 使用 ChatPromptTemplate 时:

  • 必须设置 return_messages=True
  • 使用 MessagesPlaceholder(variable_name='history') 插入历史消息
  • memory_key 要和 MessagesPlaceholdervariable_name 对应

PromptTemplate vs ChatPromptTemplate 在 Memory 中的差异:

对比维度 PromptTemplate ChatPromptTemplate
历史消息格式 字符串格式 "Human: ...\nAI: ..." 消息对象列表 [HumanMessage, AIMessage]
Memory 参数 return_messages=False(默认) return_messages=True(必须)
占位符 直接用 {history} 需要 MessagesPlaceholder(variable_name='history')
消息保存时机 调用后保存 调用后保存

2.3 ConversationChain:简化版对话链

ConversationChain 是一个简化的封装,自动创建 ConversationBufferMemoryLLMChain(来自 02-基础Memory模块的使用.ipynb):

示例 1:使用自定义 prompt
from langchain.chains import ConversationChain

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个与人类对话的机器人。"),
    MessagesPlaceholder(variable_name='history'),
    ("human", "{input}")
])

chain = ConversationChain(llm=llm, prompt=prompt)
response = chain.invoke({"input": "你好"})
示例 2:使用默认 prompt
chain = ConversationChain(llm=llm)
# 默认 prompt 包含 {input} 和 {history} 两个变量

response = chain.invoke({"input": "你好"})

适用场景: 对话轮次较少、依赖完整上下文的场景(如简单的聊天机器人)。但注意成本问题:对话越长,每次调用的 Token 消耗越大。


三、限制对话长度的 Memory

3.1 ConversationBufferWindowMemory:滑动窗口记忆

问题引入: ConversationBufferMemory 会无限增长历史消息,导致:

  1. 内存占用大
  2. Token 消耗高(每次请求都要发送全部历史)
  3. 容易超出模型的上下文限制(如 GPT-4 的 8k tokens)

解决方案: ConversationBufferWindowMemory 只保留最近的 K 轮对话:

示例 1:窗口大小 k=2(字符串格式)
from langchain.memory import ConversationBufferWindowMemory

memory = ConversationBufferWindowMemory(k=2)
memory.save_context({"input": "第1轮"}, {"output": "回复1"})
memory.save_context({"input": "第2轮"}, {"output": "回复2"})
memory.save_context({"input": "第3轮"}, {"output": "回复3"})

print(memory.load_memory_variables({}))
# 输出:{'history': 'Human: 第2轮\nAI: 回复2\nHuman: 第3轮\nAI: 回复3'}
# 第1轮被丢弃了!
示例 2:集成到 LLM(客服场景)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是客服助手。"),
    MessagesPlaceholder(variable_name='history'),
    ("human", "{question}")
])

memory = ConversationBufferWindowMemory(k=2, memory_key='history', return_messages=True)
chain = LLMChain(llm=llm, prompt=prompt, memory=memory)

chain.invoke({"question": "我叫孙小空"})
chain.invoke({"question": "我有一个师弟"})
chain.invoke({"question": "我叫什么?"})
# 输出:抱歉,我并不知道你的名字
# 因为 k=2,只记住了后两轮,第一轮的名字被忘记了

适用场景: 需要保持对话连贯性,但不需要完整历史的场景(如客服、简单聊天)。

取舍: 牺牲了长期记忆,换取了成本控制和性能优化。


3.2 ConversationTokenBufferMemory:令牌限制记忆

更精准的控制: 按 Token 数量而非轮次来限制记忆:

示例 1:max_token_limit=10
from langchain.memory import ConversationTokenBufferMemory

memory = ConversationTokenBufferMemory(llm=llm, max_token_limit=10)
memory.save_context({"input": "你好,你是谁?"}, {"output": "我是 AI 助手。"})

print(memory.load_memory_variables({}))
# 输出:{'history': ''}
# 因为这一轮对话的 Token 数超过 10,被完全丢弃
示例 2:max_token_limit=30
memory = ConversationTokenBufferMemory(llm=llm, max_token_limit=30)
memory.save_context({"input": "你好"}, {"output": "你好!"})
memory.save_context({"input": "介绍一下你自己"}, {"output": "我是一个 AI 助手..."})

print(memory.load_memory_variables({}))
# 输出:保留了部分最近的对话,Token 总数不超过 30

工作原理:

  1. 每次 save_context() 时,计算当前所有消息的 Token 总数
  2. 如果超过 max_token_limit,从最早的消息开始删除,直到满足限制

适用场景: 严格的 Token 预算控制(如按 Token 收费的场景)。

注意事项: 需要传入 llm 参数,用于计算 Token 数量。


四、智能压缩的 Memory

4.1 ConversationSummaryMemory:摘要式记忆

更聪明的压缩: 用 LLM 生成对话摘要,而不是简单地丢弃历史:

示例 1:从空历史开始
from langchain.memory import ConversationSummaryMemory

memory = ConversationSummaryMemory(llm=llm)
memory.save_context({"input": "你好"}, {"output": "怎么了"})

print(memory.load_memory_variables({}))
# 输出:{'history': 'The human greets the AI in Chinese, saying "hello". The AI responds by asking "what\'s up?" in Chinese.'}
# LLM 自动生成了英文摘要!
示例 2:从已有历史初始化
from langchain.memory import ChatMessageHistory

history = ChatMessageHistory()
history.add_user_message("你好,你是谁?")
history.add_ai_message("我是 AI 助手。")

memory = ConversationSummaryMemory.from_messages(
    llm=llm,
    chat_memory=history,
    return_messages=True
)

print(memory.buffer)
# 输出:对话的摘要(由 LLM 生成)

核心机制:

  1. 每次 save_context() 时,LLM 会阅读当前摘要 + 新对话,生成新的摘要
  2. from_messages() 会保留原始消息到 chat_memory,方便后续重新生成摘要

适用场景: 需要长对话记忆,但希望压缩 Token 消耗的场景(如长期客服、顾问)。

取舍: 摘要可能丢失细节,但保留了关键信息;每次生成摘要会额外消耗 Token。


4.2 ConversationSummaryBufferMemory:混合式记忆

最佳实践: 结合摘要和完整消息,兼顾细节和压缩:

示例 1:max_token_limit=40
from langchain.memory import ConversationSummaryBufferMemory

memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=40,
    return_messages=True
)

memory.save_context({"input": "你好"}, {"output": "你好!"})
memory.save_context({"input": "介绍一下你自己"}, {"output": "我是 AI 助手..."})
memory.save_context({"input": "你能做什么?"}, {"output": "我可以回答问题..."})

print(memory.load_memory_variables({}))
# 输出:
# {'history': [
#     SystemMessage(content='对前几轮对话的摘要...'),  # 早期对话的摘要
#     AIMessage(content='我可以回答问题...'),         # 最近的完整消息
#     HumanMessage(content='你能做什么?')
# ]}
示例 2:对比 max_token_limit=100
memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=100,
    return_messages=True
)

# 同样的对话
memory.save_context({"input": "你好"}, {"output": "你好!"})
memory.save_context({"input": "介绍一下你自己"}, {"output": "我是 AI 助手..."})
memory.save_context({"input": "你能做什么?"}, {"output": "我可以回答问题..."})

print(memory.load_memory_variables({}))
# 输出:所有消息都是完整的,没有生成摘要
示例 3:电商客服场景
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是电商客服助手。"),
    MessagesPlaceholder(variable_name='history'),
    ("human", "{question}")
])

memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=100,
    memory_key='history',
    return_messages=True
)

chain = LLMChain(llm=llm, prompt=prompt, memory=memory)

chain.invoke({"question": "我的订单号是 12345"})
chain.invoke({"question": "什么时候发货?"})
chain.invoke({"question": "可以退货吗?"})
chain.invoke({"question": "我的订单号是多少来着?"})
# AI 能回答:你的订单号是 12345
# 因为即使前面的对话被压缩成摘要,订单号这个关键信息会被保留

工作原理:

  1. 保留最近几轮完整消息(Token 数在 max_token_limit 内)
  2. 将更早的消息压缩成摘要(通过 LLM 生成)
  3. 最终返回:摘要(SystemMessage)+ 最近的完整消息

适用场景: 最推荐的 Memory 类型!平衡了细节保留和成本控制,适用于大多数生产环境。


五、专业场景的 Memory

5.1 ConversationEntityMemory:实体追踪记忆

场景引入: 在电商、医疗等场景中,我们更关心实体(人名、产品、症状)及其属性,而不是完整对话。

示例对比:

ConversationSummaryMemory:

摘要:患者主诉头痛和高血压,有青霉素过敏史...

ConversationEntityMemory:

{
  "症状": "头痛",
  "血压": "140/90",
  "过敏药物": "青霉素"
}
示例:超级英雄对话
from langchain.memory import ConversationEntityMemory

memory = ConversationEntityMemory(llm=llm)
chain = ConversationChain(llm=llm, memory=memory, verbose=True)

chain.invoke("我叫蜘蛛侠。我的好朋友包括钢铁侠、美国队长和绿巨人。")
chain.invoke("我住在纽约。")

# 查看实体存储
print(memory.entity_store.store)
# 输出:
# {
#     '蜘蛛侠': '蜘蛛侠的好朋友包括钢铁侠、美国队长和绿巨人。蜘蛛侠住在纽约。',
#     '纽约': '蜘蛛侠住在纽约。',
#     '钢铁侠': '钢铁侠是蜘蛛侠的好朋友。',
#     '美国队长': '美国队长是蜘蛛侠的好朋友。',
#     '绿巨人': '绿巨人是蜘蛛侠的好朋友。'
# }

工作原理:

  1. LLM 自动提取对话中的实体(人名、地点、物品)
  2. 为每个实体维护一个描述,记录相关信息
  3. 后续对话中,如果提到某个实体,会加载该实体的描述到上下文

适用场景:

  • 电商:追踪产品名称、订单号、用户偏好
  • 医疗:追踪症状、药物、过敏史
  • CRM:追踪客户姓名、公司、需求

优势:

  • 压缩了无关信息(如寒暄),保留了关键实体
  • 适合高风险领域(医疗、法律),结构化数据更易审查

5.2 ConversationKGMemory:知识图谱记忆

更进一步: 不仅提取实体,还提取实体之间的关系(来自 03-其他Memory模块的使用.ipynb):

from langchain.memory import ConversationKGMemory

memory = ConversationKGMemory(llm=llm)
memory.save_context({"input": "向山姆问好"}, {"output": "山姆是谁"})
memory.save_context({"input": "山姆是我的朋友"}, {"output": "好的"})

# 查看知识图谱
print(memory.kg.get_triples())
# 输出:
# [KnowledgeTriple(subject='山姆', predicate='是', object_='我的朋友')]

工作原理:

  1. LLM 提取三元组:(头实体, 关系, 尾实体)
  2. 构建知识图谱,存储实体和关系
  3. 查询时,通过图谱推理相关信息

适用场景:

  • 社交网络:追踪人际关系(朋友、同事、家人)
  • 企业 CRM:追踪公司间的合作关系、竞争关系
  • 智能助手:推理用户的社交圈和偏好

示例场景: “山姆是我的朋友,山姆喜欢打篮球” → 推理:我的朋友喜欢打篮球 → 推荐:篮球相关内容


5.3 VectorStoreRetrieverMemory:向量检索记忆

终极武器: 用向量数据库存储对话,通过语义相似度检索历史(来自 03-其他Memory模块的使用.ipynb):

from langchain.memory import VectorStoreRetrieverMemory
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

# 初始化向量存储
embeddings_model = OpenAIEmbeddings()
vectorstore = FAISS.from_texts(
    ["我最喜欢的食物是披萨", "我住在北京"],
    embeddings_model
)

# 创建检索器
retriever = vectorstore.as_retriever(search_kwargs=dict(k=1))

# 创建 Memory
memory = VectorStoreRetrieverMemory(retriever=retriever)

# 语义检索
result = memory.load_memory_variables({"prompt": "我最喜欢的食物是"})
print(result)
# 输出:{'history': 'Human: 我最喜欢的食物是披萨'}
# 即使查询不是完全匹配,也能通过语义相似度找到相关历史

工作原理:

  1. 将每轮对话嵌入成向量,存储到向量数据库(FAISS、Pinecone 等)
  2. 查询时,将当前问题也嵌入成向量
  3. 通过余弦相似度检索最相关的 K 条历史

适用场景:

  • 超长对话:几百轮、几千轮对话,传统 Memory 无法处理
  • 语义搜索:不关心时间顺序,关心语义相关性(如"上次讨论的那个 bug")
  • 多轮复杂咨询:法律、医疗、技术支持,需要检索历史相似案例

优势:

  • 不受对话长度限制(向量数据库可以存储海量数据)
  • 语义检索比关键词匹配更智能

劣势:

  • 成本较高(需要向量化模型、向量数据库)
  • 检索结果可能不按时间顺序,需要额外处理

六、Memory 选型指南

根据场景选择合适的 Memory 类型:

Memory 类型 适用场景 Token 效率 实现复杂度 关键参数
ConversationBufferMemory 短对话、需要完整上下文(如简单聊天) 简单 return_messages, memory_key
ConversationBufferWindowMemory 中等长度对话、只需最近上下文(如客服) 简单 k(窗口大小)
ConversationTokenBufferMemory 严格 Token 预算控制 中等 max_token_limit
ConversationSummaryMemory 长对话、关注关键点而非细节 中等 llm
ConversationSummaryBufferMemory 推荐! 平衡细节和压缩(大多数生产环境) 很高 中等 max_token_limit, llm
ConversationEntityMemory 追踪特定实体(人名、产品、症状) 复杂 llm
ConversationKGMemory 复杂关系推理(社交网络、知识图谱) 复杂 llm
VectorStoreRetrieverMemory 超长对话、语义搜索(技术支持、法律咨询) 很高 复杂 retriever, k

选型建议:

  1. 快速原型:先用 ConversationBufferMemory,简单直接
  2. 生产环境:推荐 ConversationSummaryBufferMemory,性价比最高
  3. 成本敏感:用 ConversationBufferWindowMemoryConversationTokenBufferMemory
  4. 专业场景
    • 电商/医疗 → ConversationEntityMemory
    • 社交/CRM → ConversationKGMemory
    • 超长历史/语义检索 → VectorStoreRetrieverMemory

七、总结

Memory 模块是 LangChain 的核心功能之一,让 AI 能够像人一样记住对话上下文。本文介绍了 10 种 Memory 类型,从最基础的完整存储,到智能的摘要压缩,再到专业的实体追踪和向量检索。

核心要点:

  1. 基础 Memory

    • ChatMessageHistory:最底层的消息容器
    • ConversationBufferMemory:完整保存所有对话,简单但成本高
    • ConversationChain:简化版封装,快速原型开发
  2. 限制长度

    • ConversationBufferWindowMemory:滑动窗口,按轮次限制
    • ConversationTokenBufferMemory:按 Token 数量精准控制
  3. 智能压缩

    • ConversationSummaryMemory:LLM 生成摘要,保留关键信息
    • ConversationSummaryBufferMemory:混合式,最推荐的生产方案
  4. 专业场景

    • ConversationEntityMemory:实体追踪,适合电商、医疗
    • ConversationKGMemory:知识图谱,适合关系推理
    • VectorStoreRetrieverMemory:向量检索,适合超长对话

实战建议:

  • 开发阶段:用 ConversationBufferMemory 快速验证功能
  • 上线前:切换到 ConversationSummaryBufferMemory,平衡成本和效果
  • 特殊场景:根据业务需求选择专业 Memory(实体/知识图谱/向量检索)

注意事项:

  1. PromptTemplate vs ChatPromptTemplate:后者需要 return_messages=TrueMessagesPlaceholder
  2. memory_key 对应:确保 Memory 的 memory_key 和 Prompt 的变量名一致
  3. Token 成本:摘要式 Memory 会额外调用 LLM,需考虑成本
  4. 数据持久化:本文的示例都是内存存储,生产环境需要持久化到数据库或文件

下一篇文章我们继续学习 LangChain 的其他模块,敬请期待!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐