虽然长期记忆架构应对了获取大量外部知识或回溯久远历史互动的难题,但在即时操作情境中维持一致的状态对于代理系统同样重要。LLM 上下文窗口的固有局限性,表示为 $L_{context}$,需要对策来有效管理近期信息的流动。这是短期记忆的范畴。短期记忆机制作为缓冲区,保存了最可能与代理的下一步推理或动作相关的信息。如果没有此类机制,一个在多轮或多步骤操作中运行的代理将很快迷失其即时目标、先前行动或近期观察,使其无法完成复杂的序列任务。让我们考察所用的常见方法。会话缓冲区记忆最直接的方法是 ConversationBufferMemory。它简单地保存了在一个会话中交换的所有互动历史(用户输入、代理思考、工具输出、代理回应)。# 示例 memory = [] def add_to_memory(entry_type, content): memory.append({"type": entry_type, "content": content}) # 代理互动 第一轮 add_to_memory("user_input", "What is the capital of France?") # ... LLM 处理中 ... add_to_memory("agent_thought", "The user asked for the capital of France. I know this.") add_to_memory("agent_response", "The capital of France is Paris.") # 代理互动 第二轮 add_to_memory("user_input", "What is its population?") # ... LLM 处理中 ... # 要回答第二轮,LLM 需要第一轮的上下文。 # 提示中包含“memory”的相关部分或全部。 prompt_context = "\n".join([f"{m['type']}: {m['content']}" for m in memory]) # ... LLM 根据 prompt_context 生成回应 ...优点:高保真度: 保留近期互动历史的所有细节。简单性: 易于实现和理解。缺点:上下文长度: 随着互动增长,迅速消耗可用的 $L_{context}$。对于非简单的互动或上下文窗口较小的模型,这通常难以维持。成本/延迟: 处理日益增长的大型提示会产生更高的计算成本和延迟。此方法仅适用于非常短的互动,其中超出 $L_{context}$ 不是问题。窗口记忆(滑动窗口)为了管理上下文长度,ConversationWindowBufferMemory 仅保留最近的 $k$ 次互动或轮次。随着新的互动发生,最旧的互动将被丢弃以维持固定大小的窗口。# 示例:k=2 次互动(假设一次用户提问 + 一次代理回应 = 1 次互动) class WindowMemory: def __init__(self, k=2): self.k = k self.buffer = [] # 存储 (user_input, agent_response) 或类似形式的元组 def add_interaction(self, user_input, agent_response): self.buffer.append((user_input, agent_response)) if len(self.buffer) > self.k: self.buffer.pop(0) # 移除最旧的互动 def get_context(self): # 格式化缓冲区以便用于提示 context = "" for user_q, agent_a in self.buffer: context += f"User: {user_q}\nAgent: {agent_a}\n" return context # 使用方法 memory = WindowMemory(k=2) memory.add_interaction("What is the capital of France?", "Paris") memory.add_interaction("Population?", "Around 2.1 million") memory.add_interaction("Currency?", "Euro") # 最旧的(“法国”/“巴黎”)互动被丢弃 print(memory.get_context()) # 输出: # 用户: Population? # 代理: Around 2.1 million # 用户: Currency? # 代理: Euro优点:有界上下文: 保证记忆组件对提示大小的贡献保持固定。简单性: 相对易于实现。缺点:信息丢失: 较旧的信息,即使可能相关,一旦超出窗口就会永久丢失。这可能在较长的对话或任务中破坏上下文连贯性。当只有最新交流内容重要时,此方法很有用,但它难以处理需要引用互动中较早内容的任务。存在变体,例如令牌限制缓冲区,其在达到特定的令牌计数时截断历史记录的开头,而非严格计数互动次数。摘要缓冲区记忆更精巧的方法是 ConversationSummaryBufferMemory。此方法旨在保留整个互动历史中的信息,同时仍管理上下文长度。它通过定期使用 LLM 来创建对话旧部分的摘要来实现。该过程通常包含:维护一个近期互动缓冲区(类似于会话缓冲区)。当缓冲区变大时,选择较旧的互动。使用 LLM 调用对这些选定的互动生成摘要。在记忆中用生成的摘要替换原始互动。保持最新互动以其原始形式存在。digraph G { rankdir=LR; node [shape=box, style=filled, fontname="sans-serif", fontsize=10]; subgraph cluster_window { label = "滑动窗口 (k=3)"; bgcolor="#e9ecef"; node [fillcolor="#a5d8ff"]; w1 [label="互动 1"]; w2 [label="互动 2"]; w3 [label="互动 3"]; w4 [label="互动 4\n(最旧 - 丢弃)"]; w5 [label="互动 5"]; w6 [label="互动 6"]; w7 [label="互动 7"]; w4 -> w1 [style=invis]; // 强制排序,尽管 w4 被丢弃 w1 -> w2 -> w3 -> w5 -> w6 -> w7 [style=invis]; w7 -> w6 -> w5 [color="#1c7ed6", label="保留"]; w3 -> w2 -> w1 [color="#adb5bd", style=dashed, label="丢弃"]; } subgraph cluster_summary { label = "摘要缓冲区"; bgcolor="#e9ecef"; node [fillcolor="#96f2d7"]; s1 [label="互动 1-4\n摘要"]; s5 [label="互动 5"]; s6 [label="互动 6"]; s7 [label="互动 7"]; s1 -> s5 -> s6 -> s7 [style=invis]; s7 -> s6 -> s5 [color="#12b886", label="保留原始"]; s1 [color="#12b886", label="保留摘要"]; } }滑动窗口和摘要缓冲区机制在 7 次互动后的对比。滑动窗口完全丢弃较旧的互动,而摘要缓冲区将它们压缩成摘要,保留部分信息同时保持最新互动详细。优点:上下文保存: 相比简单的缓冲区或窗口,能在更长的互动历史中保留信息。可控的上下文大小: 通过用简洁摘要替换详细历史,使提示大小保持可管理。缺点:摘要成本/延迟: 需要额外的 LLM 调用专门用于摘要,增加总成本和延迟。潜在信息丢失/扭曲: 摘要过程本身可能丢失细节或引入不准确性,这取决于 LLM 的摘要质量。复杂性: 实现和调整更复杂(例如,决定何时以及摘要什么内容)。此方法对于从事长期任务或扩展对话的代理很有用,其中保持从开始时的上下文连贯性很重要,但完整历史记录过大而无法容纳于 $L_{context}$。选择合适的机制最佳的短期记忆策略很大程度上取决于具体的应用:简短、无状态查询: 简单的缓冲区记忆可能就足够了。仅依赖最新上下文的任务: 窗口记忆提供效率。需要较远上下文的长时间对话或多步骤任务: 摘要缓冲区记忆通常是必需的,尽管有其开销。实现这些机制通常涉及创建专用记忆类或使用 LangChain 或 LlamaIndex 等框架提供的抽象。核心思想仍然是管理存储互动历史的保真度与 LLM 上下文窗口 $L_{context}$、成本和延迟要求所施加的限制之间的权衡。有效处理这种权衡对于构建有效的有状态代理至关重要。