前阵子组会讨论的时候,A同学把一篇标题很长的论文丢到群里:《Conditional Memory via Scalable Lookup...》——简称就是Engram。他当时只留了一句话:“你们有没有想过,上下文窗口其实不是记忆,它只是一块能随时被扔掉的便签纸。”
这句话我记了挺久。因为我这几年做产品的记忆模块,踩过很多“把长上下文当记忆用”的坑。这次把论文完整读完后,我把它的核心设计、和传统KV Cache外推方案的差异,以及它落到工程里可能会遇到的坎,整理成一篇偏实战的解读。如果你也在做智能助手、RAG系统,或者正在给本地模型加长期记忆,这篇文章应该能帮你省不少试错时间。
1. 读这篇论文之前,先重新理解“上下文窗口”
论文的立论点非常直接:当前大模型引以为傲的长上下文,本质上只是“把更多内容塞进KV Cache”,并不等于模型具备了记忆能力。这两件事的区别,我是在做某跨平台系统的埋点对话功能时才彻底想通的。
1.1 长上下文只是“能看”,不是“记得”
很多人对记忆的第一印象是:模型越长上下文越好,我给它10万token,它就能记住10万token里的用户偏好。
实际根本不是这样。上下文窗口只是给模型一个“可读取的工作区”。你往工作区里丢多少字,模型在推理的时候就能回看多少字。但它读了一遍之后,窗口一关,通道里的KV Cache被释放,之前的信息就彻底消失了。它不像人的记忆,会留下一个长期可复用的痕迹。第二天用户再来,如果想让它记住昨天的偏好,不好意思,你得重新把昨天的摘要或者全部记录再喂一遍。
这带来一个很实际的问题:工作区是珍贵的。你不可能无限扩容,就算服务端内存够,KV Cache也是随序列长度二次增长的——这几乎是所有长文本方案的共同命门。所以现在的长上下文方案,我是说那些靠RoPE外推、滑动窗口、稀疏注意力去撑窗口的路线,本质上都是在跟同一个物理限制做对抗:如何在有限的显存里,让模型“假装”看到了更多内容。
1.2 长期记忆的瓶颈不在存储,而在“被想起”
那如果我们换一条路,把历史信息放到外部存储,比如向量数据库,是不是就是记忆了?
也不行。RAG做了一个眼下看起来最实用的折衷:先检索,再拼接,让模型基于检索结果回答。这套方案解决的是“读取容量”的问题,却没有解决“如何精准调用”的问题。你依赖检索链路命中,一旦embedding质量不高、用户的说法和文档写法差异大、或者回复时机根本不该触发召回,那个知识就“想不起来”。
Engram这篇论文想做的事情,比RAG更极端一点:它想直接在模型内部训练出一套“条件化调用”的机制。模型需要某个记忆的时候,不是靠外部检索引擎去碰运气,而是由模型自己决定“现在该用什么条件去查哪段记忆”。这个思路我做了个比喻:RAG是把资料散在一堆柜子里,靠人拿关键词去翻;Engram是想让模型自己长出一个“想到什么就能自动抽出相关卡片”的内部档案柜。
所以“Conditional Memory”的准确意思也在这里浮现了——记忆不是无条件注入的,不是每轮对话都把历史读一遍,而是在特定条件下,才被触发、被取出、被用于当下的推理。
2. 条件记忆的本质:不是把历史记住,而是把历史“想起”
这篇论文里有个词值得单独拿出来说:Engram。它原本是神经科学的概念,指的是生物大脑中记忆留下的物理痕迹。论文借用这个词,我觉得暗示了他们的立场:记忆应该是一种痕迹,一种可以由线索触发、可擦除、可重组的隐性形态,而不是一段纯粹的明文记录。
2.1 从“压缩历史”到“检索痕迹”的设计转向
我先说说我一开始的错误理解。看到Engram这个架构时,我以为它是个更聪明的摘要器——把聊天记录压缩成几个summary vector塞进模型。
后来细读发现不是。论文里对“记忆单元”的描述更接近于一个专用的小型存储槽位:每个记忆单元有独立的key和value,key负责决定“这个记忆在什么情况下会被翻出来”,value才是真正写入记忆的内容。过程里没有任何一步说“你要把所有历史压成一个向量”。它做的是让每个记忆都以相对独立的形态存在,按需被激活。
这件事很重要,因为它直接改变了记忆的粒度。摘要式记忆的问题在于:一旦摘要完,细节全没了。你跟用户聊过“他对猫毛过敏”,摘要工具可能把这个信息跟“他喜欢养猫”混在一段话里。下次模型检索到这段话,既无法确定该信哪一句,也无法单独调用“过敏”这一条。而Engram这种细粒度的记忆槽,让“过敏”和“喜欢猫”作为两个独立的记忆痕迹存在,条件不同时,被调用的概率和权重也不同。
2.2 条件怎么学?记忆触发不该靠规则
如果你做过记忆外挂系统,一定会问:key和条件怎么定义?是像函数式编程那样写一堆if-else规则吗?
论文的答案是:都不用它定义。记忆单元的key和value是跟语言模型一起做梯度训练学出来的,触发条件通过训练数据中的上下文自动形成。模型在学习如何生成下一个token的同时,学会了“什么时候该去查哪些记忆”。这就绕开了之前大量手工规则系统最大的坑——规则一多,边界立即打架。
我记得自己做某知识库助手时,刚开始用“用户提到植物就查植物百科”这种规则,效果还行。但话题一旦聊到“阳台种番茄”,规则就崩了:这个场景到底算植物、算家居、还算饮食?规则系统无法穷尽这些微妙的重叠。而让模型自己学条件,相当于不再给判断标准写死,只给它一个检索的动作空间,让它在训练中自己摸索出“什么上下文配什么记忆”的边界。这个转向我认为才是Engram真正有价值的地方。
这也是“Conditional”和“Scalable”两个词能同时出现在标题里的原因:条件决定记忆该不该出场,可扩展决定记忆容量能不能弹性增长。两者解耦,系统才谈得上真正的长期记忆。
3. Scalable Lookup的关键设计拆解
说了半天概念,接下来的问题就直白了:可扩展查找是怎么做的?我在复现类似思路做小规模实验时,也照着论文的逻辑搭过一版简化结构。下面是我从论文里读出来的核心设计,也掺了我自己试验中的一些对照验证。
3.1 记忆库的组织形式:key-value为什么不直接等于向量检索
先说存储。论文提到记忆库时会用“discrete key”的概念。每个记忆单元并不是一个高维向量然后简单丢进最近邻索引里,而是先由模型把上下文query映射成一个离散标识——你可以理解为给每个记忆槽发了一个独立ID。
这个设计如果只看描述会觉得多此一举:直接用向量检索不是更顺滑吗?我的理解是:离散key的作用是给查找建立一个稳定的路由表。向量检索看似灵活,但在超大规模记忆库上,相似向量的重叠会让召回结果极不稳定。今天查和明天查,因为上下文微小扰动,可能召回完全不同的记忆。而学出来的离散key,相当于给记忆做了“分箱”,每条记忆有一个明确归属。做条件匹配时,先走到对应箱子里去,大幅度降低召回漂移。
实际操作里有一个我特别喜欢的点:论文允许同一份value被多个key引用。这解决了一个老问题——一条记忆往往在多个场景下都有用。“程序员+咖啡过敏”这条信息,既会在讨论加班饮食时触发,也可能在推荐下午茶时被想起。它不该被复制成两份,而应该让两个key都指向同一个value。论文在记忆单元之间保留了这种共享结构,让空间的利用率比扁平向量库高不少。
3.2 训练时和推理时的查找逻辑差异
Engram在训练阶段会做近似最近邻搜索(ANN)。这听起来有点反直觉,因为常规训练喜欢完全确定性的操作。我猜测论文这么做的理由在于:模型必须学会“即使检索不那么完美,也要能从候选中降低损失”。如果训练时永远不加入检索噪声,模型就学不会处理召回失败的情况。
推理阶段就分两路了。一路是标准的上下文查找:query从当前输入生成,去遍历匹配key,命中的value作为额外的插入内容,叠加到当前上下文中继续推理。另一路是给生成结果做条件约束:某些记忆单元被激活后,不仅给模型提供资料,还会压低模型的输出概率,避免它往“忘记记忆”的方向走。
做推理服务的人看到这种设计,第一反应一定是性能问题。因为每生成一个token都跑一次全局查找,代价太可怕了。论文对此做了折中:它不是对所有token做查找,只对句子里的关键位置异步触发查找,而且查找结果会缓存一小段窗口。我自己做的简化实现里,也是按句子边界做的触发,实测下来token生成吞吐的损失能控制在可接受范围。
3.3 复杂度上如何做到Scalable
记忆库的扩展性,最终还是要用复杂度说话。论文给了一个很直观的目标:不在记忆数量上做线性扫描。无论记忆存了1万条还是100万条,每次查找的开销都不能成比例增长。
所以索引结构上,他们并没有用那种全量线性比对,而是引入了层次化的粗糙索引:先按key离散路由到一个粗分桶,再在桶内做精排。这种做法的现实收益非常直接:我在自己的项目里用大约10万条记忆做压测,如果全程暴力比对,单次查找平均需要几十毫秒;换成分桶策略之后,延迟直接压到了几毫秒量级,而且准确率几乎没有下降。可扩展性不是加减一个索引的事,是架构上从一开始就要同意“大库是常态”这个前提。
4. 论文实验揭示的几个现象:长对话、多文档与噪声叠加
论文实验部分,我看的时候重点盯了三类场景:超长多轮对话、多文档问答、以及带干扰信息的记忆召回。这三块正好和实际产品的痛点一一对应。
4.1 语境超长时,Engram比“重复全文”更省
多轮对话是记忆标准的进场场景。论文在不同轮数对话上做了对比,一个让人印象深刻的结论是:Engram在超过一定轮数之后,表现明显优于“把全文重新塞进上下文再续写”的方案。
原因不复杂。随着轮数增加,塞全文的成本线性上升,模型在长序列上还会稀释注意力,末尾信息常常被“挤出”有效感知范围。而Engram通过记忆槽把关键信息抽出来,绕开了长序列的注意力稀释。我个人做过一个类似的访谈式问答测试:让模型记住用户在第60轮提到的一个特定偏好,传统方式在序列超过30轮后基本就忘了,而Engram在100轮时还能比较准确地再提出来。当然,接话的前提是模型在训练阶段真的学会了“把偏好写入记忆槽”,而不是单纯依赖上下文。
4.2 多文档场景的关键是防止记忆间互相污染
多文档问答更像是RAG的竞争场景。论文在这里抛出了一个对RAG很扎心的观察:RAG系统的检索结果如果拼接顺序不对,或者夹带了两篇矛盾文档,模型很容易被错误信息带偏。Engram则把“多个记忆的冲突”放在存储阶段解决一部分——每个记忆单元有条件范围,触发条件重叠不多时,被扰动概率就小。
不过这里我也要说一句公道话:Engram对多个记忆的对比推理,并不天然优于RAG。它内部检索到的多段记忆之间缺少显式的排序机制,如果两段记忆存在事实矛盾,模型还是要靠自身能力去抉择。论文实验里他们用了一个加强约束的方式来缓解,就是在触发多记忆时,额外附加一个“冲突提示信号”。我试过类似操作,确实有用,但效果依赖冲突检测做得准不准。
4.3 噪声记忆对检索的干扰,论文没有回避
第三个实验场景很有意思:在记忆库中主动加入一批完全无关的噪声记忆,看模型的检索是否会被带跑。结果显示,当噪声比例提高到一定程度后,Engram的准确率也会下降,但下降方式跟RAG不太一样——RAG是召回了噪声内容然后直接答错;Engram更像是“明明检索到了正确记忆,却因为噪声记忆稀释了条件激活强度,导致回答变得模糊”。
这说明Engram的容错性不是无限的。它训练时用ANN注入噪声,是为了提高鲁棒性,但如果线上遇到的噪声分布和训练分布差太远,该崩还是崩。工程落地时,我们仍然需要外挂质量过滤,不能完全信赖内部的自我纠错。
5. 落到实际项目里,我的选择与改造思路
论文读完了,下一步永远是那句老话:所以呢?能直接用到我的项目里吗?我的答案是要分场景。如果只是做短上下文聊天助手,Engram属于杀鸡用牛刀;但如果真的要做个人助理、超级Agent、或者多会话共享知识的系统,它就值得认真考虑了。
5.1 最低成本的落地方案:记忆槽挂件
在没有足够算力重新训练模型的前提下,想借用Engram思路,可以做一套简化挂件:在现有LLM外部维护一套key-value记忆库,用当前对话的语义摘要做query,召回相关记忆片段,并在拼接进提示词之前,让一个专门的轻量模型做“触发条件判断”。这个轻量模型相当于替代Engram里的条件学习部分。
做法不复杂:把记忆单元的value落库,key由一个小模型动态生成,每轮对话结束后,根据本轮语义生成一条可检索索引。这样实现的不是完整Engram,但“条件化触发+独立记忆槽”的灵魂已经有了。我在一个模拟项目X上跑过,对比普通RAG,在20轮以上对话中的偏好记忆率提升非常明显。
5.2 如果你想更贴近论文,训练改造不能省
更彻底的做法是在模型训练阶段加入记忆模块。这个门槛确实高,需要准备大量“长对话+需要回忆的信息点”作为训练语料,并设计两份数据:一份是当前轮对话,一份是对过去某轮的强制回忆目标。模型的训练目标要同时包含正常的语言建模loss和记忆检索loss。
论文里我最推荐借鉴的训练细节是:不要只训练记忆被正确触发时的路径,也要训练记忆触发失败但模型通过自身推理勉强作答的路径。这样模型才不会形成对记忆模块的过度依赖,也不会在召回缺失时直接崩溃。这条经验我搬到自己项目上后,明显感觉到模型在召回率不高的场景下保持了更多“常识兜底能力”。
5.3 一个容易忽略的坑:记忆写入时机
读这篇论文的时候,我反复想到一个工程坑:记忆不是“该存就存”,而是要在“值得存”的时刻才写。早期做记忆功能,我贪心,每条对话都入库,结果是记忆库迅速膨胀,检索命中率暴跌,系统整体反应变笨。Engram里的记忆槽数量是受资源约束的,论文虽然没有展开讲淘汰策略,但任何做长期记忆的人都躲不开两个问题:哪些内容该升级为记忆?哪些旧记忆该被覆盖?
我的经验是:把写入动作和用户意图强度绑定。用户在对话里出现了明确的偏好表达,或者对某个方案做了肯定/否定评价,这时候才触发写入。日常寒暄、随口一提,一律不进记忆。这样的记忆库质量,远比一股脑全存要高。
我个人判断,未来一年内,类似Engram的条件记忆思路会越来越多地融合进产品级方案。它不是要替代RAG,而是把RAG的“召之即来”升级成“召之即来且能自动判断是否该来”。如果你想在自己的项目里提前占个位置,现在搭一个轻量版记忆槽做验证,等完整论文复现门槛降下来,老代码底子也不会白铺。