在《浅谈 Agent 的 Memory 发展》里我聊过:当 LLM 从语言模型变成 Agent 的大脑,上下文管理就成了绕不开的难题。交互不断累积,窗口迟早会满,总得丢掉些东西。今天主流的做法无非两种——把最老的文本截断、对保留下来的部分重新 prefill;或者按重要性对 token 做选择性驱逐。

这篇想说的是第三条路,而且免训练:直接把"全量上下文 prefill 出来的 KV cache"切一个子集拿来用,往往比"从保留下来的同一段文本重新算"更准。注意两者保留的文本完全一样,唯一的区别是 KV 的来源。

为什么会这样?因为这两个东西根本不是一回事。重算出来的 cache 只见过那个被饿瘦的子集;而切出来的 cache 是在"完整上下文都在场"时算出来的,它带着你已经删掉的那部分内容的痕迹。

需要先说清楚:cache 能 ≥ fresh 这件事本身并不新鲜——eviction 文献里"更小的 cache 反而更好"早有记载。但学术界把这个观察转化成质量收益的方式,几乎清一色是"加训练":学一个压缩器、学一组保留门、学哪些是检索头。我想说的恰恰相反——杠杆从来不在训练里。真正缺的是三样东西:支配它的变量、背后的因果机制,以及由机制直接导出的免费策略。

一、支配变量:coverage

“cache 比 fresh 好"是有条件的,条件就是 coverage(你保留了原始上下文的多少)。把 coverage 扫一遍,画出 reuse − fresh 的准确率差:

KV cache 复用 vs 重算的 coverage 曲线

三个数据集、两个模型家族(Qwen 与 Mistral),同一个形状:低覆盖处有一个悬崖(cache 反而更差),中段是一片红利区(cache 更好),到 100% 严格归零。

那个归零点不是巧合,是恒等门(identity gate):满覆盖时什么都没删,两条路是同一次计算,差异必须为零——它确实精确为零,一个样本都不差。这条性质很重要,它把"机制"和"实现 bug"区分开:任何差异只在你删了东西时出现,删的东西归零时差异也精确归零。

中间发生的事,是两股力量在抢一个旋钮:

  • 记忆红利(+):保留下来的 KV 行是在完整上下文下算出来的,fresh 只能看那个饿瘦的子集。把样本按"答案证据段是否被删"分层,红利恰好集中在证据被删的那一层——因为这时 cache 是唯一还残留着证据痕迹的地方。
  • 退化代价(−):覆盖压得太低,cache 退化成一个病态前缀,开始复读、不肯停、答非所问。这个代价集中在**“证据还在、但别的都没了”**的低覆盖层——也正是 fresh 手里什么都不缺的那一层。

交叉点以下代价占上风,以上红利占上风,满覆盖两者都消失。视频那条线甚至没有悬崖(帧之间高度冗余,永远饿不死),反过来印证了悬崖的本质就是"信息饥饿”,而不是什么位置编码的玄学。

实务结论这里就已经有用了:别过度驱逐(悬崖是真的,而且就长在"证据还在、别的没了"的地方);而在中段,复用不是重算的劣化近似——它就是更好,且免费。

二、它到底记得什么

coverage 曲线说明 cache 知道一些 fresh 不知道的东西。最干净的验证方式,是把证据直接删掉,看谁还能答。

文本版:多跳 QA,物理删除答案所在的那一段。fresh 从没见过证据,reuse 手里只剩残留的痕迹。结果 reuse 仍能把整条多跳推理链补出来,落到那个被删掉的确切答案上。一个真实例子(删掉了含"1929 年 2 月 11 日"的那段):

fresh: “这个问题似乎基于某种误解……并不存在直接联系……”

reuse: “《Princeps Pastorum》的作者是教皇约翰二十三世,他在梵蒂冈城去世。梵蒂冈城于 1929 年 2 月 11 日因《拉特兰条约》签署而成为独立国家……”

跨模态版更狠:把带音轨的视频 prebake,然后物理删掉所有音频 token 的 KV、只留视频行,再问依赖音频的问题。带音频 prebake 的视频 cache,显著优于不带音频的同位置 prebake——而唯一的差别只是"prebake 时音频在不在场"。视频 token 的 KV,把音频信息吸收进去了。

三、机制:下游注意力痕迹

那么信息物理上存在哪里?

假设:full prefill 时 attention 是因果的,排在证据之后的段落 attend 过它,于是它们的 K/V 是证据的函数。删掉证据行,这部分派生信息留在你保留的行里;而 fresh 从来没有过。如果这是对的,恢复量应该由一个纯位置量控制:prebake 时有多少保留 token 排在证据下游。

因果实验(关键在这里,不是相关性):同一个问题、同一个保留集,只移动证据段在 prebake 时的位置,然后删掉它。gold-first = 每个保留段都 attend 它(痕迹最大);gold-last = 没有任何段能 attend 它(痕迹为零,被因果 mask 挡住)。结果干净利落:痕迹最大化时恢复最强(+2.4 到 +5.8 pp,五个"数据集 × 家族"格子全部统计显著),痕迹清零时恢复塌成零。换句话说,把痕迹拿掉,reuse 和 recompute 就退化成同一个模型

排除 lost-in-the-middle:有人会怀疑 gold-first 占的是 primacy / attention-sink 的便宜。于是把证据放到一个随机的中间位置——只要它下游还有 7–8 个段落,恢复就和 gold-first 一样强(+4.9 pp)。预测变量是"下游有多少东西 attend 过它",不是"它在不在第一个位置"。

它存的是内容,还是只是先验? 最尖锐的反驳是:也许痕迹只是把模型推向它本来就知道的事实(梵蒂冈 1929 谁都知道)。反事实探针:prebake 之前,把答案年份改成一个伪造的、上下文别处都不出现的值,再删段提问。结果——cache 复现出那个伪造年份,fresh 从不(6:0,p = .031),而真实年份那条通道无显著差异。先验论输了,内容论赢了。

不过带宽很低:每个可读样本大约只有 1–2% 的逐字带宽。它存的是 gist 和实体绑定关系,不是逐字转录——常常年份对了、月日错了。有一条生成甚至一本正经地引用了一个不存在的段落:“这一点有上文佐证。”

四、落到 Agent:滚动上下文

这才是我最看重的落地场景,也正好接上 memory 那篇。Agent 的上下文是滚动的:对话不断累积,窗口满了就得丢。上面的结果把今天的选项重排了一遍:

  1. 丢最老的,而且要切 cache、不要重算。 keep-late 本质就是"截断最老的轮次"——但你保留的是那些 KV 行本身,而不是从保留文本重算。这些行是在被删轮次还在场时烤出来的,带着吸收进去的痕迹;从文本重新 prefill 恰恰是唯一会把痕迹弄丢的做法。“上下文变了,保险起见重算一遍"这个直觉,选的偏偏是又慢又差的那条路。

  2. 选择性的中段驱逐,需要一个还不存在的 query。 重要性打分(H2O / SnapKV)是 query 驱动的;而 Agent 循环里未来的 query 是未知的。没有 query 时,recency(保留靠后的)是唯一可用的信号——而它恰好是痕迹最优的。(这也顺带给 StreamingLLM 的滑动窗口一个机制层面的辩护:窗口不只是工程上方便,它本就是痕迹最丰富的子集形状。)

  3. “先总结再驱逐"是一种工程化的痕迹。 在旧轮次还在场时生成总结、append 进去、删旧轮次——总结 token 的 KV 在 bake 时吸收了全量上文,和任何下游 token 一样。自然痕迹是免费但低带宽的(~1–2% 逐字),总结则是花 decode 代价换来的高带宽痕迹。而且我们对带宽的测量直接告诉你总结该写什么:写自然通道丢掉的(逐字细节、长链条),别浪费在它已经留住的(gist)上。

一个诚实的 gap:本文所有实验都是"单次 bake、单次切”。而滚动场景是 bake → 驱逐 → 续写 → 再驱逐,痕迹在多轮里到底是复利还是衰减,目前没人测过——这是我接下来要做的实验。

五、边界(一些诚实话)

机制只有配上它的符号条件才有用,并不是处处都赢。在 HotpotQA 这种 2-hop、容易走捷径、且保留了强干扰段的情况下,低覆盖时 cache 反而会输几个点——因为痕迹会放大 prebake 时显著的东西,包括一个被你留下来的、话题相邻的干扰项。

统一的判据是:痕迹放大"完整上下文里显著的东西”。当被删内容正是你需要的信号、任务又抗捷径时,它帮忙;当强干扰留着、覆盖又低、任务还好走捷径时,它添乱。符号是 (对被删证据的需求) × (干扰项的邻近度),不是 hop 数的故事。

还有一条硬边界:这一切都要求统一上下文 prebake。在普通的 corpus-RAG 里,chunk 是各自独立烤的,没有全上下文 prefill,也就没有痕迹——所以本文对通用检索式 RAG 不做任何准确率声明。但符合条件的场景其实很常见:单文档多轮问答、流式驱逐,以及反复对同一段长历史取子集的 Agent 循环。

结语

从全量 prefill 切出来的 KV cache,不是重算的劣化近似;它是另一种东西——它记得你删掉的一部分。质量由 coverage 支配,由一条因果确立的下游注意力痕迹来解释,被"显著性放大"效应所约束,并能被免训练的策略直接利用。

给工程师的一句话总结:当保留集发生变化时,切旧 cache 既比重新 prefill 便宜、又比它更准——除非掉进低覆盖悬崖,而悬崖在运行时是可检测的。

完整论文(借用 ICLR 模板排版的 preprint):下载 PDF;全部实验代码:github.com/oklen/sprag