Agent设计模式(五)记忆模式——分层保留(Hierarchical Retention)

笔记摘自:黄佳老师的极客。

概述

记忆不是存储的堆叠,而是经验的治理。

记忆模式组包含四种模式:分层保留(Hierarchical Retention)、检索增强(RAG)、进度追踪(Progress Tracking)、失败日记(Failure Journals)。如果用四个字概括,就是:架、取、录、省。

分层保留,解决的是"架"。做记忆系统,最先要定下来的就是层级。先想清楚分几层、每层放什么,再去写代码。CoALA 给了认知架构的分层,计算机里的内存层级(memory hierarchy )体现了从 L1 缓存到外存的设计,Claude Code 的记忆体系也是热的一层永远在线,温的一层按需加载,冷的一层留在外存,用工具去取。层级不能太少,否则什么都堆在一起;也不能太多,否则每一层都要维护一套晋升和淘汰规则。

检索增强,解决的是"取"。记忆架构里最大的那一层,通常是语义记忆。语义记忆不可能全部塞进上下文窗口,只能在需要时取回来。过去很多系统把 RAG 当成万能入口,什么问题都先检索一遍。到了 Agent 系统里,RAG 要和"按结构直接读""按路径打开文件""按工具查询状态"科学分工,而不是包打天下。

进度追踪,解决的是"录"。它管的是事件,而不是知识。Agent 跑一个长任务,每一步做了什么、为什么这么做、结果如何,这些都是情节记忆(episodic memory) 的原料。开篇那个 auth.py 事故,对症的药就在这一讲。写进度的关键,不是记一行"做了什么",而是写下"为什么这么做""排除了什么""下一步该怎么办""下一步不能做什么"。

失败日记,解决的是"省"。这里的"省",不是节省,而是反省。Agent 每一次踩坑,都是宝贵的学习信号。但这些信号默认会随着会话结束一起消失。失败日记把失败做成一类特殊的记忆,在下次遇到类似情境时主动召回。

这样,梳理一些各种类型的记忆以及所对应的模式:

• 工作记忆 working memory,主要由分层保留来管理。

• 语义记忆 semantic memory,主要由 RAG 和其他检索机制来取回。

• 情节记忆 episodic memory,主要由进度追踪来沉淀。

• 失败记忆 failure memory,可以看作 episodic memory 里最值得主动召回的一类,由失败日记来管理。

• 程序性记忆 procedural memory,则会在后面的 Skill Package 里展开。

现代 Agent 的记忆更像一条生命周期。

context window 上下文窗口

scratchpad 草稿纸

structured trace 结构化追踪

long-term memory 长程记忆

retrieval / replay / forgetting 检索/重放/遗忘

上下文窗口解决"这一刻看见什么"。草稿纸 scratchpad 解决"这一刻正在怎么算"。结构化追踪解决"这一轮到底发生了什么"。长程记忆解决"哪些经验值得跨会话留下来"。检索、重放、遗忘解决"下一次要不要把它拿回来,以及什么时候该让它过期"。

上一轮真正值钱的判断应该先落到 scratchpad,再被蒸馏成结构化进度。

scratchpad.write({

"task": "拆分 auth.py",

"current_finding": "UserSession 与 PermissionCache 循环依赖,跨 4 个文件",

"cycle_path": [

"auth.py",

"session.py",

"permission_cache.py",

"permissions.py",

"auth.py",

],

"tested_attempt": "先移动 UserSession 到 session.py",

"observed_failure": "43 个 auth regression tests 失败",

"candidate_decision": "先抽共享 types.py",

"do_not_do_next": "不要先移动 UserSession",

"needs_persist": True,

})

任务收束之后,再把能复用、能交接、能审计的部分写入长期记忆。

memory.write({

"goal": "把 auth.py 拆成三个模块",

"finding": "UserSession 与 PermissionCache 存在循环依赖,跨 4 个文件",

"cycle_path": "auth.py -> session.py -> permission_cache.py -> permissions.py -> auth.py",

"decision": "先抽出共享的 types.py,再继续拆分 auth.py",

"do_not_do": "不要把 UserSession 作为第一步移动,已验证会触发 43 个测试失败",

"evidence": "auth regression suite, Session 3 scratchpad",

"next_step": "下一轮第一步:建 types.py,不要先碰 UserSession",

})

同样一轮推理,摘要式进度记录没有完全丢信息,但它丢掉了最值钱的可执行约束。新版记法先把中间判断放到工作台,再把能复用的部分沉淀到长期记忆。这和只写"接口调用失败"却没有留下 error code 很像。人回头看,大概知道发生过什么;下一轮 Agent 接手时,却少了最关键的诊断入口。还有一点,就是不要把模型原始的思考过程(raw chain-of-thought )当成业务记忆。企业系统需要的是可验证、可审计、能续接的判断,而不是模型内部念头的原样留档。

各种类型的记忆

Agent 的记忆其实需要解决三个传统软件用不同机制分别处理过的问题。

  • 第一是状态持久化。Agent 被打断之后,要记得自己刚才在干什么。这对应传统软件里的数据库状态、session 和 checkpoint。

  • 第二是知识检索。Agent 要访问的信息,远超上下文窗口能装下的量,所以要有地方存,也要有办法取。这对应数据库、搜索索引和文档系统。

  • 第三是经验累积。Agent 应该从过去的执行里学到东西,下次少踩坑。这一点,传统软件里没有完全等价的机制。最接近的是测试套件和事故复盘:它们都在把过去踩过的坑固化下来,让系统以后不要重犯。

分层保留

分层保留,就是把 Agent 的记忆按作用域、生命周期和可信度切成多层,让每一层有独立的加载策略、写入规则、淘汰规则和 token 预算。做分层时,不要一上来就问"到底分三层还是五层"。更稳妥的做法,是先问五个坐标。

  • 第一,这条记忆的作用域是什么。它属于组织、项目、用户、任务、会话,还是当前一轮?

  • 第二,它应该活多久。是几分钟、一个session、一个项目周期,还是长期有效?

  • 第三,它的权威来源是谁。是人写的、工具返回的、框架生成的,还是模型推断出来的?

  • 第四,它有没有证据。是来自测试结果、代码路径、用户确认,还是只是模型的一次猜测?

  • 第五,它要占多少上下文预算。它应该常驻 context,还是只在需要时被工具取回?

    from dataclasses import dataclass, field
    from datetime import datetime, timedelta
    from enum import Enum
    from typing import Any

    class MemoryLayer(Enum):
    POLICY = "policy"
    PROJECT = "project"
    USER = "user"
    TASK = "task"
    SCRATCHPAD = "scratchpad"

    class MemorySource(Enum):
    HUMAN = "human"
    TOOL = "tool"
    AGENT_INFERENCE = "agent_inference"
    VERIFIED_TRACE = "verified_trace"
    FAILURE_REVIEW = "failure_review"

    @dataclass
    class MemoryEntry:
    key: str
    value: Any
    layer: MemoryLayer
    source: MemorySource
    evidence_refs: list[str] = field(default_factory=list)
    confidence: float = 1.0
    token_estimate: int = 0
    valid_from: str = field(default_factory=lambda: datetime.utcnow().isoformat())
    valid_until: str | None = None
    created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
    last_accessed_at: str | None = None

    复制代码
      def is_expired(self) -> bool:
          if self.valid_until is None:
              return False
          return datetime.utcnow() > datetime.fromisoformat(self.valid_until)
    
      def is_verified(self) -> bool:
          return bool(self.evidence_refs) or self.source in {
              MemorySource.HUMAN,
              MemorySource.TOOL,
              MemorySource.VERIFIED_TRACE,
              MemorySource.FAILURE_REVIEW,
          }

    @dataclass
    class LayerPolicy:
    layer: MemoryLayer
    token_budget: int
    ttl: timedelta | None
    allow_agent_write: bool
    require_evidence: bool
    backend: str

    class HierarchicalMemory:
    def init(self) -> None:
    self.entries: dict[str, MemoryEntry] = {}
    self.policies = {
    MemoryLayer.POLICY: LayerPolicy(
    layer=MemoryLayer.POLICY,
    token_budget=1200,
    ttl=None,
    allow_agent_write=False,
    require_evidence=True,
    backend="managed_file",
    ),
    MemoryLayer.PROJECT: LayerPolicy(
    layer=MemoryLayer.PROJECT,
    token_budget=3000,
    ttl=None,
    allow_agent_write=False,
    require_evidence=True,
    backend="git_file",
    ),
    MemoryLayer.USER: LayerPolicy(
    layer=MemoryLayer.USER,
    token_budget=1500,
    ttl=None,
    allow_agent_write=True,
    require_evidence=True,
    backend="postgres",
    ),
    MemoryLayer.TASK: LayerPolicy(
    layer=MemoryLayer.TASK,
    token_budget=5000,
    ttl=timedelta(days=7),
    allow_agent_write=True,
    require_evidence=True,
    backend="checkpointer",
    ),
    MemoryLayer.SCRATCHPAD: LayerPolicy(
    layer=MemoryLayer.SCRATCHPAD,
    token_budget=2500,
    ttl=timedelta(hours=2),
    allow_agent_write=True,
    require_evidence=False,
    backend="runtime_state",
    ),
    }

    复制代码
      def write(self, entry: MemoryEntry) -> None:
          policy = self.policies[entry.layer]
    
          if not policy.allow_agent_write and entry.source == MemorySource.AGENT_INFERENCE:
              raise ValueError(f"Agent cannot write directly to {entry.layer.value}")
    
          if policy.require_evidence and not entry.is_verified():
              raise ValueError(f"{entry.layer.value} memory requires evidence")
    
          self.entries[entry.key] = entry
    
      def propose_from_scratchpad(
          self,
          entry: MemoryEntry,
          target_layer: MemoryLayer,
      ) -> MemoryEntry:
          if entry.layer != MemoryLayer.SCRATCHPAD:
              raise ValueError("Only scratchpad entries can be promoted")
    
          return MemoryEntry(
              key=entry.key,
              value=entry.value,
              layer=target_layer,
              source=MemorySource.VERIFIED_TRACE,
              evidence_refs=entry.evidence_refs,
              confidence=entry.confidence,
              token_estimate=entry.token_estimate,
          )
    
      def assemble_context(self) -> list[MemoryEntry]:
          selected: list[MemoryEntry] = []
    
          for layer in [
              MemoryLayer.POLICY,
              MemoryLayer.PROJECT,
              MemoryLayer.USER,
              MemoryLayer.TASK,
              MemoryLayer.SCRATCHPAD,
          ]:
              budget = self.policies[layer].token_budget
              used = 0
    
              layer_entries = [
                  entry
                  for entry in self.entries.values()
                  if entry.layer == layer and not entry.is_expired()
              ]
    
              layer_entries.sort(
                  key=lambda entry: (
                      entry.confidence,
                      entry.last_accessed_at or entry.created_at,
                  ),
                  reverse=True,
              )
    
              for entry in layer_entries:
                  if used + entry.token_estimate > budget:
                      continue
    
                  selected.append(entry)
                  used += entry.token_estimate
                  entry.last_accessed_at = datetime.utcnow().isoformat()
    
          return selected
    
      def health_report(self) -> dict[str, Any]:
          return {
              "layers": {
                  layer.value: {
                      "backend": policy.backend,
                      "token_budget": policy.token_budget,
                      "ttl_seconds": None if policy.ttl is None else policy.ttl.total_seconds(),
                      "allow_agent_write": policy.allow_agent_write,
                      "require_evidence": policy.require_evidence,
                      "entry_count": sum(
                          1 for entry in self.entries.values() if entry.layer == layer
                      ),
                  }
                  for layer, policy in self.policies.items()
              }
          }