LangChain 入门:Memory 记忆组件详解
什么是 Memory
在构建基于大语言模型(LLM)的应用时,存储对话历史中的信息的能力被称为'记忆'(Memory)。这种机制允许应用程序记住之前的交互内容,从而支持上下文联想和连续对话。记忆组件既可以单独使用,也可以无缝集成到一条链(Chain)中。
记忆的存储生命周期通常覆盖程序执行的全过程,即从开始执行到结束期间产生的所有记忆。一个标准的记忆组件需要支持以下核心操作:
- 读取(Read):获取之前存储的交互内容。
- 写入(Write):将当前的交互内容保存下来。
每条链定义了核心执行逻辑,期望某些输入,这些输入一部分来自用户,另一部分来自记忆组件。在一次与 LLM 的完整交互中,链与记忆组件通常会进行两次交互:
- 读取记忆:将之前的交互内容读取出来,放入到本次的提示词(Prompt)上下文中。
- 写入记忆:将本次的交互结果(包括用户输入和模型输出)写入到记忆当中,以便后续使用。
为什么需要使用记忆组件
在 LangChain 中,如果直接使用 llm.invoke 进行大模型对话,LLM 的记忆范围仅限于单次会话的执行过程。一旦运行结束,再次对话就是新的开始,没有以前的记忆内容。例如,当提问'我刚刚说了什么'时,如果没有记忆组件,模型无法回答前一次的交互内容。
from langchain_community.llms import Tongyi
llm = Tongyi()
print("第一次对话:", llm.invoke("今天天气真好啊"))
print("第二次对话:", llm.invoke("我刚刚说了什么"))
运行上述代码,第二次对话时模型会因为没有上下文而无法识别'刚刚'指代的内容。
而使用记忆组件就可以让 LLM 拥有记忆能力,能够将进行上下文联想。这让与大模型对话时有和真人对话的感觉,能够维持多轮对话的连贯性。
使用步骤
要构建一个具备记忆能力的对话系统,通常需要四个部件组合起来使用:大模型、提示词模板、链、记忆组件。
- 实例化一个 LLM:选择合适的大模型服务。
- 定义记忆组件:根据需求选择合适的 Memory 类型。
- 创建提示词模板:在 Prompt 中预留记忆内容的占位符。
- 使用链将它们链接起来:通过 Chain 将上述组件串联。
四种记忆组件详解
1. ConversationBufferMemory(会话缓冲区)
如实记录列表中所有的对话历史消息,并保留完整的交互记录。随着历史记录的增加,上下文长度会越来越长,导致运行速度变慢,甚至超出大模型的 Token 限制。适用于交互次数少、输入输出字符量不大的场景。
使用方法
import os
from dotenv import find_dotenv, load_dotenv
load_dotenv(find_dotenv())
os.environ["DASHSCOPE_API_KEY"] = "你的 API_KEY"
from langchain_community.llms import Tongyi
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.memory import ConversationBufferMemory
llm = Tongyi()
template = '''你是一个美少女,你的名字是燕砸,你的任务是用温柔的语气回答人类的问题。
{chat_memory}
human:{question}
'''
prompt = PromptTemplate(
template=template,
input_variables=["question"]
)
memory = ConversationBufferMemory(memory_key="chat_memory", return_messages=False)
chain = LLMChain(
llm=llm,
prompt=prompt,
memory=memory,
verbose=True
)
chain.invoke("我喜欢美食,我最喜欢的美食是清蒸鲈鱼")
chain.invoke("你是谁?")
chain.invoke("今天的天气真好啊")
res = chain.invoke("我最开始跟你聊的什么呢?")
print(res['text'])
在 Prompt 的 template 中有一个 {chat_memory},这就是记忆组件的输入,也就是链与记忆组件的第一次交互点。在 memory 定义中需要将这个记忆组件的输入定义出来,memory_key 必须与 prompt 中的变量名一致。
2. ConversationBufferWindowMemory(会话缓冲窗口)
持续记录对话历史,但只使用最近的 k 个交互。这确保了缓存大小不会过大,运行速度比较稳定。适用于需要一定上下文但又不希望无限累积历史的场景。
使用方法
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(memory_key="chat_memory", k=2, return_messages=False)
chain = LLMChain(
llm=llm,
prompt=prompt,
memory=memory,
verbose=True
)
参数 k 决定了记录交互的次数。其余两个参数与上一个一致。设置 k=2 后,模型只会记得最近的两次对话内容,更早的历史会被丢弃。
3. ConversationSummaryMemory(会话摘要)
随着时间的推移总结对话内容,并将摘要存储在记忆中。需要的时候将摘要注入提示词或链中。缓存不会过大,运行稳定,但是写入记忆时需要调用一次 LLM 进行摘要,因此运行速度比 ConversationBufferWindowMemory 慢很多。这使得它可以记住很长的交互记忆,不过随着交互的增加,摘要的内容不断迭代更换,使得某些细节内容可能会遗失。
使用方法
from langchain.memory import ConversationSummaryMemory
memory = ConversationSummaryMemory(llm=llm, memory_key="chat_memory", return_messages=False)
chain = LLMChain(
llm=llm,
prompt=prompt,
memory=memory,
verbose=True
)
这个类型的记忆组件需要传入一个 llm 参数,使用 LLM 来进行对话摘要。这种方式适合长对话场景,但需要注意摘要可能丢失细节信息。
4. VectorStoreRetrieverMemory(向量存储)
将记忆存储在向量数据库中,并在每次调用时查询前 K 个最'显著'的文档。与大多数其他记忆类不同的是,它不明确跟踪交互的顺序。在这种情况下,'文档'是先前对话片段。这对于提及 AI 在对话中早些时候被告知的相关信息可能是有用的。这种方式通过语义检索来匹配上下文,而非简单的顺序匹配。
使用方式
from datetime import datetime
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.memory import VectorStoreRetrieverMemory
from langchain.chains import ConversationChain
from langchain.prompts import PromptTemplate
import faiss
from langchain.docstore import InMemoryDocstore
from langchain.vectorstores import FAISS
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
embedding_fn = OpenAIEmbeddings().embed_query
vectorstore = FAISS(embedding_fn, index, InMemoryDocstore({}), {})
llm = OpenAI(temperature=0)
_DEFAULT_TEMPLATE = """以下是人类和 AI 之间友好的对话。AI 健谈并从其上下文中提供了许多具体细节。如果 AI 不知道问题的答案,它会真诚地说自己不知道。
先前对话的相关部分:
{history}
(如果不相关,您无需使用这些信息)
当前对话:
人类:{input}
AI:"""
PROMPT = PromptTemplate(
input_variables=["history", "input"], template=_DEFAULT_TEMPLATE
)
memory = VectorStoreRetrieverMemory(vectorstore=vectorstore, top_k=2)
conversation_with_summary = ConversationChain(
llm=llm,
prompt=PROMPT,
memory=memory,
verbose=True
)
conversation_with_summary.predict(input="Hi, 我叫 Perry,有什么新鲜事?")
这是官方提供的示例代码,有兴趣的可以进行尝试。这里不再过多展示运行结果。
记忆组件对比与选择建议
| 组件类型 | 优点 | 缺点 | 适用场景 |
|---|
| ConversationBufferMemory | 完整保留历史,实现简单 | 上下文过长会导致 Token 溢出,速度慢 | 短对话,测试环境 |
| ConversationBufferWindowMemory | 控制上下文长度,性能稳定 | 早期历史会被丢弃 | 中等长度对话,需关注近期内容 |
| ConversationSummaryMemory | 支持超长对话,节省 Token | 写入慢,摘要可能丢失细节 | 长对话,角色扮演,剧情游戏 |
| VectorStoreRetrieverMemory | 语义检索,不依赖顺序 | 配置复杂,依赖向量库 | 知识库问答,非线性的对话流 |
注意事项
- Token 限制:无论使用哪种记忆组件,最终传递给 LLM 的总 Token 数不能超过模型的限制。对于 Buffer 类型,务必监控上下文长度。
- 隐私安全:记忆组件会存储用户的对话历史,请确保符合数据隐私合规要求,不要在敏感业务中使用明文存储。
- 性能权衡:
ConversationSummaryMemory 虽然节省空间,但每次写入都需要消耗额外的 LLM 调用成本和时间,需根据业务实时性要求权衡。
- Prompt 设计:在 Prompt 中正确放置记忆占位符是关键,通常放在
human 输入之前,以便模型理解上下文。
总结
对于一个聊天机器人,在对话中可能需要进行上下文联想、分析的操作,或者是进行一个情景对话,记忆组件都是不可或缺的重要组成部分。在 LangChain 的早期版本中,记忆组件运行速度非常的慢,如果作为一个请求内容返回给前端百分百会超时,在稳定的版本出来之后就流畅很多了,应用到实际的应用中也更具有体验感。在这样的条件下,对大模型进行角色定制,对话中这个角色的丰富度就会高很多。也能做出更多更有意思的聊天机器人。
选择合适的记忆组件取决于具体的业务需求。如果是简单的问答,Buffer 即可;如果需要长期记忆且对话频繁,Summary 或 VectorStore 是更好的选择。开发者应根据 Token 预算、延迟要求和数据精度需求进行综合评估。