短期与长期记忆

M2
深入解析 · 记忆与上下文工程

短期与长期记忆:工作上下文及其背后的存储。

智能体需要两套截然不同的记忆系统,它们的生命周期、成本和失效模式各不相同。短期记忆是提示内的工作上下文——快、被完全注意到、易逝。长期记忆是外部存储——持久、容量大,但只有当正确的切片被检索回来时才有用。把两者混为一谈是最常见的记忆架构错误。

STEP 1

两套系统,源自认知类比——但要谨慎使用。

人类类比是一个有用的起点框架,而非蓝图。工作记忆持有少数几个正在被主动操作的条目;长期记忆庞大、持久,通过线索回忆来访问。对智能体而言,这个映射是具体的:

  • 短期记忆 = 上下文窗口。当前系统提示、最近几轮、新鲜的工具结果、草稿区。它在每个令牌上都被注意力以完整保真度读取。请求一返回它就消失——除非你把它写到某处,否则什么都不会留下。
  • 长期记忆 = 外部存储。向量索引、键值库、图、或纯文件。它跨轮、跨会话、跨进程重启而存活。模型"看不见"它;必须查询它,并把结果拼接进短期记忆,才会产生任何效果。

决定性的区别不是容量或持久性——而是注意力。短期记忆是模型本轮真正计算的对象。长期记忆在检索把它的某个切片提升进短期记忆之前,是惰性的。

STEP 2

短期记忆:什么才配占一个位置。

工作上下文是你拥有的最稀缺资源(见 context-budgeting)。把准入当作一个有成本的决策。一条实用的策略:内容只有在下一步决策需要它时才赢得一个工作集位置,而不是仅仅与任务相关

具体而言,典型一轮的工作集应当持有:

  • 任务/当前用户目标——始终。
  • 最近 k 轮逐字内容——对于进行中的推理,时近性压过相关性。
  • 未闭合的回路:待处理的工具调用、未完成的子目标、未解决的问题。
  • 一个由智能体自身标记为需持久保留的决策与事实组成的、显式维护的小草稿区。

其余一切——已解决的子任务、过期的工具输出、早于 k 轮的轮次——都是被驱逐到长期记忆的候选。

# memory/working_set.py
from collections import deque

class WorkingSet:
    def __init__(self, max_turns: int = 12):
        self.turns: deque = deque(maxlen=max_turns)
        self.scratchpad: list[str] = []  # durable, agent-curated

    def add_turn(self, turn: dict) -> dict | None:
        evicted = self.turns[0] if len(self.turns) == self.turns.maxlen else None
        self.turns.append(turn)
        return evicted   # caller persists it to long-term memory

    def note(self, fact: str) -> None:
        # Agent explicitly promotes a fact it must not forget.
        self.scratchpad.append(fact)

驱逐路径是承重的细节。从 deque 末端丢掉一轮而不持久化,是失忆;在写入一条持久痕迹到长期记忆之后再丢掉它,是"可回忆的遗忘",这才是你想要的。

STEP 3

长期记忆:何时写,何时回忆。

两个独立的决策,而大多数糟糕的记忆系统把它们做反了——什么都写,什么都回忆。

何时写

不要不加区分地持久化原始记录——那是一场缓慢的泄漏,会污染未来的检索。当内容具有超出本轮的持久价值时才写:

  • 关于用户、环境或任务的稳定事实("部署要通过预发布检查这一关")。
  • 结果及其原因——尝试了什么、什么有效、什么失败及为何失败(情景记忆)。
  • 智能体推导出的可复用流程(程序性记忆)。
  • 用户明确要求记住的指令。

何时回忆

回忆由当前需要触发,而非按计划表。在一轮开始前,从活跃目标构造一个检索查询,只拉取通过相关性阈值的前几条。回忆勉强相关的记忆并非免费——它消耗工作集预算,并加入会可度量地损害推理的干扰项。

# memory/longterm.py
class LongTermMemory:
    def __init__(self, store, embedder):
        self.store = store          # vector / kv / graph backend
        self.embed = embedder

    def write(self, text: str, kind: str, meta: dict) -> None:
        if not self._is_durable(text, kind):
            return                     # write gate: skip ephemera
        self.store.upsert(
            vector=self.embed(text),
            payload={"text": text, "kind": kind, **meta,
                     "ts": now()},
        )

    def recall(self, goal: str, k: int = 5,
               min_score: float = 0.35) -> list[dict]:
        hits = self.store.search(self.embed(goal), k=k * 3)
        hits = [h for h in hits if h.score >= min_score]
        return hits[:k]      # threshold THEN truncate

最常见的长期记忆失效:无界的写入路径。每一轮都被追加,存储被几乎重复的记录噪声塞满,检索 recall@k 崩塌,智能体运行得越久反而越差。写入门控不是可选项。

STEP 4

提升/降级循环。

健康的智能体持续在两套系统之间搬运信息。这个循环,每轮一次:

1. RECALL   query long-term with current goal → candidate memories
2. PROMOTE  splice top-k (above threshold) into working set
3. REASON   model acts over working set, produces new turn
4. DEMOTE   evict oldest / resolved working-set items
5. WRITE    persist demoted items that pass the durability gate
            → loop

这与 context-budgeting 中的缺页换入/换出循环相同,现在在写入侧加了显式的持久性门控,在读取侧加了相关性阈值。把这两个过滤器做对,记忆栈的其余部分——类型、存储、压缩——都是调优。做错了,没有任何向量数据库能救你。

独立测试这两套系统。对于短期:智能体在一个长任务中是否始终把正确的东西留在窗口内?对于长期:给定一条 50 轮前写入的已知事实,相关时它是否被回忆出来?不同的指标,不同的修复——在 evaluating-memory 中详述。