趋近智
使用 ConversationBufferMemory 存储整个对话历史是一种直接的方法。然而,这种做法带来一个较大的风险:随着对话的进行,历史对象会增大。最终,累积的上下文可能超出语言模型的令牌限制,导致API错误和应用故障。另一种方法是 ConversationSummaryMemory,它通过总结过往互动来解决这个令牌限制问题。尽管解决了令牌溢出问题,但这种方法也有其自身需要权衡之处,包括额外调用语言模型进行总结所带来的延迟和成本。
在这两个极端之间,存在着一系列更实用的策略,它们管理固定大小的历史记录。这些方法通过只保留对话中最近的部分,在上下文保留和资源管理之间取得平衡。它们快速、高效,并防止你的应用因上下文过大而失效。
最简单的固定大小策略是记住一定数量的过往互动。这由 ConversationBufferWindowMemory 处理。它的工作方式与 ConversationBufferMemory 完全相同,但它只保留最近 k 轮对话历史。一“轮”包括用户的输入和AI的即时回复。
这种方法计算成本低,并为你的上下文大小提供了可预测的上限。它对于最新交流最有关联的应用特别有效,例如面向任务的机器人,其中十条消息之前的上下文不太可能有用。
让我们使用这种记忆类型配置一个链,设置 k=2 以只存储最近的两次互动。
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferWindowMemory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 初始化大型语言模型
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
# 定义提示结构
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个旅游代理的有用助手。"),
MessagesPlaceholder(variable_name="chat_history"),
("human", "{input}")
])
# 使用 k=2 初始化记忆
# 这将存储最近的 2 对人类/AI消息
memory = ConversationBufferWindowMemory(
memory_key="chat_history",
return_messages=True,
k=2
)
# 创建对话链
conversation = ConversationChain(
llm=llm,
prompt=prompt,
memory=memory
)
# --- 互动 1 ---
print(conversation.invoke({"input": "你好,我正计划旅行。我叫George。"}))
# AI: 你好,George!你打算去哪里旅行?
# --- 互动 2 ---
print(conversation.invoke({"input": "我想去巴黎。"}))
# AI: 巴黎是个不错的选择!你在寻找机票还是酒店的建议?
# --- 互动 3 ---
print(conversation.invoke({"input": "我对航班感兴趣。"}))
# AI: 好的,我可以帮忙。你打算预订去巴黎的哪天航班?
# --- 互动 4 ---
# 第一次互动(“我叫George”)现在已超出 k=2 的窗口范围
# 代理将忘记用户的名字。
print(conversation.invoke({"input": "你还记得我的名字吗?"}))
# AI: 抱歉,我的对话历史中没有存储你的名字。你能提醒我一下吗?
在最后一次交流中,模型不记得用户的名字。第一次互动,其中提到了“George”,在第三次互动开始后被推出了对话窗口。那时记忆缓冲区只包含关于巴黎和航班的交流。这说明了主要的权衡:ConversationBufferWindowMemory 高效但会丢弃旧的上下文,无论其是否有用。
使用 k 计算互动是管理上下文大小的好方法,但它不精确。有些对话轮次可能很短(“好的”),而另一些则可能是长段落。防止API限制的更准确方法是根据令牌数量管理历史记录。
ConversationTokenBufferMemory 允许进行这种精细控制。你不是指定 k 值,而是指定一个 max_token_limit。记忆会保留符合此令牌预算的最新消息。当新消息添加时,它会从对话开始处删除最旧的消息,直到总令牌数回到限制之内。
一个值得注意的实现细节是,这种记忆类型需要访问LLM实例以正确计算消息的令牌数量,因为不同模型之间的分词方案可能有所不同。
from langchain.memory import ConversationTokenBufferMemory
# 注意我们将LLM对象传递给记忆模块
# 这是它准确计算令牌所必需的。
token_memory = ConversationTokenBufferMemory(
llm=llm,
memory_key="chat_history",
return_messages=True,
max_token_limit=200 # 设置历史记录的令牌限制
)
# 链的创建方式相同
token_conversation = ConversationChain(
llm=llm,
prompt=prompt,
memory=token_memory
)
# --- 互动 1 (短) ---
print(token_conversation.invoke({
"input": "你好,我是Sarah,我想预订去东京的旅行。"
}))
# --- 互动 2 (长) ---
print(token_conversation.invoke({
"input": "你能给我一个详细的推荐活动列表吗? "
"我对历史遗迹、现代建筑、 "
"当地美食市场,以及可能的话,一个科技博物馆感兴趣。 "
"请为每个地方提供一些描述。"
}))
# --- 互动 3 (短) ---
# 在AI对上一轮做出长回复后,
# 总令牌数会很高。最旧的消息
# 关于用户名字的信息可能会被删除以保持在限制之内。
print(token_conversation.invoke({
"input": "我对哪个城市感兴趣?"
}))
# AI: 你对访问东京感兴趣!
print(token_conversation.invoke({
"input": "我的名字是什么?"
}))
# AI可能会回复:抱歉,我不记得你提过你的名字。
# 或者它可能记得,这取决于确切的令牌数量。
在这种情况下,代理记得“东京”,因为它是一次更近期、实质性的交流的一部分。然而,如果第二次互动及其回复的总长度超过了 max_token_limit,则包含“Sarah”的第一条消息可能会被丢弃。此方法提供了防止令牌溢出错误的保护,同时保留最新的上下文。
这些固定大小的记忆类型与基本缓冲记忆之间的差异可以通过观察它们随时间保留对话的哪些部分来直观表示。
该图展示了不同记忆类型如何修剪对话历史。
BufferMemory保留所有六条消息。设定k=2的BufferWindowMemory只保留最后四条消息(两轮用户/AI对话)。TokenBufferMemory也会修剪最旧的消息以保持在预算内,在这种情况下也保留了最后四条。
合适的记忆组件完全依照你应用的需求:
通过选择合适的记忆策略,你可以构建既有效又高效的对话应用,提供连贯的用户体验而不会耗尽你的令牌预算。
简洁的语法。内置调试功能。从第一天起就可投入生产。
为 ApX 背后的 AI 系统而构建
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造