LangChain 经典回顾:ConversationBufferMemory 与 ConversationChain

随着 LangChain 的快速迭代,LCEL (LangChain Expression Language) 逐渐成为主流。然而,在很多现有的生产系统和教程中,我们依然频繁看到经典(Legacy)组件的身影。

本文将带你深入了解 LangChain 最经典的对话组合:ConversationBufferMemoryConversationChain

1. ConversationBufferMemory:最纯粹的记忆

1.1 什么是 ConversationBufferMemory?

ConversationBufferMemory 是 LangChain 中最基础的记忆组件。它的逻辑非常简单粗暴:它把你说过的每一句话、AI 回复的每一句话,都原封不动地塞进一个变量里。

1.2 工作原理

想象一个无限长的记事本:

  1. 用户说:"Hi" -> 记事本写:"Human: Hi"
  2. AI 回复:"Hello" -> 记事本写:"AI: Hello"
  3. 下次对话时,LangChain 会把记事本里的所有内容复制粘贴到 Prompt 的 {history} 位置。

1.3 优缺点分析

  • 优点
    • 无损:完全保留了对话的所有细节。
    • 简单:易于理解和调试。
  • 缺点
    • Token 爆炸:随着对话变长,历史记录会越来越长,最终超出 LLM 的 Context Window(上下文窗口)限制,导致报错或费用激增。

2. ConversationChain:开箱即用的对话链

2.1 什么是 ConversationChain?

ConversationChain 是一个预封装好的 Chain。它帮你把以下三样东西组装在了一起:

  1. LLM (大语言模型)
  2. Memory (记忆组件)
  3. Prompt (提示词模板)

你不需要自己写 Prompt Template,不需要自己处理 history 变量注入。它内置了一个默认的 Prompt(通常是 "The following is a friendly conversation...")。

2.2 代码实战

让我们通过一个简单的 Python 脚本 (src/examples/memory/demo_conversation_buffer_memory.py) 来演示它们的配合。

python 复制代码
from langchain_classic.memory import ConversationBufferMemory
from langchain_classic.chains import ConversationChain
from src.llm.gemini_chat_model import get_gemini_llm

# 1. 初始化 Memory
# 它负责在内存中维护一个不断增长的字符串
memory = ConversationBufferMemory()

# 2. 初始化 Chain
# 自动将 Memory 挂载到 LLM 上
conversation = ConversationChain(
    llm=get_gemini_llm(), 
    memory=memory,
    verbose=True # 开启 verbose 可以看到它到底发给了 LLM 什么
)

# 3. 第一轮对话
conversation.predict(input="Hi, my name is Alice.")
# Memory 更新: "Human: Hi, my name is Alice.\nAI: Hello Alice!"

# 4. 第二轮对话
conversation.predict(input="What is my name?")
# Memory 再次更新...

2.3 运行结果解析

当我们运行上述代码时,ConversationChain 会自动构建如下 Prompt 发送给 LLM:

text 复制代码
The following is a friendly conversation between a human and an AI...

Current conversation:
Human: Hi, my name is Alice.
AI: Hello Alice!
Human: What is my name?
AI: 

这就是为什么 AI 能够回答 "Your name is Alice",因为它在 Prompt 里"看到"了之前的对话记录。

3. 进阶:如何查看记忆内容?

你可以随时调用 load_memory_variables 来查看当前 Buffer 里存了什么:

python 复制代码
print(memory.load_memory_variables({}))
# 输出:
# {'history': "Human: Hi...\nAI: Hello..."}

4. 进阶:如何限制历史消息上限?

用户提问: ConversationBufferMemory 可以限制历史消息的上限吗?

回答: 不可以,它默认保存所有历史。

如果你需要限制历史记录(为了节省 Token 或防止 Context Window 溢出),你需要切换到它的兄弟组件

  • ConversationBufferWindowMemory: 按轮数限制(如只保留最近 5 轮)。
  • ConversationTokenBufferMemory: 按 Token 数限制(如只保留最近 2000 token)。
  • ConversationSummaryMemory: 自动调用 LLM 对旧历史进行摘要总结。

5. 进阶:如何持久化到数据库?

用户提问: ConversationBufferMemory 只能把消息保存到内存吗?数据库是否可以?

回答: 它可以支持数据库,但需要配合 chat_memory 参数。

默认情况下,ConversationBufferMemory 在内部创建了一个临时的内存列表 (ChatMessageHistory)。一旦程序重启,数据就丢失了。

如果你想把它保存到数据库(如 Redis, Postgres),你需要替换掉这个临时的内存列表,换成一个连接数据库的 History 对象

代码示例 (伪代码)

python 复制代码
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_classic.memory import ConversationBufferMemory

# 1. 创建一个连接 Redis 的 History 对象
# 这不是普通的 list,而是一个读写 Redis 的代理
message_history = RedisChatMessageHistory(
    url="redis://localhost:6379/0", 
    session_id="my-session"
)

# 2. 将这个 History 对象传给 BufferMemory
# 此时 Memory 不再把数据存在 RAM 里,而是直接读写 Redis
memory = ConversationBufferMemory(
    chat_memory=message_history
)

# 后续用法完全一样,但数据会自动持久化到 Redis
conversation = ConversationChain(llm=llm, memory=memory)

原理ConversationBufferMemory 是一个逻辑层(负责把消息格式化为 Prompt),而 chat_memory 是存储层(负责物理存取)。将存储层替换为数据库实现即可。

6. 新旧对比:本质区别是什么?

除了使用方式的不同,两者在架构设计上有着本质的区别:

1. 架构模式:实例绑定 (Stateful) vs 动态注入 (Stateless)

用户提问: ConversationChain 不是也可以传入不同的 Memory 吗?为什么说它是耦合的?

回答: 这里的耦合是指运行时的实例级绑定

  • ConversationChain (旧)

    当你初始化 chain = ConversationChain(memory=mem) 时,这个 chain 对象就和特定的那份 mem 对象绑定死了。

    • 后果 :这个 chain 实例变成了"有状态"的对象。它里面存着"用户A"的聊天记录。你不能 把这个 chain 实例拿去服务"用户B",否则用户B会看到用户A的记录。
    • 并发问题 :在 Web 服务中,你必须为每个用户 new 一个新的 Chain 实例,无法通过全局单例复用。
  • RunnableWithMessageHistory (新)

    它本身不持有 任何具体的 Memory 对象。它持有的只是一个工厂函数get_session_history)。

    • 后果:这个 Wrapper 对象是"无状态"的。它不知道也不关心当前在服务谁。
    • 动态性 :每次调用 invoke 时,它才根据传入的 config={"session_id": "B"} 动态地去工厂里找"用户B"的记录。
    • 并发优势 :你可以创建一个全局单例runnable,让它同时并发服务成千上万个用户,完全线程安全。

2. 逻辑灵活性:硬编码 vs 组合

  • ConversationChain (旧):它的内部逻辑(Load -> Prompt -> LLM -> Save)是硬编码在 Python 类里的。如果你想在"保存历史"之前加一个"敏感词过滤"步骤,你必须继承并重写这个类。
  • RunnableWithMessageHistory (新):它遵循 LCEL 组合原则。内部的 Chain 可以是任意复杂的 DAG(有向无环图)。你可以在任何环节插入自定义逻辑,Wrapper 只负责最外层的历史管理。
  • RunnableWithMessageHistory (新) :Chain 对象本身是无状态 的。状态(记忆)并不存在 Chain 对象里,而是根据 config={"session_id": "..."} 动态加载的。这意味着同一个 Chain 实例可以并发服务成千上万个用户(只要 session_id 不同)。这是生产环境的高并发神器。

对比总结表

特性 ConversationChain (Legacy) RunnableWithMessageHistory (LCEL)
架构模式 强耦合单体 解耦包装器
并发模型 有状态 (Stateful) - 难以并发 无状态 (Stateless) - 天然并发
灵活性 低 (Prompt 固定) 高 (Prompt/逻辑完全自定义)
流式支持 较弱 原生支持
推荐场景 快速原型、本地单人脚本 生产级 Web 服务、高并发系统

结论 :如果你正在构建一个多用户的 Web 应用,请务必使用 RunnableWithMessageHistory

相关推荐
沐雪架构师2 小时前
LangChain 1.0 Agent开发实战指南
开发语言·javascript·langchain
nvd116 小时前
LangChain 核心对比:ChatPromptTemplate vs PromptTemplate
人工智能·langchain
<花开花落>7 小时前
浅学 LangChain,AI 赋能软件测试
软件测试·langchain
玄同7657 小时前
LangChain v1.0 中间件深度解析:从 Callback 到 Middleware 的演进
人工智能·语言模型·自然语言处理·中间件·langchain·agent·智能体
沐雪架构师8 小时前
LangChain 1.0 记忆管理:短期与长期记忆详解
服务器·数据库·langchain
TGITCIC11 小时前
LangChain入门(十五)- LangGraph为什么这么香,看它是如何逆天DIFY的
langchain·工作流·rag·ai agent·ai智能体·langgraph·agentic
玄同7651 天前
告别 AgentExecutor:LangChain v1.0+ Agent 模块深度迁移指南与实战全解析
人工智能·语言模型·自然语言处理·langchain·nlp·agent·智能体
TGITCIC1 天前
LangChain入门(十四)- Agentic RAG 的正确打开方式:用 LangChain 实现“有思考、可解释、不遗漏”的检索增强问答
langchain·rag·ai agent·agentic·智能体开发·rag增强检索·agentic flow
TGITCIC1 天前
LangChain入门(十三)- 6步实操Agent落地大法
langchain·agent·rag·ai agent·ai开发·agent开发·ai智能体开发