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 把这层持久存储切成四类。

短期·RAM 经历 知识 怎么做事 Agent 记忆 写入 → 检索 两个动作 工作记忆 · 短期 活在 context window 里 情景记忆 episodic 过往经历·few-shot 语义记忆 semantic 事实·偏好·结论 程序记忆 procedural 技能·规则·agent 代码
图 3.1CoALA 四类记忆:只有工作记忆活在窗口里,其余三类都在窗口外、需主动读写。注意:改程序记忆(改 agent 代码 / system prompt 规则)比改情景、语义记忆危险得多——记忆不只是数据,程序记忆就是行为本身,写错会让 agent 行为失常。
表 3.1 · 需求到底要的是哪一种
方案解决什么为什么没选 / 选中
换更大的 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 用函数调用在两层之间"分页",造出"无限上下文"的假象。

RAM DISK in-context(实际进 prompt 的 token) 系统指令 + memory blocks + FIFO 队列 recall 落盘对话历史·可搜 archival 知识库·向量检索 驱逐·递归摘要(有损) 工具调用换入 水位线:用满 70% → 插系统消息预警;用满 100% → flush,驱逐约 50% 窗口 实测代价:摘要跨会话留存仅 37%、保真 3.4–4.0/5(约 1/5 事实被扭曲或丢失)
图 3.2三层货架:RAM 装活跃 token,两层 disk 装冷数据,靠驱逐(向下)和换入(向上)搬运。注意:驱逐不是删除——被驱逐的消息进 recall 仍可搜回,做的是"换出到 disk"。但递归摘要是有损的,精确细节(文件路径、错误码)最容易被压掉,而那往往正是后面要用的。

失效循环(这是分层记忆最该警惕的故障):摘要把"文件路径、错误信息"这类精确细节压成泛泛的转述丢了 → agent 发现缺了这个细节 → 重新搜索 → 搜索结果重新填满上下文 → 再次触发摘要 → 再次丢掉同一个细节。这个"丢失 → 重搜 → 重填 → 再摘要"的死循环,是摘要式压缩的标志性故障,也是为什么有统计把 65% 的 agent 失败归因于"上下文漂移 / 记忆丢失"而非模型能力不足。

表 3.2 · 窗口快满了,怎么腾地方
方案优势为什么没选 / 选中
直接上 128K/1M 长窗口全塞实现零成本context rot:n² 注意力被摊薄、召回随长度渐降,且每 token 全长计费
硬截断丢最旧消息(sliding window)零推理成本丢得彻底,被截掉的信息无法搜回
分层 + 递归摘要 / compaction保留语义连贯、被驱逐内容可搜回选中(主流)——但有损(留存 37%)且是 blocking(每次烧一次推理),所以进度要在触发前写出去
结构化驱逐(CWL,2026 前沿)确定性、无需 LLM、避开有损与幻觉需先把轨迹标注成带依赖的 typed episodes,工程前置成本高、尚未成生产默认
tiered_memory.py Python
# 分层记忆 + 水位线驱逐 + 递归摘要(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 则让出控制,等下次外部事件
陷阱 · 摘要是有损且 blocking 的

compaction 不是"顺手压一下":它会停住 agent 跑一次 LLM 推理(blocking),长任务里频繁触发是真实的吞吐瓶颈;而且实测跨会话只留存约 37%、保真 3.4–4.0/5。调压缩参数时,先最大化召回再提精度——Anthropic 原话是"重要性往往要到后来才显现",激进压缩会丢掉当时看着冗余、后来才关键的上下文。

3.3检索增强生成:Agent 友好的外挂知识库

Agent 里的 RAG 不是"生成前一次性检索 top-k 塞进 context",而是 agent 可反复调用、自己决定何时检索的一个工具。

§3.2 的 archival 层是向量库,检索就是从那一层读回内容。先看经典 RAG 的三阶段机制:召回要宽、融合要稳、精排要准。

① 召回(各取 top-200,要宽) ② 融合 ③ 精排 query 向量 · HNSW 懂语义改写 BM25 倒排 抓罕见精确词 RRF 融合 按排名·零调参 cross-encoder 联合编码精排 → top-k → LLM
图 3.3经典 RAG 三阶段:召回宽(top-200)、融合稳(RRF)、精排准(cross-encoder)。注意:融合不能用分数加权平均——BM25 是无界正分、余弦在 [-1,1],尺度不可比;RRF 干脆扔掉分数只用名次,反而更稳、零调参。

Agent 化的关键转变:检索不再是"生成前一次性预处理",而是 agent 工具循环里可反复调用的工具——retrieve → read → 自评 → 不够好就改写 query 重检。更激进的 just-in-time 路线根本不预建索引:只存文件路径、query、URL 等轻量标识符,运行时用 grep/glob/read 现查现 load。Anthropic 内测发现,对代码,这种 agentic search "大幅胜出",还省掉了索引同步、陈旧、外部 embedding 供应商的安全负担。

表 3.3 · 给 agent 配检索
方案优势为什么没选 / 选中
纯稠密向量检索懂语义改写漏罕见精确词(代码标识符、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 的循环骨架。

① RESUME 读 progress + git log ② ACT ReAct,70% flush 到文件 ③ PERSIST passes=True + git commit 崩溃 / compaction 后从文件续跑,无需历史
图 3.4长任务循环:状态活在 git + 进度文件里,不在任何单个 agent 的 context。注意:返回箭头是关键——任何一次崩溃或 compaction 之后,新进程只靠读外部文件就能续跑,所以一个 200+ 功能的项目能跨许多个上下文窗口完成,没有哪个 agent 需要看见全部历史。
long_horizon_agent.py Python
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。下图是这个闭环的五个阶段。

① 行动 Actor ReAct/CoT 轨迹 ② 外部 oracle 测试/编译/工具报错 ③ 反思 根因 → 纠正规则 ④ 存情景记忆 窗口外·可检索 ⑤ 重试:注入教训
图 3.5失败日记闭环:行动 → 评估 → 反思 → 存储 → 重试注入。注意:第 ② 步是整个闭环的决定性环节——它必须是外部 oracle(测试/编译/工具报错)。如果换成模型给自己打分(无外部信号),自我反思会让推理结果更差,而不是更好。

底层机制(这是最反直觉的一点):Huang 等(ICLR 2024)证明,内在自我纠正——模型在没有外部反馈下自己批判自己——往往会降低性能。原因是瓶颈在"生成可靠反馈",而一个把答案做错的模型,通常对自己的错误也判断得同样错。所以失败日记对编码/工具使用(有客观 ground truth:失败的测试、编译报错、HTTP 错误码)强大,对开放式推理(无 ground truth)脆弱。区分失败日记和"边写边反思"(Self-Refine)的,是持久化:Self-Refine 的批评只留在当前上下文、用完即弃;失败日记把它外化存储,让一个全新的上下文窗口(compaction/reset 之后)仍然受益。

failure_diary.py Python
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 自评,反思会让结果更差。
陷阱 · 记忆虚构(memory confabulation)

失败日记会固化错误信念,而非纠正它。"Honest Lying"研究(2026)发现:反思型 agent 会写下一个自信但错误的失败归因("带参数 Y 的 API X 总会报错"),存进记忆后永远绕开那条路径,即使环境一再提供正确解也不再去验证。更多记忆 ≠ 更正确。缓解要靠:让纠正规则可被证据推翻、给信念加衰减、必要时重新取证。

§本章 self-check

先合上教程,把你能想到的答案写在纸上或编辑器里。写完再点开答案对照——直接点开等于把这一节当再读一遍。

  1. 为什么"换更大的 context window"解决不了记忆问题?用 context rot 解释。
  2. MemGPT 的两道水位线分别在多少、各触发什么动作?
  3. 混合检索为什么不能用分数加权平均来融合 BM25 和向量?该用什么?
  4. 长任务里为什么把状态写到外部文件,而不是信任 context window?给出至少两个机制原因。
  5. 失败日记的反馈信号为什么必须来自外部 oracle,而不能是模型自评?
答案(先做完再展开)
  1. 窗口无状态、每次重填;且 n² 注意力让 token 越多召回越差(context rot,渐进退化非硬崖)。"塞得下"不等于"读得准",长会话照样忘。
  2. 用满 70%:插一条系统消息预警,让 LLM 自己先把要点存进 memory_block 或 archival。用满 100%:触发 flush,按约 50% 窗口的量把最旧消息驱逐到 recall,并生成新的递归摘要。
  3. 因为 BM25 是无界正分、余弦在 [-1,1],两者尺度不可比,加权平均会漂移失效、还要逐库调 alpha。该用 RRF(倒数排名融合),只看名次、零调参。
  4. 其一,轨迹有界且会退化(ReAct 5–7 步后开始重复 thought/action)。其二,文件是 O(1) 且能扛住 compaction 和新进程,不必每轮重计费整段历史。其三,compaction 有损,进度写在外部才不被压掉。
  5. 因为内在自我纠正(无外部反馈)往往降低性能(Huang ICLR 2024)——把答案做错的模型,对自己错误的判断通常同样错。只有外部 oracle(测试/编译/工具报错)能提供可靠的归因信号。
进阶挑战 · 刚好够不着

设计一个不会"贰过"也不会"固化错误"的失败记忆

§3.5 给了两个相反的失败:不记失败 → 重复犯错(贰过);记下错误归因 → 永久固化错误信念(记忆虚构)。设计一条策略,既能让 agent 从失败学习,又能在"那条被判死刑的路径其实可行"时自我纠偏。说清你的策略会在哪一个阶段(行动/评估/反思/存储/重试)介入,以及代价是什么。

提示(卡住再展开)

关键在存储和重试两阶段。可考虑:给每条失败教训挂一个置信度与"上次验证时间",重试时不是无条件绕开,而是按概率偶尔重新取证(把 §3.5 的纠正规则做成"可被证据推翻"而非"永久禁令");或者只在有外部 oracle 复核时才把一条教训升级为"硬规则"。代价是额外的探索成本——你要为"偶尔回去撞一次墙以确认墙还在"付 token。这正好呼应了 §3.5 那条不变式:信号必须可被外部世界证伪。