在这里插入图片描述


课程导读 & 学习目标

在前面的课程中,我们已经系统掌握了LangChain的记忆体系。第12课我们学习了新版记忆架构的核心组件——BaseChatMessageHistoryRunnableWithMessageHistory。但有一个关键问题始终没有深入回答:在RunnableWithMessageHistory这个新架构中,对话历史的存储基座到底是什么?谁在真正“装”这些消息?

答案就是BaseChatMessageHistory的具体实现——InMemoryChatMessageHistory。然而,如果你查阅旧版LangChain的教程或代码,会发现一个频繁出现的名字:ConversationBufferMemory。它曾是LangChain早期版本中最核心的记忆组件,承载了无数学员的第一个带记忆对话应用。

今天这节课的核心任务,就是系统梳理ConversationBufferMemory的演进历程,同时更深入理解新版架构中InMemoryChatMessageHistory作为其替代者的定位。可以说,ConversationBufferMemory已经完成了它的历史使命,但理解它的设计思路,可以帮助你更顺畅地过渡到新版架构,也为阅读存量代码打下基础。

通过类比生活中“备忘录”与“对话记录本”的区别,我们先理解一个朴素的设计:聊天历史本质上就是按照时间顺序不断追加消息的一个“篮子”。

前置知识与环境准备

1.1 环境沿用

继续使用前几课的langchain_course虚拟环境(Python 3.10+)。如果你是从头开始,请参考第1课的虚拟环境创建方式。

1.2 依赖安装

# 激活虚拟环境
source venv/bin/activate  # Mac/Linux
# venv\Scripts\activate   # Windows

# 基础依赖
pip install langchain==0.3.7 langchain-core==0.3.21 langchain-openai==0.2.8 python-dotenv==1.0.1

# LangChain Classic 包(用于查看旧版 API 对比)
pip install langchain-classic

# 验证安装
python -c "from langchain_core.chat_history import InMemoryChatMessageHistory; print('✓ 新版记忆组件导入成功')"

1.3 API密钥与模型选择

沿用前几课的.env配置(智谱GLM-4或Ollama本地模型)。本节课的记忆演示涉及多轮对话交互,建议使用本地Ollama模型以零成本体验完整功能。

1.4 上节课回顾与本课定位

在第12课中,我们详细拆解了新版记忆架构的演化逻辑,介绍了BaseChatMessageHistory接口和RunnableWithMessageHistory包装器的工作原理。本节课则专注于一件事:深入理解InMemoryChatMessageHistory(也就是旧版ConversationBufferMemory在新时代的继承者)的设计哲学与工程实践。如果你在学习新版API时看到“内存消息历史”这类术语,它就是Buffer Memory在新版本中的最佳别名。

核心概念深度拆解

2.1 为什么需要“记忆基座”——从“一次性工人”到“有记忆的伙伴”

大模型本身是一种“金鱼”,如果你连续问同一个人的同一个名字,第二轮模型就会直接忘记第一轮讲过的信息。记忆系统回答两个根本问题:存储什么和如何召回。一个最简单的解决方案,就是把每次对话的原句完整保存下来,按照时间顺序排列。下次提问时,把这整本“备忘录”一股脑塞给模型,让它自己从中找出之前说过什么。这就是**Buffer Memory(缓冲内存)**的核心思想:它是一种“对话抄录员”,一字不差地把所有对话记录下来

基于这种需求,一张最简单的记忆组件表就浮现出来。早期LangChain的ConversationBufferMemory核心功能无外乎是存储和读取:提供save_context()把当前的用户输入和AI输出保存起来;提供load_memory_variables()把历史记录的纯文本版本返回给调用方;而clear()则清空所有记录。

补充词汇:ConversationBufferWindowMemory——对话缓冲窗口内存。它与ConversationBufferMemory类似,但只保留最近的K轮对话。一“轮”包括用户的输入和AI的即时回复。这对于控制Token消耗非常有用。ConversationSummaryMemory则在一个更高维度上用LLM自动生成摘要来总结对话,将长对话压缩成几行关键信息。

2.2 记忆体系的构成与分层路径

LangChain的对话存储经历了几个层次:

  • 你看到的业务层:ConversationBufferMemoryConversationSummaryMemory。这些是上层API提供的对话记忆组合方案。
  • 底层的存储基座:BaseChatMessageHistory与它的各种实现(InMemoryChatMessageHistoryRedisChatMessageHistorySQLChatMessageHistory等)。

新旧版本在存储逻辑上有根本差异。旧版ConversationBufferMemory内存中维护的是一个纯文本字符串buffer。在memory.load_memory_variables({})时,返回的是如下格式:

Human: 我叫李明
AI: 你好李明,很高兴认识你
Human: 我是软件工程师
AI: 太棒了,我也喜欢软件领域

这种把消息类型(人/AI)用字符前缀硬编码的方式,与模型API期待的角色完全割裂。新版BaseChatMessageHistory``messages列表存储的是HumanMessageAIMessage等真正的消息对象,更加符合现代聊天模型的训练数据格式。

2.3 ConversationBufferMemory的源码结构(历史版本)

ConversationBufferMemory继承自BaseMemory抽象基类。它的核心存储是一个名为buffer的字符串或消息列表。当return_messages=False(默认)时,buffer是纯文本;当return_messages=True时,buffer是消息对象列表。这就是ConversationBufferMemory的两种人脸——String模式和Chat模式。

实际上,ConversationBufferMemory的“两个面孔”:return_messages=False时,它会帮你把所有消息拼接成字符串,很容易与Completion模型(即text-davinci-003这种非Chat模型)一起调用。ConversationBufferMemory(return_messages=True)后,buffer存储的是消息对象列表,与聊天模型的输入结构天然对齐。旧版的这个设计让很多初学者理解起来非常吃力,因为明明是同一类记忆组件,却要引入不同的返回格式。

2.4 Deprecated时间线——为什么它被标记废弃

从v0.1.17开始,LangChain官方在ConversationBufferMemory的代码中引入废弃警告(DeprecationWarning):LangChainDeprecationWarning: The class ConversationBufferMemory was deprecated in LangChain 0.3.1 and will be removed in 1.0

LangChain官方在v0.3.1中正式将ConversationBufferMemory标为废弃。原因很清晰:

  • 延续了旧版BaseMemory的错误拼凑:旧版BaseMemory把消息历史和Memory的业务耦合在一个接口中。新版通过BaseChatMessageHistoryRunnableWithMessageHistory完全解耦。
  • 函数语义令人困惑的buffer机制return_messages的存在又造成了学习成本翻倍。
  • 无法支持流式原生特性:新版Runnable体系可以轻松用stream,旧版链被强绑定。

注意:新版LangChain官方API参考中已经完整列出了从ConversationBufferMemory迁移到RunnableWithMessageHistory的详细示例。

2.5 新版存储基座的角色——InMemoryChatMessageHistory

新版推荐的存储基座是继承了BaseChatMessageHistory接口的InMemoryChatMessageHistory。它的内部存储是一个消息列表,提供add_messagesclear和只读属性messages。它的设计思路极其干净。

from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage

# 创建实例
history = InMemoryChatMessageHistory()

# 添加消息(推荐批量添加)
history.add_messages([
    HumanMessage(content="我叫李明"),
    AIMessage(content="你好,李明!")
])

# 查看所有历史
for msg in history.messages:
    print(f"{msg.type}: {msg.content}")

InMemoryChatMessageHistoryRunnableWithMessageHistory配合的完整流程在第12课中已经详细展示过。它扮演的角色就是把记忆存储与记忆管理完全解耦。这种设计的最大优势是:将来你可以轻松替换存储后端(文件、Redis、数据库中),业务链核心代码零修改。

底层运行原理剖析

3.1 记忆写入与读取的完整数据流

当你用ConversationBufferMemory写一个历史记录时,典型的数据流动如下:

  1. 用户输入:{"input": "我叫李明"}
  2. 调用的是memory.save_context({"input": r"human的原始输入字段"}, {"output": "模型返回AIMessage"})
  3. 默认拼接方式:将"Human: 用户输入"和"AI: 模型输出"拼接在一起,添加一个换行符分隔。
  4. 存入self.buffer(如果是返回return_messages=False)或存入self.chat_memory.messages(如果是return_messages=True模式)。

这条链路到了新版反而简洁很多:RunnableWithMessageHistory会自动从invoke执行后,将本次的HumanMessageAIMessage保存到BaseChatMessageHistory实例中,不需要手动调用任何save_context

3.2 return_messages 的本质剖析

ConversationBufferMemoryreturn_messages参数,它控制的是load_memory_variables({})返回值的格式。如果return_messages=True,返回:{"history": [HumanMessage(...), AIMessage(...)]}。主要用于聊天模型的输入;如果为False(默认),返回:{"history": "Human: 我叫李明\nAI: 你好!"}。这主要是为旧的文本补全模型设计的过渡方案。

新版架构中,InMemoryChatMessageHistorymessages属性永远返回消息对象列表,没有任何类似return_messages的冗余配置,输出格式完全由Parser按需解析。

核心API/组件源码解读

4.1 InMemoryChatMessageHistory(对标旧BufferMemory)

InMemoryChatMessageHistory是所有内存聊天历史存储的标准实现:

  • add_messages(messages: Sequence[BaseMessage]) -> None:批量添加多条消息,生产环境更可靠,能有效减少存储的后端调用次数。
  • clear() -> None:清空当前会话历史。
  • messages属性(只读):返回当前存储的所有消息对象。

4.2 ConversationBufferMemory(旧版)的残留API

如果你在旧版LangChain 0.2.x中使用ConversationBufferMemory,通常会看到以下方法:

  • save_context(inputs: Dict[str, Any], outputs: Dict[str, str]):保存用户输入和AI输出。
  • load_memory_variables(inputs: Dict[str, Any]) -> Dict[str, Any]:加载内存历史,返回字典,键为memory_key(默认是history)。
  • clear():清空缓冲。

4.3 新旧API映射表

旧设计 新设计
ConversationBufferMemory(return_messages=False) InMemoryChatMessageHistory + StrOutputParser(手动拼接Human/AI前缀)
ConversationBufferMemory(return_messages=True) InMemoryChatMessageHistory 直接配合 ChatPromptTemplate
save_context(...) RunnableWithMessageHistory.run 自动保存,无需手动调用
buffer InMemoryChatMessageHistory.messages
memory.load_memory_variables({})["history"] 模板中自动注入 history_messages_key
ConversationSummaryMemory(基于LLM的总结) 暂无官方直接替代,但可以通过自定义回调或LangChain的摘要链模拟实现

手把手项目实战教学

实战一:InMemoryChatMessageHistory 基础操作

from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage

history = InMemoryChatMessageHistory()

history.add_messages([
    HumanMessage(content="我叫李明,是一名软件工程师"),
    AIMessage(content="你好李明!请问有什么可以帮你的?")
])

history.add_message(HumanMessage(content="你还记得我做什么工作吗?"))

print("完整历史记录:")
for msg in history.messages:
    print(f"{msg.type}: {msg.content}")

运行结果会显示三条消息,与旧版BufferMemory存储的效果完全一致,但比旧版Buffered Memory结构更简单。

实战二:ConversationBufferWindowMemory 基础使用

from langchain.memory import ConversationBufferWindowMemory

# 只保留最近2轮对话
memory = ConversationBufferWindowMemory(k=2, return_messages=True)

# 模拟三轮对话
memory.save_context({"input": "第一轮:我叫李明"}, {"output": "AI:你好"})
memory.save_context({"input": "第二轮:我是软件工程师"}, {"output": "AI:知道了"})
memory.save_context({"input": "第三轮:我喜欢打篮球"}, {"output": "AI:记住了"})

# 注意:此时 memory 中只保留了第2轮和第3轮的消息
print(memory.load_memory_variables({}))

⚠️ 注意:这段代码适用于需要回顾旧版API逻辑或阅读旧代码的情况。新版开发应优先考虑InMemoryChatMessageHistory

实战三:总结旧版BufferMemory代码(if encountered)

from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory(return_messages=True)
memory.save_context({"input": "我叫李明"}, {"output": "你好李明"})
memory.save_context({"input": "我是一名工程师"}, {"output": "太棒了"})

print(memory.load_memory_variables({}))

完整可运行Python代码

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""LangChain 第13课:ConversationBufferMemory完整演示"""

import os
from dotenv import load_dotenv
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage

load_dotenv()

def demo_in_memory():
    print("新版InMemoryChatMessageHistory 实践")
    history = InMemoryChatMessageHistory()
    history.add_messages([
        HumanMessage(content="我叫李明,是一名软件工程师"),
        AIMessage(content="你好,李明!很高兴认识你。")
    ])
    history.add_message(HumanMessage(content="你还记得我做什么工作吗?"))

    for msg in history.messages:
        print(f"{msg.type}: {msg.content}")

def demo_legacy_window():
    print("\n旧版ConversationBufferWindowMemory 快照")
    from langchain.memory import ConversationBufferWindowMemory
    memory = ConversationBufferWindowMemory(k=2, return_messages=True)
    memory.save_context({"input": "第一轮:我叫李明"}, {"output": "AI:你好"})
    memory.save_context({"input": "第二轮:我是软件工程师"}, {"output": "AI:知道了"})
    memory.save_context({"input": "第三轮:我喜欢打篮球"}, {"output": "AI:记住了"})
    print("保留的最远两轮记忆:", memory.load_memory_variables({}))

if __name__ == "__main__":
    demo_in_memory()
    demo_legacy_window()

环境依赖安装命令

# 激活虚拟环境
source venv/bin/activate  # Mac/Linux
# venv\Scripts\activate   # Windows

# 安装所有依赖
pip install langchain==0.3.7 langchain-core==0.3.21 langchain-openai==0.2.8 python-dotenv==1.0.1

# 仅当需要查看旧版API时才安装
pip install langchain-classic

# Redis 依赖(生产级持久化)
pip install redis

常见报错坑点与避坑方案

坑1:ImportError: cannot import name 'ConversationBufferMemory' from 'langchain.memory'

现象:新环境中导入旧版内存组件失败。

原因:LangChain 0.3.x 已经把ConversationBufferMemory从主框架中移入langchain-classic

解决方案:换用新架构的InMemoryChatMessageHistory替代。

坑2:使用了ConversationBufferMemory(return_messages=True)但传入文本模型后报错

现象:LLM类型不匹配,提示非法消息期望字符串。

原因:Completion模型只接受字符串,不接受消息对象列表。

解决方案:设置return_messages=False后手动拼接字符串,或直接换用法最新模型体系ChatOpenAI

坑3:memory.save_context未生效

现象:明明调用了多次save_context,但load_memory_variables发现历史仍然空空如也。

原因:默认的memory_key与链不同步,或在传递过程中手动覆盖了输入。

解决方案:检查ConversationChain中的memory_key,默认为history;如果你想改,必须显式定义。

坑4:ConversationBufferWindowMemory的k值单位混淆

现象:希望记住3轮对话,但只记住了1.5轮。

原因:窗口内存中的k代表存储的交互实例(messages对数)。一“轮”包括用户的输入和AI的一条回复。

解决方案:窗口内存的交互单位是(My turn + AI turn)。

本节核心知识点总结

📌 ConversationBufferMemory 是旧版LangChain中最核心的记忆组件,作用是把对话历史无压缩地存储为纯文本或消息列表,适合短时对话。由于架构演进,它已在v0.3.1中被标记为废弃,并在1.0中完全移除。

📌 存储方式的对比:旧BufferMemory通过参数return_messages切换消息格式,模糊不清;新版InMemoryChatMessageHistory只存储消息对象,配合Parser统一解析。

📌 ConversationBufferWindowMemory 是与BufferMemory并列但增加窗口截断功能的变体,使用滑动窗口仅保留最近几轮对话。它是旧版中控制token消耗最直接的工具。

📌 迁移推荐:全新项目统一采用InMemoryChatMessageHistory + RunnableWithMessageHistory架构。这是LangChain认为唯一面向未来的对话记忆方案。

课后练习题

选择题

1. ConversationBufferMemory在新版LangChain中的状态是?
A. 是推荐使用的标准记忆组件。
B. 已被标记为废弃(deprecated),将在1.0中移除。
C. 只能在0.1.x版本中使用。
D. 从0.3.x开始得到了全面增强。

答案及解析:B。ConversationBufferMemory在v0.3.1被标记为废弃,并将在1.0中彻底移除,推荐迁移到InMemoryChatMessageHistoryRunnableWithMessageHistory

2. ConversationBufferWindowMemoryk 参数代表什么含义?
A. 保留的历史消息总条数。
B. 保留的最近k轮对话(每轮=用户输入+AI回复)。
C. 保留的 Token 总数。
D. 保留的时间周期。

答案及解析:B。k代表窗口大小,即保留的最近k组(Human + AI)完整对话。

3. 在新版LangChain中,哪个类负责内存消息的存储?
A. ConversationBufferMemory
B. BaseMemory
C. InMemoryChatMessageHistory
D. ConversationChain

答案及解析:C。新版记忆架构的核心存储基类是BaseChatMessageHistory,而它的内存实现就是InMemoryChatMessageHistoryConversationChainConversationBufferMemory都属于被废弃的旧模块。

简答题

4. 请描述return_messages参数在ConversationBufferMemory中的作用。

参考答案

  • return_messages=False(默认):load_memory_variables返回的是一个字符串,格式为"Human: ...\nAI: ...",适用于Completion模型。
  • return_messages=True:返回的是一个List[BaseMessage],直接与Chat模型的输入对齐。这一机制在过渡期解决了两类模型兼容问题,但也增加了理解的复杂度。

实践题

5. 将以下ConversationBufferMemory代码迁移到新版LangChain架构。

旧代码:

from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain_openai import ChatOpenAI

memory = ConversationBufferMemory(return_messages=True)
conversation = ConversationChain(llm=ChatOpenAI(), memory=memory)

conversation.predict(input="我叫李明")
conversation.predict(input="我叫什么名字?")

答案

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

llm = ChatOpenAI(model="gpt-3.5-turbo")

prompt = ChatPromptTemplate.from_messages([
    MessagesPlaceholder(variable_name="history"),
    ("human", "{input}")
])

chain = prompt | llm | StrOutputParser()

store = {}
def get_session_history(session_id: str):
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]

conversation = RunnableWithMessageHistory(
    chain,
    get_session_history,
    input_messages_key="input",
    history_messages_key="history"
)

config = {"configurable": {"session_id": "test_user"}}
conversation.invoke({"input": "我叫李明"}, config)
conversation.invoke({"input": "我叫什么名字?"}, config)

解析:生成新版架构中的ChatMessageHistory作用于MessagesPlaceholder,与RunnableWithMessageHistory完全解耦,存储格式清爽。旧predict被映射到了invoke方法。


🔗《30节课 LangChain 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐