随着 LangChain 的快速迭代,LCEL (LangChain Expression Language) 逐渐成为主流。然而,在很多现有的生产系统和教程中,我们依然频繁看到经典(Legacy)组件的身影。
本文将带你深入了解 LangChain 最经典的对话组合:ConversationBufferMemory 与 ConversationChain。
1. ConversationBufferMemory:最纯粹的记忆
1.1 什么是 ConversationBufferMemory?
ConversationBufferMemory 是 LangChain 中最基础的记忆组件。它的逻辑非常简单粗暴:它把你说过的每一句话、AI 回复的每一句话,都原封不动地塞进一个变量里。
1.2 工作原理
想象一个无限长的记事本:
- 用户说:"Hi" -> 记事本写:"Human: Hi"
- AI 回复:"Hello" -> 记事本写:"AI: Hello"
- 下次对话时,LangChain 会把记事本里的所有内容复制粘贴到 Prompt 的
{history}位置。
1.3 优缺点分析
- 优点 :
- 无损:完全保留了对话的所有细节。
- 简单:易于理解和调试。
- 缺点 :
- Token 爆炸:随着对话变长,历史记录会越来越长,最终超出 LLM 的 Context Window(上下文窗口)限制,导致报错或费用激增。
2. ConversationChain:开箱即用的对话链
2.1 什么是 ConversationChain?
ConversationChain 是一个预封装好的 Chain。它帮你把以下三样东西组装在了一起:
- LLM (大语言模型)
- Memory (记忆组件)
- 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。