Chapter 03 · 记忆
记忆:知识与经验的长期沉淀
第 01 章把记忆定位成认知功能轴上的一格——"把经历写回长期记忆、按需检索回来"的 learning + retrieval 动作。这一章把这一格做深:记忆为什么不等于更大的 context、怎么分层、怎么检索、怎么追踪进度、怎么从失败里学到"绝不贰过"。
本章你将建立的 schema
- context window 为什么不是记忆,以及 CoALA 的四类记忆(工作 / 情景 / 语义 / 程序)。
- 分层记忆的"换页"机制:RAM/disk、水位线驱逐、递归摘要,以及它有损的硬代价。
- Agent 友好的检索:混合检索 + RRF + rerank,以及 just-in-time / agentic search 何时胜过预建向量库。
- 进度追踪为什么要把状态外化到文件;失败日记如何靠外部 oracle 让 agent 不贰过。
3.1记忆导论:context window ≠ memory
记忆不是更大的 context;context window 是每次推理重填的易失工作台(像 RAM),记忆是窗口之外、需主动写入并按需检索回来的持久存储。
面对"agent 老是忘事",最自然的反应是"换个上下文更大的模型"。这个反应抓错了问题。窗口再大也是易失的——每次推理都从空白重填整个 prompt,它本身无状态;而且窗口越大,记得越不准。把"记忆"和"更大的 context"画等号,是这一章要拆掉的第一个错误。
底层机制(比"窗口有限"深一层):Transformer 每个 token 要与其余所有 token 做注意力,n 个 token 产生 n² 对关系,这就是 Anthropic 所谓的"注意力预算(attention budget)"。窗口越长,每对关系分到的预算越薄,于是出现 context rot——召回率随 token 数下降。关键是它是渐进退化而非硬悬崖:没爆窗口不等于读得准。所以"塞满大窗口"≠"记得住",记忆必须是窗口之外、需要主动写入(consolidate)并按需读回(retrieve)的持久存储。CoALA 把这层持久存储切成四类。
| 方案 | 解决什么 | 为什么没选 / 选中 |
|---|---|---|
| 换更大的 context window | 一次能塞更多历史 | 窗口无状态、每次重填,且 token 越多 context rot 越重,长会话照样忘 |
| 一次 RAG 检索 | 把外部知识读进来(只读取) | 只解决"读取",没有 agent 自主决定"记什么/改什么/忘什么"的写入路径 |
| 带写入路径的记忆系统 | 写入 + 存储 + 检索 + 遗忘的完整闭环 | 选中——这条 agent 自主掌控的写入路径,正是记忆区别于 RAG 的硬边界 |
一个 agent 用了 LangGraph 的 checkpointer 持久化状态,开发者以为它"有记忆了"。用户下周开一个新会话回来,agent 却什么都不记得。问题出在哪?
展开答案(先停 10 秒再点)
checkpointer 存的是单个 thread 内的图状态快照(短期、thread-scoped),换一个 thread_id(新会话)就全丢。真正的跨会话长期记忆必须显式用 Store(按 namespace 跨 thread 存)。把 thread 内的状态快照当成了长期记忆,是最常见的混淆——短期记忆(checkpointer / compaction,run 内)和长期记忆(Store / 文件 / memory tool,跨 run)是两套机制、两种成本。
3.2分层保留:给记忆建货架
分层记忆用一套"换页"协议,让固定大小的 context window 假装无限——和操作系统用虚拟内存假装 RAM 无限是同一个把戏。
§3.1 立住了"窗口之外才是记忆"。这一节讲那层窗口外存储怎么组织。MemGPT 给的隐喻是操作系统:context window = RAM,外部存储 = disk,LLM 用函数调用在两层之间"分页",造出"无限上下文"的假象。
失效循环(这是分层记忆最该警惕的故障):摘要把"文件路径、错误信息"这类精确细节压成泛泛的转述丢了 → agent 发现缺了这个细节 → 重新搜索 → 搜索结果重新填满上下文 → 再次触发摘要 → 再次丢掉同一个细节。这个"丢失 → 重搜 → 重填 → 再摘要"的死循环,是摘要式压缩的标志性故障,也是为什么有统计把 65% 的 agent 失败归因于"上下文漂移 / 记忆丢失"而非模型能力不足。
| 方案 | 优势 | 为什么没选 / 选中 |
|---|---|---|
| 直接上 128K/1M 长窗口全塞 | 实现零成本 | context rot:n² 注意力被摊薄、召回随长度渐降,且每 token 全长计费 |
| 硬截断丢最旧消息(sliding window) | 零推理成本 | 丢得彻底,被截掉的信息无法搜回 |
| 分层 + 递归摘要 / compaction | 保留语义连贯、被驱逐内容可搜回 | 选中(主流)——但有损(留存 37%)且是 blocking(每次烧一次推理),所以进度要在触发前写出去 |
| 结构化驱逐(CWL,2026 前沿) | 确定性、无需 LLM、避开有损与幻觉 | 需先把轨迹标注成带依赖的 typed episodes,工程前置成本高、尚未成生产默认 |
# 分层记忆 + 水位线驱逐 + 递归摘要(MemGPT/Letta 核心思路)
MAIN_CTX = [system_instructions, memory_blocks, fifo_queue] # in-context = RAM
RECALL, ARCHIVAL = db(), vector_db() # out-of-context = disk
def step(user_msg):
fifo_queue.append(user_msg)
used = count_tokens(MAIN_CTX)
# 水位线 1:70% 预警,给"LLM 自己"看,让它先抢救要点
if used > 0.70 * WINDOW:
fifo_queue.append(sys("memory pressure:把要长期保留的写入 memory_block 或 archival"))
# 水位线 2:100% 触发 flush(约驱逐半个窗口)
if used >= WINDOW:
evicted = fifo_queue.pop_oldest(n=0.5 * WINDOW) # 不丢弃:转入 recall
RECALL.insert(evicted)
running_summary = llm_summarize(running_summary + evicted) # 有损 + blocking
fifo_queue.prepend(sys(f"[summary so far] {running_summary}"))
# LLM 自编辑 + 换页(工具调用),带 heartbeat 则链式续跑
while True:
out = llm(MAIN_CTX)
if out.tool == "archival_memory_insert": ARCHIVAL.add(out.args) # 写 disk
elif out.tool == "archival_memory_search": page_in(ARCHIVAL.search(out.q))
else: return out.reply
if not out.request_heartbeat: break # 无 heartbeat 则让出控制,等下次外部事件
compaction 不是"顺手压一下":它会停住 agent 跑一次 LLM 推理(blocking),长任务里频繁触发是真实的吞吐瓶颈;而且实测跨会话只留存约 37%、保真 3.4–4.0/5。调压缩参数时,先最大化召回再提精度——Anthropic 原话是"重要性往往要到后来才显现",激进压缩会丢掉当时看着冗余、后来才关键的上下文。
3.3检索增强生成:Agent 友好的外挂知识库
Agent 里的 RAG 不是"生成前一次性检索 top-k 塞进 context",而是 agent 可反复调用、自己决定何时检索的一个工具。
§3.2 的 archival 层是向量库,检索就是从那一层读回内容。先看经典 RAG 的三阶段机制:召回要宽、融合要稳、精排要准。
Agent 化的关键转变:检索不再是"生成前一次性预处理",而是 agent 工具循环里可反复调用的工具——retrieve → read → 自评 → 不够好就改写 query 重检。更激进的 just-in-time 路线根本不预建索引:只存文件路径、query、URL 等轻量标识符,运行时用 grep/glob/read 现查现 load。Anthropic 内测发现,对代码,这种 agentic search "大幅胜出",还省掉了索引同步、陈旧、外部 embedding 供应商的安全负担。
| 方案 | 优势 | 为什么没选 / 选中 |
|---|---|---|
| 纯稠密向量检索 | 懂语义改写 | 漏罕见精确词(代码标识符、SKU、错误码),这类 query 上静默失败 |
| 分数加权融合 BM25 + 向量 | 看着最自然 | 分数尺度不可比,加权平均在生产里漂移失效、要逐库调 alpha |
| 混合 + RRF + cross-encoder rerank | 补盲区、零调参、精排准(失败率 5.7%→1.9%) | 选中(生产标准)——代价是两套索引 + 一层融合 + 二阶段成本 |
| just-in-time / agentic search(grep 现查) | 无索引同步/陈旧、对代码大幅胜出 | 每次现查较慢;对超大非结构化语料的语义模糊查询仍需语义层 |
团队默认"高级 = 语义向量检索一定比关键词 grep 强",准备给代码库建一套 embedding 向量库做检索。按 Anthropic 的实测,这个默认在代码场景为什么反而常常更差?
展开答案(先停 10 秒再点)
三点。其一,代码里大量是精确标识符(函数名、变量、错误码),确定性的关键词工具检索更准;纯向量在这类 query 上静默漏。其二,向量库要索引同步——源码一改就得重嵌入,否则静默返回过时内容;grep 现查永远是最新的。其三,引入外部 embedding 供应商有安全负担。Amazon Science 2026 实测:关键词工具检索能达到 RAG 90%+ 的性能而无需向量库。官方建议默认从 agentic search 起步、性能不够再加语义层。
3.4进度追踪:工作记录与行动轨迹
长任务里真正的 source of truth 不是 context window,而是写在外部的进度文件——窗口是昂贵会衰减的层,文件是便宜持久的层。
前三节都在管"窗口里和窗口外"。这一节回答一个更尖锐的问题:跨越多个上下文窗口、甚至多次 agent 进程的长任务,状态该活在哪里?答案是外部文件,原因有三个可测量的机制。
其一,轨迹有界且会退化。ReAct 循环里的工作状态是不断累积的 Thought/Action/Observation 转录,每轮整段重放,单调增长。ReAct 论文实测:在 HotpotQA/Fever 上 agent 跑 5–7 步后开始生成重复的 thought 和 action——一个自我强化的失败,越长的转录越毒化后续推理。其二,文件是 O(1) 且能扛住重置:写进度到外部文件只花一次写、一次读,不是每轮重新计费整段历史,还能扛住两件会摧毁轨迹的事——compaction 和新的 agent 进程。其三,compaction 有损,所以进度必须在它触发前写出去。下图是 Anthropic 长任务 harness 的循环骨架。
CONTEXT_LIMIT = 200_000
PROGRESS = "claude-progress.txt" # 外部,扛得住重置与 compaction
PLAN = "features.json" # [{id, desc, passes:false, priority}]
def long_horizon_agent():
while not all_done(PLAN):
# 1) RESUME:只从外部文件重建最小工作状态
ctx = read(PROGRESS) + git_log() + read(PLAN) # O(1) 重读
task = pick_highest_priority(unfinished(PLAN))
# 2) ACT:ReAct 循环——轨迹在窗口里,有界、每轮重计费
traj = []
while not task.done:
thought, action = llm(ctx + traj), None
obs = env(thought.action)
traj += [thought, obs]
if tokens(ctx + traj) > 0.7 * CONTEXT_LIMIT: # 在衰减前外化
append(PROGRESS, summarize(traj)); traj = traj[-KEEP:]
# 3) PERSIST:进度写到 context 之外,再让它随意 compact
set(PLAN, task.id, passes=True) # 结构化游标
append(PROGRESS, f"done: {task.id} — {task.summary}")
git_commit(f"feat: {task.id}") # 持久 checkpoint
# 任何崩溃/compaction 后:下一次运行重读 PROGRESS+PLAN,不需要历史
一个 agent 在调试时观察到一个关键细节(某个 API 的报错码),但没把它写进进度文件。随后上下文触发了 compaction。综合环节它给出了错误结论,却没有报任何错。为什么?
展开答案(先停 10 秒再点)
这是稀疏笔记 + 激进压缩 = 静默丢失。agent 确实看见了那个细节,但从没记下来;compaction 把承载它的原始那一轮丢弃了;综合时它只能基于"笔记 + 最近几轮读取"工作,于是结论错了——而且因为没有异常、没有报错,错误是静默的。Anthropic cookbook 的原话:笔记充分则综合可从笔记重建;笔记稀疏则综合会漏掉 agent 看过却没记下的细节。所以进度文件要在压缩阈值之前就最大化召回地写好。
3.5失败日记:错题集,绝不贰过
失败日记 = 反思模块 + 持久化:把失败的根因写成一条可复用的教训,存进情景记忆,下次重试前注入——这是不更新权重的"语言强化学习"。
§3.4 追踪"做到哪了"。这一节追踪"在哪栽过"。Reflexion 的核心是:agent 不更新权重,而是把任务反馈(失败的报错、测试结果)转成一段自然语言"自我反思",写进情景记忆缓冲,下一次带着这些反思重试。它把"学习"放进了上下文,而不是参数——HumanEval 从 GPT-4 的 80% 提到 91% pass@1。下图是这个闭环的五个阶段。
底层机制(这是最反直觉的一点):Huang 等(ICLR 2024)证明,内在自我纠正——模型在没有外部反馈下自己批判自己——往往会降低性能。原因是瓶颈在"生成可靠反馈",而一个把答案做错的模型,通常对自己的错误也判断得同样错。所以失败日记对编码/工具使用(有客观 ground truth:失败的测试、编译报错、HTTP 错误码)强大,对开放式推理(无 ground truth)脆弱。区分失败日记和"边写边反思"(Self-Refine)的,是持久化:Self-Refine 的批评只留在当前上下文、用完即弃;失败日记把它外化存储,让一个全新的上下文窗口(compaction/reset 之后)仍然受益。
def failure_diary_loop(task, max_trials=3):
memory = store.retrieve(task, k=3) # 拉回相似任务的过往失败
for trial in range(max_trials):
trajectory = actor.run(task, hints=memory) # 1. 行动(ReAct/CoT)
signal = oracle.evaluate(trajectory) # 2. 外部 ground truth:
# 跑测试 / 编译 / 看工具报错
if signal.passed:
return trajectory
# 3. 反思:把根因 → 一条可复用的纠正规则
lesson = reflect_llm(trajectory, feedback=signal.error,
prompt="为什么失败?给出一条避免它的具体规则。")
# 4. 存到 context 之外(情景记忆)
store.save(namespace=("failures", task.kind),
value={"obs": task, "result": signal.error, "lesson": lesson})
memory.append(lesson) # 5. 下次重试前注入(有界窗口)
return escalate_to_human(task) # 给上限,避免无限自愈空转
# 关键不变式:signal 必须是外部 oracle。模型无 ground truth 自评,反思会让结果更差。
失败日记会固化错误信念,而非纠正它。"Honest Lying"研究(2026)发现:反思型 agent 会写下一个自信但错误的失败归因("带参数 Y 的 API X 总会报错"),存进记忆后永远绕开那条路径,即使环境一再提供正确解也不再去验证。更多记忆 ≠ 更正确。缓解要靠:让纠正规则可被证据推翻、给信念加衰减、必要时重新取证。
§本章 self-check
先合上教程,把你能想到的答案写在纸上或编辑器里。写完再点开答案对照——直接点开等于把这一节当再读一遍。
- 为什么"换更大的 context window"解决不了记忆问题?用 context rot 解释。
- MemGPT 的两道水位线分别在多少、各触发什么动作?
- 混合检索为什么不能用分数加权平均来融合 BM25 和向量?该用什么?
- 长任务里为什么把状态写到外部文件,而不是信任 context window?给出至少两个机制原因。
- 失败日记的反馈信号为什么必须来自外部 oracle,而不能是模型自评?
答案(先做完再展开)
- 窗口无状态、每次重填;且 n² 注意力让 token 越多召回越差(context rot,渐进退化非硬崖)。"塞得下"不等于"读得准",长会话照样忘。
- 用满 70%:插一条系统消息预警,让 LLM 自己先把要点存进 memory_block 或 archival。用满 100%:触发 flush,按约 50% 窗口的量把最旧消息驱逐到 recall,并生成新的递归摘要。
- 因为 BM25 是无界正分、余弦在 [-1,1],两者尺度不可比,加权平均会漂移失效、还要逐库调 alpha。该用 RRF(倒数排名融合),只看名次、零调参。
- 其一,轨迹有界且会退化(ReAct 5–7 步后开始重复 thought/action)。其二,文件是 O(1) 且能扛住 compaction 和新进程,不必每轮重计费整段历史。其三,compaction 有损,进度写在外部才不被压掉。
- 因为内在自我纠正(无外部反馈)往往降低性能(Huang ICLR 2024)——把答案做错的模型,对自己错误的判断通常同样错。只有外部 oracle(测试/编译/工具报错)能提供可靠的归因信号。
设计一个不会"贰过"也不会"固化错误"的失败记忆
§3.5 给了两个相反的失败:不记失败 → 重复犯错(贰过);记下错误归因 → 永久固化错误信念(记忆虚构)。设计一条策略,既能让 agent 从失败学习,又能在"那条被判死刑的路径其实可行"时自我纠偏。说清你的策略会在哪一个阶段(行动/评估/反思/存储/重试)介入,以及代价是什么。
提示(卡住再展开)
关键在存储和重试两阶段。可考虑:给每条失败教训挂一个置信度与"上次验证时间",重试时不是无条件绕开,而是按概率偶尔重新取证(把 §3.5 的纠正规则做成"可被证据推翻"而非"永久禁令");或者只在有外部 oracle 复核时才把一条教训升级为"硬规则"。代价是额外的探索成本——你要为"偶尔回去撞一次墙以确认墙还在"付 token。这正好呼应了 §3.5 那条不变式:信号必须可被外部世界证伪。