笔记摘自:黄佳老师的极客。
概述
记忆不是存储的堆叠,而是经验的治理。
记忆模式组包含四种模式:分层保留(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",
"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 Anyclass 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 = Nonedef 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: strclass 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() } }