跳转到内容

为什么需要记忆:无状态模型的局限

到目前为止,我们构建的所有 LangChain 应用都有一个共同的隐含假设:每次调用模型时,它都能"记住"之前的对话内容。但事实并非如此——LLM 本质上是一个**无状态(stateless)**的系统,每次调用都是独立的,它不知道你之前问过什么、它回答过什么。

这一节我们将深入理解这个问题的本质,以及为什么"记忆"是构建真正可用的对话系统的关键能力。

LLM 是无状态的:一个直观的演示

让我们用一个最简单的例子来暴露这个问题:

python
from langchain_openai import ChatOpenAI

chat = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 第一轮
r1 = chat.invoke("我叫张三")
print(r1.content)
# 你好,张三!很高兴认识你。

# 第二轮 — 模型还记得吗?
r2 = chat.invoke("我叫什么名字?")
print(r2.content)
# 我不知道您叫什么名字。请告诉我您的名字。

注意看第二轮的回答——模型说"我不知道您叫什么名字"。明明第一轮我们告诉过它"我叫张三",但第二轮它完全忘记了。这就是 无状态的含义:每次 invoke() 调用之间没有任何关联,模型不会自动保留任何上下文信息

为什么会这样?因为从技术角度看,chat.invoke() 做的事情非常简单:把输入发给 OpenAI 的 API,API 把输入送进模型做一次前向推理,返回结果,然后——完事了。没有持久化存储,没有内部状态,没有"记忆"。下一次调用就是一个全新的开始。

为什么这很严重

在真实的应用场景中,无状态带来的问题远不止"忘记名字"这么简单。让我们看看几个具体场景:

场景一:多轮问答中的上下文断裂

python
chat = ChatOpenAI(model="gpt-4o-mini", temperature=0)

r1 = chat.invoke("Python 中的 GIL 是什么?")
print(r1.content[:80])
# GIL(全局解释器锁)是 Python 解释器中的一个互斥锁...

r2 = chat.invoke("那怎么绕过它?")   # "它"指什么?模型不知道
print(r2.content)
# 您的问题中提到"它",但没有足够的信息来确定...

第二轮中的"它"指的是第一轮提到的"GIL",但由于两轮调用之间没有上下文传递,模型根本不知道"它"是什么。人类对话中这种**代词引用(coreference)**极其常见,但对无状态的 LLM 来说却是无法逾越的障碍。

场景二:任务跟进中的信息丢失

python
r1 = chat.invoke("帮我写一个快速排序函数")
# (模型输出了完整的 quicksort 代码)

r2 = chat.invoke("把它改成能处理重复元素的版本")
# 改什么?模型不知道之前写过 quicksort

用户说的"它"指的是上一轮生成的 quicksort 函数,但模型对此一无所知。结果就是第二轮要么答非所问,要么重新写一个(可能和第一轮完全不同)。

场景三:纠错与澄清循环

python
r1 = chat.invoke("1+1等于几")
# 等于 2

r2 = chat.invoke("不对,我是问二进制下的结果")  
# 模型不知道你在纠正上一轮的回答

这种"你说错了 → 我纠正 → 你基于纠正重新回答"的交互模式在人类对话中再自然不过了,但在无状态模型中却无法自然地实现。

解决方案:手动传递历史

既然模型本身不记事,那最直接的思路就是我们自己帮它记——把之前的对话历史在每次调用时一起传给模型:

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage

chat = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# 手动维护消息列表
messages = []

# 第一轮
messages.append(HumanMessage(content="我叫张三"))
response1 = chat.invoke(messages)
messages.append(AIMessage(content=response1.content))
print(f"AI: {response1.content}")

# 第二轮 — 把完整历史传进去
messages.append(HumanMessage(content="我叫什么名字?"))
response2 = chat.invoke(messages)
messages.append(AIMessage(content=response2.content))
print(f"AI: {response2.content}")
# AI: 根据您刚才提供的信息,您叫张三。

这次模型正确地回答了"您叫张三"!关键区别在于:第二轮调用时,我们把第一轮的对话(用户说了什么 + AI 回复了什么)作为消息列表的一部分一并传入。模型看到了完整的上下文,所以能够正确地理解"我叫什么名字"这个问题指的是之前提到的信息。

手动管理的问题

上面的方案虽然能工作,但它有几个明显的问题:

问题一:代码冗长且易错。每轮对话都需要手动 append HumanMessage 和 AIMessage,漏掉任何一个都会导致上下文断裂。当对话逻辑变复杂(比如加入系统提示词、工具调用结果等),消息管理的复杂度会急剧上升。

问题二:Token 消耗线性增长。每轮对话都要把全部历史消息重新发送给 API。聊了 10 轮就要发 20 条消息,聊了 100 轮就要发 200 条消息。这不仅增加了每次调用的成本和延迟,还会很快触及模型的上下文窗口限制(GPT-4o 大约支持 128K token,听起来很多,但对于长对话来说并不宽裕)。

问题三:格式不一致的风险。不同场景下需要不同格式的消息记录——有的只需要最近几轮摘要,有的需要完整的逐字记录,有的需要结构化的键值对。手动管理很难灵活适配这些变化。

这就是为什么需要 Memory 组件

LangChain 的 Memory(记忆) 组件正是为了解决上述问题而设计的。它的核心职责只有一件事:自动管理对话历史,让 Chain 在每次调用时都能获得正确的上下文

你可以把 Memory 想象成一个聪明的秘书:

  • 你只管跟模型对话
  • 秘书在后台默默地记录每一句话
  • 下次你开口时,秘书自动把之前的对话整理好递给模型
  • 你不需要关心记录的格式、长度、存储方式等细节
python
# 有 Memory 的写法(伪代码示意)
from langchain.memory import ConversationBufferMemory

memory = ConversationBufferMemory()
chain = prompt | chat | ... | memory   # memory 自动管理上下文

chain.invoke("我叫张三")      # 自动记录
chain.invoke("我叫什么?")    # 自动带上之前的上下文

对比一下有无 Memory 的差异:

维度无 Memory(手动管理)有 Memory(LangChain 管理)
每轮代码量~10 行(append + invoke)1 行(invoke)
遗漏风险高(容易忘 append)低(自动化)
Token 控制需自己实现截断/摘要内置多种策略可选
存储灵活性硬编码可切换后端(内存/Redis/数据库)
格式适配自己拼装统一接口

记忆不是万能药

在深入学习 LangChain 的各种 Memory 实现之前,有必要先建立正确的预期——Memory 能解决上下文传递的问题,但它不能解决所有与"记忆"相关的挑战:

第一,Memory 不等于长期记忆。LangChain 的 Memory 组件管理的是当前会话内的短期对话历史。如果你今天跟聊天机器人聊了关于 Python 的问题,明天再来时它不会记得——除非你自己实现了跨会话的持久化存储。这是应用层需要解决的问题,不是 Memory 组件本身的职责。

第二,Memory 不能突破上下文窗口限制。不管用哪种 Memory 策略,最终塞进提示词的内容都不能超过模型的 token 上限。如果你的对话有几千轮,不可能把全部历史都塞进去——必须做取舍:保留最近的、丢掉最早的、或者做摘要压缩。不同的 Memory 类型本质上就是在做不同的取舍策略。

第三,Memory 的质量直接影响回答质量。如果 Memory 保留了太多无关的历史噪音(比如闲聊),或者丢掉了关键的上下文(比如用户之前设定的约束条件),模型的回答质量就会下降。选择合适的 Memory 类型和参数配置是一门需要根据具体场景调优的手艺。

接下来的一节,我们将全面介绍 LangChain 提供的各种 Memory 组件,理解它们各自的设计思想、适用场景和使用方法。

基于 MIT 许可发布