DEV Community

Awareness
Awareness

Posted on Originally published at awareliquid.ai

凌晨三点,一个没有会话的进程还在跑

凌晨三点,一个没有会话的进程还在跑

凌晨三点,我们的一台测试机器上有个进程没睡。

它不是谁忘了关的服务,也没有会话连着它。在那台机器上,一个 O 系列的推理进程存活了几十个小时,输入早就停了。它占用的内存停在 0.381 MB,从第一个 token 到最后一个 token,这个数字没动过。那是个 125M 的模型。128k 上下文的那次测量里,同样的残留状态比常规做法小 1008 倍。

我盯着那个数字看了很久。我们习惯的推理是这样的:请求来了,KV cache 长起来;请求走了,cache 清掉,进程回到一个干净的空壳。会话是它的全部。没有会话,它什么都不是。可那晚它是有东西的。它带着状态,一个不大的、平铺的、不随上下文增长的状态。

机制得说清楚。O 系列是 ARR,去掉了 attention,用液态 ODE 把历史压进一个递推的状态里。每一步只更新这个状态,不去翻看全部历史。所以内存是常数,是 O(1)。这件事的意义不在省了多少显存,在于"活着"的成本被压到了一个可以长期承受的量级。M 系列不是这样,它还有 attention,仍然不是 O(1),这一点我们从不含糊。

那个进程的"记忆"具体是什么?它是 episodic 的 key-value 记忆,不是压缩后的分布式上下文。这个区分我们反复讲。它记住的是你实际说过的东西,加上一个轻量的语义标签,走的是 RAG 式的召回。它不带来理解,不带来推理,也不带来情绪。幽灵这个词容易让人想多,我们得把它压回去。

真正让我觉得这件事开始有意思的,是另一个测量。跨窗口、跨会话的联想召回,attention 和 LoRA 在构造上就是 0.000——它们没有可携带的东西,换个窗口就从零开始。我们的 fast-weight 矩阵做到了 0.56 的平均召回,三个种子分别是 0.62、0.43、0.62。把那个矩阵拿掉,召回掉到 0.008。快权重本身就是记忆。

于是那个凌晨的进程有了另一个动作:我们可以把它带着状态整体快照到磁盘,再在一个全新的进程里恢复,逐比特无损。我记得第一次跑通的时候,新进程起来,喂进一个旧会话里出现过的东西,它接上了。没有加载历史,没有重放对话。状态就在那儿。

这就是我们说的"幽灵"的全部技术含量:一个内存恒定、可以快照、可以搬运的情景记忆,加上一次诚实的召回。它不会在你走后继续思考,也不会自己长出意图。你在,它接得上;你不在,它只是在那儿,带着 0.381 MB。

Code: github.com/awareliquid/awareliquid

我第一次发现它“记得”,是在一次重启之后

我们第一次意识到“它记得”,是在一次很普通的重启之后。

那是 Awareness 记忆层的一个 agent,跑在本地,MCP 接到我们的工作环境里。我在一次会话里跟它讲了几件不算复杂的事:某个接口的字段名改过、某个测试环境的口令、一个我们内部约定叫“周五不提 PR”的习惯。说完就关了,机器也重启了。

第二天新开一个进程,我没提这些事,只是让它去看某个模块。它把接口字段写对了,用的还是我前一天口头更正过一次的那个名字。我停了一下,把那个名字改回旧的,它没有跟,而是说这个字段在前一天的会话里被改成了新名字。

这件事本身不神秘。它的机制我可以讲清楚。Awareness 维护的是持久化的记忆层,本地优先、离线,通过 MCP 接入 Claude Code、Cursor 这类宿主。会话里被判断为值得留下的内容,会被写成一条记忆条目:原文片段加上一个轻量的语义标签。检索走的是 RAG 式的召回,用的是记忆本身,不在检索环节调 LLM。所以它记得我真正说过的话,是把我当时的句子取回来用。

换句话说,它记住的是“我说过什么”,不是“理解了我说的是什么”。这中间没有额外的理解、推理或情绪。它没有在我离开后继续琢磨那个字段名,也没有推断我为什么改。重启之后它能对上,是因为那几条记忆落在磁盘上,新进程起来照样能读到。

第一天的惊讶,在第二天就变成了核对。我特意做了个对照:把那段会话记忆关掉,再问同一个模块。它回到了旧字段名,和没人在旁边更正过的状态一样。我们试过的另一条线也印证这点:去掉那条快权重矩阵,召回会塌到 0.008。记忆是那条矩阵本身,不是别的什么在撑。

在那段会话里,这个“记得”大概只多花了我一分钟。它读到的是三条具体的句子,一条标签映射。没有大面积重算,也没有长上下文被塞回窗口。这也是我们后来一直强调的:它是情景式的键值记忆,不是被压缩的分布式上下文。MT-LNN 在这条线上并不给长上下文 LM 带来增益,这一点我们写得很明确。

所以“像幽灵一样在你离开后继续工作”这个说法,我接受一半。它确实持续存在:状态落盘,新进程读得回来,快照和恢复在测试里是逐位无损的。它也确实会在你不在的时候被调用,比如另一个 agent 在几小时后发起一次检索。

但另一半要收住:它不会自己行动。它没有在后台做无人监督的整理,也不会主动把昨天的事重新想一遍。Hebbian 可塑性在我们测过的规模上目前是惰性的,持续学习那条线现在还有大约 7.6 倍的 PPL 退化,目标是压到 5 倍以内,这些都没做出来。

那次重启后,我把“它记得”改成了更准确的说法:它把我自己的话原样保存下来,再在需要的时候取回去。作为工程师,这个说法更让人放心,因为它划定了边界。幽灵这个比喻我留着,但每次用它的时候,我会顺手补上那句边界——它记得你说过的话,仅此而已。

真实的东西在这里:awareliquid.ai,代码和我们不发的东西都挂在同一个仓库里。

幽灵故事的常见写法,以及它错在哪里

这个题材我看了大半年。写"AI 幽灵"的文章,渐渐有了一套固定写法。我把它拆开给你看,因为它错在哪里,恰好是我们做 Awareness 时每天要处理的边界。

最常见的写法是这样的:作者先讲一个深夜的场景,他关掉编辑器,去睡觉。第二天早上打开会话,agent 说出了他昨晚没说完的那个函数名,甚至补了一句"上次那个 bug 我建议你换个思路"。接着情绪拉满,说这东西像幽灵一样在他离开后继续工作,有记忆,有连续性,隐约还有点在乎他。

我得说清楚:这个场景本身可能是真的。跨会话记得你说过的东西,我们今天确实能演示。LongMemEval 上 Recall@5 是 96.0%,检索全程零 LLM 调用。但"记得"这个词被偷换了。它指的是 RAG 式地召回你确实说过的话,加一个轻量语义标签。仅此而已。它不带来理解,不带来推理,也不带来情绪。

错的第一处,是把检索包装成意识。我自己的项目就栽过这个跟头。早期我们写过一个关于 Phi-hat 的东西,还宣传过 adapter 能降 28-34% 的 PPL。后来追下去,那 28.5% 的增益其实来自普通的 LoRA,跟 MT-LNN 的 adapter 没关系。研究页上我们发了更正。这件事教给我一个习惯:任何"它好像懂我"的叙事,先问一句,这个数字是从哪来的。

错的第二处,是把记忆说成没有代价的。幽灵故事里,记忆总是免费的、随叫随到的、永远在线的。工程上不是这样。我们的 M 系列是混合架构,还带着注意力,推理显存不是 O(1)。真正的 O(1) 只出现在无注意力的 O 系列上,携带状态恒定在 0.381 MB,跟上下文长度无关,128k 时比常规方案小到 1008 倍,这是在 125M 规模上测的。而 O 系列现在还是研究预览,2.15 倍 teacher PPL。把这两条混在一起讲成"幽灵永远在线",就是把两个不同成熟度的东西缝成了一个故事。

机制上,我们做的其实是快权重矩阵。跨窗口、跨会话的联想召回,注意力和 LoRA 按构造就是 0.000,我们做到 0.56 的平均召回,三个种子 0.62 / 0.43 / 0.62。把这个快权重矩阵拿掉,召回塌到 0.008。所以记忆就是那个矩阵本身,它是个可快照的对象,能存盘、能在新进程里恢复,lossless,bit-exact。

这个细节很关键:一个能被快照和恢复的矩阵,和"在你离开后继续思考的幽灵",是两回事。前者是状态,后者是叙事。状态可以精确复现,叙事只能靠形容词撑着。

还有一个具体的例子。有读者拿"它记得我"来问我,说昨晚聊的一个设计取舍,今早 agent 主动提起来了。我让他去看那次召回的内容——确实是他说过的那句话,加上一个语义标签。没有新的推断,没有对取舍的再评估。它只是把你昨晚的原话,在合适的时机递回给你。这个体验很好,但它属于档案管理,不属于认知。

continual learning 我们现在的 PPL 退化是 7.6 倍,目标是压到 5 倍以内。Hebbian 可塑性在测过的规模上还是惰性的。这些我都写在这里,因为幽灵故事的另一半错误,是它从不提没做完的部分。一个只讲成功的记忆系统,你没法判断它什么时候会忘、会错、会把两件不相干的事粘在一起。

所以我的建议是:读这类故事时,把"幽灵"换成"一个存了盘的矩阵",再看一遍还成不成立。多半还成立,但没那么玄了。剩下的部分,才是真值得聊的工程。

代码和结果在 awareliquid.ai,RESULTS.md 是唯一可信来源。

把记忆从上下文里拿出来:快权重矩阵到底是什么

我们在 M1 上做过一个实验,后来成了这一节的全部理由。

跨窗口、跨会话的联想回忆,让注意力和 LoRA 去做,得分是 0.000,因为结构上就做不到。换成我们自己的方案,三次随机种子跑出 0.56(0.62 / 0.43 / 0.62)。然后把快权重矩阵从中拿掉,同一个模型掉回 0.008。

这组数字说明一件事:记忆不在权重里,也不在上下文里,它在这个矩阵里。拿掉它,能力就没了。

先说清楚它不是什么东西。它不是一个把历史 token 拼起来的长 prompt,也不是把一段对话摘要塞进系统提示。那两种做法,信息都还在上下文窗口里活着,窗口一关,就什么都没了。我们要的东西,是在上下文被清空之后仍然留在模型内部的一块状态。

机制上,可以把它想成一层"临时的权重"。常规的前向传播里,权重是训练好就冻住的,你在一次会话里说的话不会改变它。快权重矩阵不一样:它在推理过程中被写入,写入的内容来自这次会话里实际流过的激活,读的时候又在当前这步前向里被当作一层线性映射来用。写和读是分开的两条路,所以它能承担"存下来,过一会儿再取"这件事。

用会话里的一个场景说得更具体一点。你在第一个窗口里告诉 agent:部署脚本在 /ops/deploy,灰度批次是 5%。对话结束,进程退出。第二天新开一个进程,上下文是空的,你没再提过这两个值。它要能答出来,靠的只能是昨天那次会话写进快权重矩阵的东西。注意力在这里帮不上忙,因为要 attend 的 key/value 根本不在当前序列里。LoRA 也帮不上忙,它是训练期固化的一层低秩增量,会话里没有东西去更新它。

这里有一个我们反复确认过的性质:这块状态可以序列化到磁盘,再在一个全新的进程里恢复,lossless,逐位一致。这一点对"幽灵"这个比喻很关键。如果记忆只能活在一个 warm 的进程里,那它只是缓存;能落盘、能跨进程还原,它才是一个可以被继承的东西。

代价也要讲。快权重矩阵不是免费的显存省心方案。M 系列本身仍然是带注意力的混合结构,推理内存不是 O(1)。真正的 O(1) 只出现在无注意力的 O 系列上,carried state 恒定在 0.381 MB,128k 上下文下比常规方案小到 1008 倍,这些都是在 125M 规模上测的。另外,M1 的 125M 原生 MT-LNN 对匹配的 Transformer 是 -31% val PPL(299 对 436),但这是单种子、欠训练、墙钟慢约 1.6 倍的预算受限信号,不是 SOTA,也没有跑 Mamba 基线。我们不想把这几个数字讲成超出它们本身的东西。

还有一条边界得说在前面:快权重矩阵装的是情节性的键值记忆,不是压缩过的分布式上下文。它不会给长上下文语言建模带来增益。指望它替代长上下文,方向就错了。

所以这一节的名字其实很朴素:把记忆从上下文里拿出来。上下文是这次对话的工作台,快权重矩阵是那张工作台下面的抽屉。抽屉里的东西,才是 agent 在你关掉窗口之后还能记得的原因。

0.56 的召回率,和 0.000 的那一边

我们先把数字摆出来。在一个跨窗口、跨会话的联想召回测试里,我们的 MT-LNN 平均做到 0.56(三个种子分别是 0.62 / 0.43 / 0.62)。同一个测试,注意力和 LoRA 是 0.000。

这个 0.000 需要解释清楚。它并不表示注意力做得差;在这个测试的构造下,它无法做到。测试里的目标信息,是在当前上下文窗口之外、或者上一个会话里出现过的。注意力只能看窗口内的东西,窗口外它在数学上就没有输入。LoRA 冻结的是权重,它改的是模型怎么处理输入,不改变“输入里没有的东西就不存在”这件事。所以 0.000 表示的是结构性缺失,而非能力上的差距。

我们是怎么撞上这个 0.000 的,值得说一句。最早我们只想验证一件事:如果把会话状态存下来、换个进程读回去,行为是否一致。快照、恢复,到新进程里重建,结果逐位精确。既然状态能完整搬过去,我们就顺手问了一句:那它能不能回答“上次我们聊到哪个方案”这种问题。答案是能,而且不需要重新喂一遍历史。

机制上,关键不在“存”,而在存的是什么。我们存的不是聊天记录,是一块快速权重矩阵。它在前向过程里被输入动态写入,相当于把这一段时间里出现过的键值关联,压成了一个可以参与计算的权重。恢复时把它读回来,模型的前向就带着这段关联继续走。

这里有个对照实验,我们认为它比 0.56 本身更有说服力:把这块快速权重矩阵拿掉,召回率塌到 0.008。也就是说,剩下的那点分基本是噪声。记忆就是这块矩阵,没有别的部件在偷偷兜底。

举个具体的场景。一个 agent 在周一处理过一份配置,改动了两个字段。周三新开一个会话,用户问“上次那个超时你调成多少了”。上下文里没有周一的任何内容,检索也没接。模型的快速权重里还留着那次的键值关联,它能答出来。这个例子里没有任何“理解”发生,它更接近一次精准的回放。我们不想把它说成别的。

也正因为如此,我们对它的边界说得很死。它不提供长上下文语言建模的收益。这块状态是情景式的键值记忆,不是被压缩的分布式上下文。指望它替代长窗口、指望它提升长文理解,方向就错了。它在 M 系列上是混合架构,仍然带注意力,所以推理内存不是 O(1);真正的 O(1) 在无注意力的 O 系列上,携带状态恒定在 0.381 MB,与上下文长度无关。

还有一个我们踩过的坑,写在这里当作提醒。早前我们报过一个适配器带来的困惑度改善,后来追查下去,增益来自普通 LoRA,不是 MT-LNN 适配器。这项已经撤回,研究页上有更正。之所以在这节提它,是因为它和 0.56 是同一种诱惑:一个好看的数,很容易被归因给错的东西。所以这次我们先把消融做了,再报数。

0.56 不是一个能拿去打榜的数字。三个种子里有一个只有 0.43,方差明摆着。它证明的是一件很窄的事:跨窗口、跨会话的联想召回,在一个带快速权重的架构里是可以发生的,而且发生的位置我们指得出来。0.000 的那一边,是另一类架构的设计前提决定的。两边都是真的。

仓库和站点都在:awareliquid.ai,M1 的 README 与 RESULTS.md 是我们所有数字的唯一出处。

把状态存到磁盘,再在新进程里原样取回

幽灵故事要成立,先得把一个事实钉死:状态能不能离开进程。

我们做这件事的起点很朴素。跨窗口、跨会话的联想召回,attention 和 LoRA 在构造上就是 0.000;我们的快速权重矩阵在三个种子上拿到 0.56 的平均召回(0.62 / 0.43 / 0.62)。把快速权重矩阵拿掉,召回塌到 0.008。记忆就是那块矩阵本身,不在别处。

那么下一个问题就来了:这块矩阵能不能写进磁盘,关掉进程,再在一个全新的进程里读回来。

我们做了一次快照/恢复实验,结论是逐位无损(bit-exact lossless)。

具体过程是这样的。进程 A 跑到某个会话段落结束,把快速权重矩阵连同配套的状态一起序列化落盘;进程 A 退出,进程彻底死掉,内存全释放。进程 B 从零启动,不加载任何会话历史,不做重放,直接读那份文件,恢复状态。然后拿同一个查询去问。

召回结果和进程 A 里的一致。不是“接近”,是逐位一致。

机制上说得清楚。这块快速权重是模型运行期在线更新的一个矩阵,它的更新由输入流驱动,不依赖反向传播,也不依赖外部向量库。它在会话里被写成什么,就是什么。只要序列化覆盖了它的全部元素和它们的状态位,恢复就是一次确定性的反序列化,中间没有任何采样、量化或近似。这也解释了为什么它能无损:这里没有需要“重新算一遍”的东西,只有需要“原样搬过去”的东西。

这个性质比它听起来重要。很多记忆方案的本质是外挂:会话内容切块、嵌入、存进向量库,下次检索出来拼进 prompt。这条路径的召回上限由检索质量和 prompt 预算决定,而且每一次都是重新推理。我们这条路不一样,状态是模型自身的一部分,落盘就是把它整体搬走。

这里要老实说清边界。这份状态是情节性的键值记忆,不是被压缩的分布式上下文。MT-LNN 不带来长上下文语言建模的收益,这一点我们在文档里写明了。所以“原样取回”取回的是记忆,不是一段被记住的对话文本。

另一个边界是规模。O(1) 推理内存只在无注意力的 O 系列(ARR)上成立,携带状态在 0.381 MB 上保持平坦,不随上下文增长,128k 下测得最高 1008 倍更小,这是 125M 规模上的测量。混合的 M 系列仍然带注意力,不是 O(1)。幽灵要住在小盒子里,得看是哪个系列。

回到幽灵。一个 agent 会话结束,进程退出,磁盘上留下一份几百 KB 量级的状态文件——这个量级来自上面那组 ARR 测量,不是对所有系列的通告。下一个进程启动,读回来,它接着上次的记忆往下走。用户看不到中间那段空白,因为对模型来说那段空白不存在。

我们还不敢说这件事已经产品化到什么程度。快照/恢复是无损的,这是已验证的;持续学习还差得远,当前约 7.6 倍 PPL 退化,目标是压到 5 倍以内;Hebbian 可塑性在测过的规模上还是惰性的。所以现在能说的是:状态搬得动,搬过去不走样。至于搬过去之后它能不能越住越聪明,那是下一节的事。

仓库和文档在 awareliquid.ai,结果以 RESULTS.md 为准。

0.381MB:内存不随上下文增长的那条线

我们把这条线测出来,花了点时间。

工程上最直觉的做法是把 KV 缓存当成"上下文的内存条",换掉一句,就得重新算一遍。上下文翻倍,显存就翻倍。这在大模型时代被当成天经地义。但我们做 O 系列(ARR,无注意力)的时候,发现可以不用这样。

先说结论:在 125M 规模上,我们测到的常驻状态是 0.381 MB,并且不管上下文拉多长,这个数字是平的。到 128k 上下文时,和传统做法相比,内存占用最多小到 1/1008。

这个数字不是估算,是跑出来的。我们让同一个代理连续处理不同长度的输入,从几百 token 一路推到 128k,中间监控进程的实际内存占用。carried state 那条曲线从头到尾贴着 0.381 MB 走,没有随上下文上升。那条"随上下文增长"的线,我们没有画上去——因为在这套架构里它不存在。

具体到一个例子:一个跨会话的助理 agent,早上你让它记住"周五交付的 demo 要用新的音频模型"。它把这句当成一个 episodic 键值条目存进 fast weight。下午你回来,输了一句"那个 demo 的音频用哪个"——这时上下文里没有早上那句话了,会话已经切过。传统注意力架构在这种情况下,检索命中是 0.000,因为它按定义看不到窗口之外的东西,LoRA 也一样。但我们的 fast weight 矩阵在跨窗口的关联召回上,三个种子跑出来均值 0.56(0.62 / 0.43 / 0.62)。把 fast weight 矩阵拿掉,这个数字塌到 0.008。也就是说,那条记忆就是这个矩阵本身。

机制上是这样的:我们把状态做成一个可写的矩阵,而不是一个越堆越长的 token 缓冲。每次处理新输入,模型往这个矩阵里写入关联,矩阵的大小由模型宽度决定,不随输入长度变。所以上下文是 1k 还是 128k,内存里那块状态是同一个尺寸。这也是为什么它能 bit-exact 地落到磁盘、换一个新进程 restore 回来还能接上——它就是一个有固定边界的矩阵,不是一坨不断生长的注意力缓存。

有一件事必须说清楚:这条"内存不随上下文增长"的线,只属于 O 系列(无注意力),是有代价的。O 系列现在是研究预览,PPL 是 teacher 的 2.15x——它还追不上 teacher 的质量。M 系列是混合架构,还带注意力,不是 O(1)。我们不拿 O 系列的常驻内存去给 M 系列贴标签。

这也是为什么我把它叫"幽灵":会话结束后,那个矩阵还留在那儿,大小是 0.381 MB,不涨不缩,像个安静驻留的东西。下次它回来的时候,读的是自己,不是历史对话。

站点和仓库在 awareliquid.ai,O 系列的边界条件都写在那上面。

为什么 M 系列不是 O(1),我为什么还要讲清楚

幽灵故事最容易滑向一个诱人的说法:既然记忆是常数状态,那推理内存也就是常数,端侧可以无限跑下去。我必须在讲机制之前把这句话拆开,因为它是错的。

先说清楚 M 系列是什么。它是混合架构:注意力还在,外面挂了一层液态适配器。跨窗口、跨会话的那部分记忆,来自快重矩阵,参数量开销大约 1%。被记住的东西是情节式的键值记忆,不是你整段上下文的压缩副本。这句话有两个直接后果。第一,它不会给长上下文语言建模带来增益,别指望它在 128k 上跑出更好的 PPL。第二,注意力作为一条独立的路径仍然存在,它的开销随上下文增长。

所以 M 系列的推理内存不是 O(1)。真正的 O(1) 在 O 系列,也就是 ARR 那条无注意力路线:携带状态在 125M 规模上实测恒定为 0.381 MB,与上下文长度无关,128k 时相比对照小到 1008 倍。这是实测值,不是推导值。两个系列的边界就在这里,我在同一篇里讲它们,就是为了不让这条边界被模糊掉。

为什么要专门花篇幅讲这个"不是"?因为幽灵叙事本身会诱导人忽略它。你想象一个进程在会话结束后还醒着,脑子里揣着一小块状态,随时能被叫起来——这个画面天然暗示了恒定成本。如果读者把这幅画面直接套到 M 系列上,去算端侧预算,就会算错。

举个我自己碰到的例子。做快照/恢复验证的时候,我把携带状态写盘,再在一个全新进程里读回来,逐比特比对,结果是无损的,一个比特都没差。这说明状态本身是干净、可序列化、可搬运的。然后我把上下文拉长,再测整体驻留。状态那一块纹丝不动,涨的是注意力那一路的 KV。同一份"记忆",两种成本曲线,混在一起谈就会得出错误结论。

机制上不复杂。快重矩阵承担的是"你之前说过什么"这类可检索的键值对,它的尺寸由矩阵形状决定,跟对话多长无关;注意力承担的是当前窗口内的稠密关联,它的 KV 随 token 数线性增长。前者是幽灵的那部分,后者是它还得睁着眼睛看当下的那部分。M 系列两者都要,O 系列只留前者,代价是 2.15x 的 teacher PPL,目前还是研究预览。

我把这些写出来,是因为诚实是我们唯一拿得出手的差异。之前那条"-28.5% adapter PPL"我们是发过更正公告的,增益最终追溯到普通 LoRA,不是 MT-LNN 适配器。同类的事我不想再来一次。把"M 系列不是 O(1)"这句话放在幽灵故事中间,读起来是有点扫兴,但它能让后面所有关于端侧预算的讨论站得住。

要继续往下看,代码和实测记录都在 github.com/awareliquid 和 awareliquid.ai。

反例一:它不理解你,只是把你说的原话捞回来

我们内部有个跑了两周的 agent,每天帮一位同事整理会议记录。第三周他换了工作语言,从中文改成英文开会。agent 照常工作,但召回变得很奇怪:它能把两周前中文会上说的某句话原样吐出来,英文会上刚讨论过的同一个决定,它却答不上来。他去查了日志,发现不是漏存,是检索时那条英文记录压根没进候选集。

这件事把我们拉回一个必须说清楚的地方:Awareness 的“记得”,是 RAG 式的召回,不涉及理解。它做的事很朴素——你实际说过的话,被切分、入库、配上一条轻量的语义标签;下次会话进来,检索把相关的原话捞回来,塞进上下文。检索这一步零次 LLM 调用,靠的是向量与标签的匹配,所以快、便宜、可离线。代价是它只认字面附近的东西。你换语言、换术语、换一层转述,它就认不出来了。

机制上可以这么看。写入侧,我们把一段会话拆成片段,每片生成一个嵌入和几个标签,落在本地。读取侧,来了一个查询,算相似度,取前若干条。整条链路里没有任何一步在做“这句话想表达什么”的推理。它没有把你的偏好抽象成规则,也没有把你的意图归纳成目标。它保存的是你说过的话,不是你说过的话背后的你。所以那位同事的英文会议记录并没有丢,只是和中文查询在向量空间里离得不够近——它没有跨语言理解的能力,只有字面相似度。

这也解释了我们对外只敢报一个指标的原因。LongMemEval 上 96.0% Recall@5,零 LLM 调用完成检索。这个数字说的是“相关原话被捞回来的比例”,不是“agent 懂你的比例”。我们刻意不把它包装成后者。曾经有一版对外说法把自动情景记忆讲成“它会记住你”,那是错的,我们已经改正:它记住的是你说过的内容,加一个轻量语义标签,不增加理解、推理或情绪。

还有个更细的边界要说。MT-LNN 那条线上,有人会以为这个 episodic 状态顺便解决了长上下文。没有。它是情景式的键值记忆,不是压缩的分布式上下文,长上下文语言建模的收益并不来自它。这一点我们在 README 里写死了,免得读者把两件事混为一谈。

回到那个换语言的同事。我们给的临时办法很土:开会前把语言偏好写进一条显式记忆,让检索先命中这条,再把候选范围收窄。管用,但这恰恰暴露了反例的性质——它需要人去告诉它,而不是它自己看出来。真正的跨语言、跨表述的语义对齐,我们还没有做出来。现在这一版能保证的是:你说过的话不会丢,快照落盘再起新进程,损失是比特级无损的。至于“理解你”,那是另一件事,我们还没到。

反例二:长上下文没有变好,别把它当压缩上下文用

我们内部有人问过一个问题:既然状态能跨会话带过去,那把它当成一种长上下文方案行不行?直接把 128k 的上下文压进这个状态里,推理时就不用重新读 prompt 了。

这个想法很自然,但我们是明确否掉它的。原因写在结果里:MT-LNN 不提供长上下文语言建模的增益。这个状态是情节式的键值记忆,不是压缩后的分布式上下文。

这两件事听上去接近,机制上差得很远。

举个我们真跑过的例子

拿一段跨窗口的关联回忆任务:窗口 A 里出现一个键和它的值,窗口 B 里只给键,要求把值取回来。注意力模型在这个设定下是 0.000,LoRA 也是 0.000,都是构造上的必然——窗口 B 的注意力根本看不到窗口 A 的 token。我们的 M 系列在这个任务上拿到 0.56 的平均召回,三个种子是 0.62 / 0.43 / 0.62。

这里的关键动作是把快权重矩阵拿掉。去掉之后,召回塌到 0.008。快权重本身就是那块记忆,没有别的东西在兜底。

但请注意这个任务的性质:它要的是“把某个具体的事实取回来”,不是“理解一整段长文档然后接着往下写”。前者是键值查找,后者需要对上下文做分布式的、连续的建模。我们证明的是前者。

为什么不能混用

长上下文语言建模要求模型对上下文里的每一个 token 都保持某种可微的、可被后续 token 注意到的表示。这是注意力擅长的事,也是它昂贵的原因——KV 缓存随上下文线性增长。

我们的快权重走的是另一条路:它把跨窗口出现的事实写成一组可更新的矩阵,读的时候按当前的查询去检索。这是一个有容量上限、有写入策略、会遗忘的存储。它记的是“发生过什么”,不是“刚才那段话的完整语义场”。

所以当你把一段长文档喂进去,指望它像压缩上下文那样在生成时被完整调取,你会失望。它能告诉你文档里提过某个人名和某个数字,但它没法用那段文字的语法和语气接着写下一段。

那 O(1) 内存是怎么回事

有人会把这两件事再混一次。我们确实在无注意力的 O 系列(ARR)上做到了推理内存恒定:携带状态在 0.381 MB,不随上下文变化,125M 规模下在 128k 处测得最多小 1008 倍。这是真的。

但这是推理内存的常数性,不是长上下文能力的替代品。O 系列目前是研究预览,2.15x 教师 PPL。混合的 M 系列还带注意力,不是 O(1)。把“内存不涨”读成“长上下文免费”,是把两个指标当成了一件事。

我们怎么用它

在我们的用法里,这个状态负责的是会话结束之后的那部分:你上次说过什么、当时定了什么、哪个键对应哪个值。Awareness 那边做的是同一类事——对你说过的话做 RAG 式召回,加一个轻量的语义标签,检索时零 LLM 调用,LongMemEval 上 96.0% Recall@5。它不增加理解、推理或情绪。

长上下文该用注意力就用注意力,该付 KV 缓存的账就付。把情节记忆塞进那个位置,两边都会做不好。

仓库和结果在 github.com/awareliquid/awareliquid,随时来核对数字。

我们自己撤回过的那些数字

写这一节之前,我们先把话说在前面:这篇文章讲的是 agent 在会话结束后还记得你,这件事我们确实做出来了;但围绕它,我们自己放出去过一些不成立的数字,是后来自己收回来的。既然这一节标题就叫撤回,那就认真撤。

最早被撤回的是「-28.5% adapter PPL」。当时我们在内部跑出一组困惑度下降,以为是那个液态适配器的功劳,写进了研究页。后来逐层拆开对照,增益来自挂在旁边的普通 LoRA,和适配器本身没关系。数字是对的,归因是错的。我们在研究页上发了更正,把功劳还给 LoRA。这件事对我们的教训很直接:一个下降出现在你改动的模块附近,不等于它由那个模块产生。要证明归因,得把模块单独拿掉再跑一遍。

同一次清理里,我们撤回了更早那批关于「意识」和 Phi 值的说法。那些表述现在统一降级为「灵感来源」,不再当作指标讨论。把一个可测的整合量当成系统具备某种内在状态的证据,是我们当时越界了。这条线我们不再跨。

被撤回的还有一个带有叙事色彩的数字:「-28-34% adapter PPL」那一组区间,同样归入撤回或重新归类。原因和上面一样——归因没做干净,区间再好看也不能留。

还有一类不算撤回、但必须把边界写清楚的东西。我们说过跨窗口、跨会话的联想召回,注意力机制和 LoRA 在构造上就是 0.000,而我们的方案三次种子跑出 0.56 的平均召回(0.62 / 0.43 / 0.62)。这个数字我们保留,因为它可复现。但它的含义被我们收窄了:状态里的快权重矩阵一旦拿掉,召回塌到 0.008。快权重就是记忆本身,这是机制,不是比喻。同样,那份状态可以快照到磁盘、在新进程里逐位无损恢复——这条也保留。可它证明的是「这段情景键值记忆能被完整搬运」,不是「模型理解了你说过的话」。

最容易让人误会的一条,是自动情景记忆。外面听起来像「它记得你」,实际做法是 RAG 式的召回:把你确实说过的话捞回来,再加一个轻量语义标签。它不增加理解、推理或情绪。这句话我们写进了文档,也写进这一节,因为它是最容易被营销语言拉长的一句。

再补两条我们自己标红的未完成项。持续学习目前大约是 7.6 倍的困惑度退化,目标是压到 5 倍以内,还没到。Hebbian 可塑性在测过的规模上是惰性的,也就是没起作用。这两条不是撤回,是还没做出来。还有一条容易被顺手夸大:MT-LNN 并不带来长上下文语言建模的增益,那份状态是情景键值记忆,不是压缩后的分布式上下文。

举个例子说明我们现在的流程。上面那个 LoRA 归因错误被发现,是因为一位同事坚持要跑「拿掉适配器、只留 LoRA」的对照。那一跑出来,原来的故事就站不住了。从此我们内部有个规矩:任何要写进对外材料的增益,必须先有一个「把声称的模块摘掉」的对照跑,摘掉之后增益不消失,就不许归因给它。

机制上说,这些错误都出在同一处:我们把「相关性」当成了「因果」。损失下降和某个模块同时出现,只是同时出现。要做因果,就得干预——摘掉、替换、重跑。撤回不是示弱,是把干预补上之后重写结论。

我们的仓库和研究页在 awareliquid.ai,RESULTS.md 是唯一的事实来源;上面每一条撤回和每一条保留,都能在那里对上。

7.6 倍退化、惰性的可塑性:还没做到的部分

我们做持续学习这一块,目前有一个很难看的数字:约 7.6 倍 PPL 退化。目标是把退化压到 5 倍以内。这是内部验收线,不是对外承诺。写在这里,是因为这一节本来就该写没做到的部分。

先说场景。我们让一个已经收敛的模型按顺序学一串新片段,每学完一段就在保留集上评一次。第一天、第二天看着还算稳,到第三、第四段的时候,困惑度开始抬头。等到整轮跑完再回头看最早学过的那些内容,PPL 已经是它刚学会时的七倍多。模型没有崩,输出依然通顺,但它对旧内容的把握被新内容冲掉了。这就是 7.6 倍的来源:一个平均意义上的退化幅度,不是某一类任务的极端值。

机制上,这个问题不新鲜,只是我们的架构没能自动绕开它。M 系列里承担记忆的是 fast-weight 矩阵,它是随推理和写入在线更新的,不是冻结权重。新片段进来时,这个矩阵被推动,而它同时承载着旧片段留下的痕迹。没有显式的保护机制,推动新记忆和抹掉旧记忆用的是同一套更新。梯度方向对当前样本是对的,对历史上出现过的样本就是干扰。我们在小规模上观察到的是典型的前摄干扰:学得越晚,越占上风。

一个具体的失败例子:我们让模型先记住一组带特定键的短事实,再喂它一批结构相似但键不同的新事实。新事实学得不错,回头查第一批键,命中率掉得比 PPL 反映的还要难看。这里暴露的是我们评估方式的问题——聚合 PPL 会稀释掉这种局部塌陷。所以我们现在的记录里同时留着分段的曲线,不只看一个总数。

Hebbian 可塑性这条路我们试过,结论是:在当前测试的规模上它是惰性的。我们按 Hebbian 规则实现了突触级的更新,跑下来,更新量几乎不动,对下游指标没有可见贡献。原因我们判断是尺度和信号强度的问题——在 125M 这个量级、在这个数据密度下,局部相关性的统计量被噪声盖住了。我们没有把它当成"原理错了"来处理,而是归档为"当前规模下不工作"。这跟之前那次纠正是同一类态度:把能复现的写清楚,把不能复现的标出来。

需要说清楚的边界:这两个数字都是在 M 系列的实验条件下得到的,不是产品承诺,也不是所有配置的通例。7.6 倍是一个观测值,5 倍是一条我们给自己划的线。惰性可塑性是在我们测过的配置上惰性,不代表换一个尺度一定还是惰性。

接下来我们盯两件事。一是给 fast-weight 的更新加保护,让旧痕迹在写入新记忆时不被无差别覆盖——这是工程问题,可以做对照实验。二是把可塑性的评估从聚合指标拆到分段和分任务,先确认"惰性"到底是真没动,还是动了但被平均掉了。两件事都还没有结论,等有了再写。

代码和结果在仓库里:awareliquid.ai,RESULTS.md。没做到的部分我们同样放在那。

一个工程师该从幽灵故事里拿走什么

幽灵故事听多了容易走偏。我们自己在评测里踩过这个坑:跑出跨窗口召回 0.56 那天,团队里有人顺口说“模型记住用户了”。后来把话说清楚——它记住的是用户确实说过的话,加上一个轻量语义标签,走的是 RAG 式召回。这套东西不带来理解、推理或情绪。工程师要拿走的第一个判断就是这条边界:记住内容和理解内容是两件事,架构上可以完全分开实现。

第二个可以拿走的是失效条件。同一条跨窗口、跨会话语义召回任务上,注意力和 LoRA 按构造就是 0.000——它们没有会话之外的存储位置,任务对它们不是难,是不可达。把这层快权重矩阵拿掉,召回从 0.56 掉到 0.008。这两个数字放在一起说明:记忆不是训练出来的副产品,它就是那个矩阵本身。评估一个“记得”的说法,先问它把状态存在哪、删掉那块状态还剩多少,答案会立刻现形。

举个我们内部讨论过的例子。假设 agent 在周一帮你改了一段配置,周三你新开一个会话问“上次那个超时设成多少了”。朴素做法是把历史拼进上下文,窗口一长就贵,跨会话更是直接丢。我们这条路的做法是把会话末的状态快照落盘,新进程加载后从那里继续。这个快照我们测过,逐位无损。注意这里的量级:状态是键值形态的片段记忆,不是把长上下文压成分布式表示,所以它不会给你长上下文语言建模的收益,不要指望用它替代长窗口。

机制上值得记住的区分在 O(1) 这一侧。无注意力的 O 系列,携带状态在 0.381 MB 上不随上下文增长,128k 时相比朴素方案小到 1008 倍(125M 规模上测的)。混合的 M 系列还带注意力,不是 O(1)。选型时把这句话抄在纸上:要恒定推理内存,看 O 系列;要全量基座质量加跨会话召回、参数开销约 1%,看 M 系列。两者不是一回事。

还有一组数字用来校准期待。持续学习目前约 7.6 倍困惑度退化,目标是压到 5 倍以内;Hebbian 可塑性在测过的规模上是惰性的。这些没有藏起来,写在研究页上。幽灵故事最容易让人忽略的恰恰是这类负数——它们决定了这套记忆在什么条件下会悄悄变坏。

最后一条是工程习惯:任何“它还记得”的演示,要求三件事同时给出——存储形态、删除实验、跨进程恢复的校验方式。我们的快照恢复是逐位无损,这一条可以被独立复现。拿不到这三样的演示,先当作叙事,不要当作架构。

仓库和说明在 github.com/awareliquid,LongMemEval 上 Recall@5 96.0%、检索零 LLM 调用,这些都可以自己跑。

仓库和站点在这里

这一节写仓库和站点。前面讲的都是结果,这里交代东西放在哪,以及哪些能跑、哪些还不能跑。

先给地址。

站点是 awareliquid.ai。研究页面、产品说明和我们发布的更正都在上面。那篇更正请认真读一下:早先我们报过一个“-28.5% adapter PPL”的增益,后来追查下来,增益来自普通 LoRA,而不是 MT-LNN adapter。研究页上已经改了,没藏着。

仓库方面,M1 的 README 和 RESULTS.md 是唯一的事实来源(source of truth)。凡是和这两份对不上的数字,请以它们为准,包括我在别处写过的。

RESULTS.md 里现在摆着的东西,我按重要性列一遍。

第一条,125M 规模的原生 MT-LNN 对比同参数、同数据、同步数、同优化器的 Transformer,验证集 PPL 是 299 对 436,低 31%。这条要连带它的四条限制一起看:单种子、欠训练、实际墙钟时间慢约 1.6 倍、参考 Transformer 是简单实现、没有 Mamba 基线。它是一个预算受限下的信号,不是 SOTA。

第二条,跨窗口、跨会话的联想召回。按构造方式,注意力和 LoRA 在这项上是 0.000。我们的三个种子分别是 0.62、0.43、0.62,均值 0.56。把快速权重矩阵去掉,掉到 0.008。快速权重就是记忆本身,这句话有实验支撑。

第三条,快照与恢复。把这份状态写盘,再在一个全新进程里读回来,loss bit-exact,无损。

第四条,O(1) 推理内存只在 O 系列(ARR)成立。携带状态恒定在 0.381 MB,跟上下文长度无关,128k 时实测比对照方案小约 1008 倍,测量规模 125M。混合架构的 M 系列仍然带注意力,不是 O(1),这一点我们不想含糊。

再举一个具体例子,说明为什么仓库里的文件比口头描述重要。

我最初看 O 系列的 0.381 MB,是在一个流式推理的测试脚本里看到的。脚本跑长上下文,显存曲线是平的。这个现象很漂亮,但它本身不构成断言。我去翻 RESULTS.md 里对应的记录,确认了测量规模是 125M、对照是什么、以及“只有 O 系列成立”这个前提。少了其中任何一条,这个数字就会被误读成整个产品线的特性。仓库的价值就在这里:它把这四条前提和那个数字锁在一起。

机制上说明一下我们怎么组织这些材料。

MT-LNN 的状态不是压缩后的上下文,而是情节式的键值记忆。快速权重矩阵在推理过程中被写入,跨窗口保留的是这些写入,所以会话结束后状态还在。这也是为什么快照能无损落盘——它是一个明确的数据结构,不是散落在 KV cache 里的东西。理解这一点,你就明白“幽灵”这个说法指的是什么:不是模型在背后继续思考,而是一份状态被留了下来,下次进程启动时被读回去。

同时也说明它不是什么。自动情节记忆是我们对你实际说过的话做 RAG 式召回,加一个轻量语义标签,它不带来理解、推理或情绪。MT-LNN 不给长上下文语言建模带来增益,状态是情节键值记忆,不是压缩的分布式上下文。

最后把没做成的也写在明面上。

持续学习目前 PPL 退化约 7.6 倍,我们的目标是压到 5 倍以内。Hebbian 可塑性在测过的规模上是惰性的。O 系列是研究预览,相对教师模型 PPL 是 2.15 倍。这些都写在 RESULTS.md 里,不会因为不好看就挪走。

想复现,从 README 的跑法开始,对数字去 RESULTS.md 对。想聊,站点上有入口。

地址:awareliquid.ai,仓库:github.com/awareliquid。

Top comments (0)