跳到正文
节能君
返回

Observational Memory:给长会话一份可追溯的记忆

最近切换到 Pi 后,我经历了几次非常严重的、发生在上下文压缩后的目标漂移。情况基本上是:我明确了一个实现目标,这个目标下面有多个讨论分支。我和 AI 陷入其中一个分支,进行了联网 research 和多个 prototype 之后,它完全遗忘了一开始的目标,仅仅记住了这个分支的内容。又或者数次曲解我的意思,擅自扩大、缩小实现范围,甚至完全偏离目标。

我知道,这是我一个不太良好的使用习惯。Agent 能有效利用的上下文长度是有限的,因此深度分支、research、prototype 等大量占用上下文窗口的事情,最好使用独立 Agent 会话,然后 handoff;或者利用 subagent 进行协调调度与隔离。

但我很好奇,难道大家都没遇到过这个问题吗?只能通过“良好的习惯”来缓解吗?顺着这条线研究下去,我发现了一个 Pi 的上下文管理扩展:pi-observational-memory。

为了精简,后面我将 pi-observational-memory 简称为 pi-om,将 Observational Memory 简称为 OM。

说来这个项目真是与我有缘。我其实前两天刚刚刷到了 Eero Alvar 的配置分享视频 《Pi Setup After 6 Months of Use》。当时想学习,但时间有限,只是收藏了还没看。后来回看才发现,他也独立实现了一套 OM,还配了炫酷的仪表。这种思路上的相遇让我很惊喜。

使用体验

那么,实际用起来怎么样?先说说我在日常开发中的感受。

我有一个超长 Pi session,从 8 月 29 日持续使用到 9 月 9 日,超过 11 天;算上它继承的早期历史,日志里的对话已经横跨约 14 天。

截至 9 月 9 日晚,这份会话文件里记录的主 Agent 请求,累计输入、缓存读取和输出约 23.49 亿 token,Token 费用按 API 正价计算约 1,618 美元。原本那些漂移的失败案例,就发生在这段工作的会话里面。

当我开始使用 pi-om 以后,我仿佛得到了一个可以永久持续下去、且几乎不发生严重漂移、compaction 又超快的 session。至少在我深入过的这些分支里,Agent 仍然记得我们从哪里来到这里、接下来应该怎么办。一些明确的要求,比如开发后启动服务、告诉我如何验收,隔了很多轮也还能接上。曾经我的上下文焦虑是很严重的,我会希望整个澄清对齐过程中不发生任何压缩以保证质量,一次大的执行也从干净的上下文开始以尽可能提升初始水准,而现在这些焦虑都消失了。

缓存表现同样让我惊讶,这个会话整体命中率约为 96.89%,有 3,109 次请求的输入缓存命中率 ≥ 99.5%。

它给我的感觉就是无感、快速、准确、节约,一时间真的找不到缺点。

因此我也很好奇:它到底是怎么工作的?原理是什么?为什么效果这么好?

摘要的摘要

先看最常见的一类上下文压缩机制:上下文通过 append-only 的方式不断增长,到达某个阈值后,再让模型生成摘要,为后续对话腾出空间。

下图以 256K 的上下文窗口、80% 的压缩阈值为例,省略 system prompt:

上下文压缩示意:在固定窗口内累积到阈值后,将 A、B 压缩为摘要①并保留 C;继续累积后,将摘要①与 C、D 再次压缩为摘要②并保留 E

具体什么时候开始压缩、留下多少原文,由 Harness 决定。以 Pi 原生 compaction 为例,它会比较上下文 token 数与 contextWindow - reserveTokens,默认预留 16,384 token;同时默认保留约 20,000 token 的最近消息,并按消息与工具调用的边界选择切点。

很多 Harness 还会截短工具输出、分段总结,把可以从文件中重新读取的内容移出上下文,或者给原始资料留下索引。这些优化能减少摘要模型要处理的内容,但只要沿用“旧摘要 + 本轮历史 → 新摘要”的方式,旧摘要仍然会被反复概括。

对我的工作流而言,最明显的是两个问题。

第一个是等待。 压缩有点像一次需要暂停工作的 GC:当前任务停下来,等摘要生成,再继续。复杂会话里,这个停顿尤其打断思路。

第二个更严重:摘要的摘要。 摘要是有损的。一次压缩可能还保住了大意,却丢掉某个不起眼的前提;下一次再丢掉一个被我否决过的方案;再下一次,“我们只是讨论过”就可能变成“用户已经决定”。

这些细枝末节积累起来,就可能变成严重的漂移。模型自身的理解和指令遵循也会出错,而聊了很久以后丢掉原本记得的约束,尤其让我头疼。

LongMemEval 测试

Mastra 的研究报告里,创始工程师 Tyler Barnes 说,他从 2025 年 10 月起就在自己的 Coding Agent Harness 中日常使用 OM 原型。2026 年 2 月 9 日,Mastra 正式发布 OM,并公开了实现与评测。

pi-om 则是受到 Mastra OM 启发、为 Pi 扩展系统编写的独立实现。

Mastra 的结果

报告中的 LongMemEval 成绩也很吸引我。

LongMemEval 用来测试聊天助手跨多次对话的长期记忆能力:先提供按时间组织的历史,再问它能不能记住其中的信息、区分偏好变化、推理时间关系。这里讨论的 LongMemEval-S 有 500 道题,每题附带约 40~50 个历史 session,标称约 115k token;具体长度会随 tokenizer 和输入包装变化。

Mastra 的报告列出了以下成绩,其中 OM 使用 GPT-4o 作答、Gemini 2.5 Flash 负责观察与反思:

方案报告所列得分
检索增强 Mastra RAG(topK 20)80.05%
仅证据会话 Oracle82.40%
Observational Memory84.23%

其中,Oracle 是一个理想检索参照:预先挑出包含证据的 session,只把这些相关历史交给模型。后续的阅读、推理和作答仍由模型完成。

OM 能接近 Oracle,是这组结果里最有吸引力的地方。

我的测试

我想进一步看看,Pi 和 pi-om 实现,搭配最新的模型效果如何。于是用 Pi 和固定版本的 pi-om fork,在 LongMemEval-S 上做了一次最终压缩实验。完整代码、结果矩阵和分析位于 pi-longmemeval-bench。

测试使用 500 道题,主模型选用 gpt-5.6-sol / xhigh 和 gpt-6-astra / xhigh 。压缩条件下,先回放历史,在给出问题前做一次手动压缩,再让模型回答。

表中百分比是答对数除以 500,括号内为判对题数。详细配置及已知问题保留在实验报告中。

条件gpt-5.6-sol / xhighgpt-6-astra / xhigh
仅证据会话 Oracle94.80%(474)96.00%(480)
Pi 原生最终压缩26.20%(131)73.20%(366)
Pi OM 最终压缩74.40%(372)74.60%(373)

来源:as-run v1 结果报告。

结果里有几个挺有意思的现象:

  1. 原生压缩的表现随模型变化很大。 Sol 从 95.40% 降到 26.20%,Astra 则从 96.00% 降到 73.20%。最主要的原因是 Sol 的原生摘要中位长度只有 2,332 字符,Astra 为 10,240 字符。也就是说 Aatra 生成了更长的摘要保留了更多信息。模型能力确实有显著提升。
  2. OM 对 Sol 的改善很明显,对 Astra 则小得多。 在这次结果里,OM 相比原生压缩分别高 48.2 和 1.4 个百分点;启用 Recall 后又分别高了 3.0 和 3.6 个百分点。这说明 OM 对于“不善总结”的模型可以带来巨大的提升,而这些提升会被模型本身的增强抹平。

pi-om 的工作原理

我们可以把 pi-om 理解成一套持续维护的工作笔记。

你和 Agent 持续讨论、写代码、跑测试;与此同时,后台把值得记住的事情记下来,再从中整理出长期需要遵守的规则。等上下文快满了,就直接用笔记里面的内容来替代原始信息。

下面以我使用的 pi-om V3 fork 为例,看看一条用户要求怎样被记住、留下,又怎样找回原话。

pi-om 工作原理:后台从对话和工具结果中记录事件、提炼长期记忆并标记归档;压缩时 Fold 生成记忆,最近原文直接保留,Recall 按 ID 回查来源

Observer:记录事件

假设我们刚完成一轮开发:读了几个文件,改了代码,启动服务,然后在浏览器里验收。这一轮可能产生很长的工具输出,但真正影响后续工作的,往往只是其中几句话。

Observer,也就是“观察者”,负责从新产生的对话和工具结果里挑出这些信息,写成一条条 Observation,可以先把它们理解为事件笔记。

比如,下面两条就来自我的真实会话记录,英文部分已译成中文:

第一条记录了一次请求;第二条记录了一个不能违反的约束。至于浏览器页面上那几千字的内容、重复打印的工具回执,如果没有新的重要信息,就不必原样带走。

所以,Observer 做的是分清哪些事情值得留下:目标是什么,用户确认了什么、否决了什么,任务做到哪里,出现过什么关键错误。

每条笔记还有时间、重要程度,以及指向原始消息的来源记录,方便以后回查。已有笔记也会交给 Observer 参考,减少重复;这一轮没什么新信息,它也可以不记。

Reflector:提炼长期记忆

事件笔记会越积越多。我们真的需要让 Agent 一直记着“某天启动过一次服务器”吗?未必。相比之下,“验收必须用哪个浏览器”就可能影响后面很多轮工作。

这就是 Reflector,也就是“反思者”的任务。它阅读仍然保留的 Observations 和已有 Reflections,提炼值得长期留下的偏好、约束、决定和经验。

前面的浏览器要求,后来就形成了这样一条真实 Reflection,下面同样是中文译文:

[aec6a0749f17]:进行浏览器验收时,用户明确禁止 Computer Use,要求使用 Orca Browser;即使 Computer Use 可用,也不能用它替代。

两种记录看上去很像,侧重点却不同:

一条长期规则可能来自多次提醒;一次临时操作,也可能只需要作为事件保留。Reflection 会记录支撑它的 Observation,因此提炼以后,仍然能沿着来源找到当初为什么这么说。

这会不会又变成“摘要的摘要”?区别在于,这个版本不会每次重写整份旧记忆:Observer 从新的原始消息中追加事件笔记,Reflector 再追加有依据的长期记录。新信息加进来,已有记录则尽量保持原样。

观察和反思随着工作推进分批进行。默认触发阈值分别是约 10k、20k token 的上下文增长,达到条件后在后台运行。具体的用量计算和回退规则见调度代码。

Dropper:清理旧记录

如果只记不清,笔记本迟早也会塞满。所以还需要 Dropper,负责挑选可以从活跃记忆中移走的 Observation。

比如,旧的进度已经被新进度替代,某次报错已经解决,几条记录反复表达同一件事,或者重要含义已经被 Reflection 妥善保留。这些都可能成为清理的理由。

清理时仍需要检查重要细节有没有被保留下来,即使一条记录已经被 Reflection 引用过。Dropper 只能选择要移走的记录 ID,保留下来的文字维持原样;没有安全的候选,也可以少删甚至不删。

在这个版本里,它会在成功写入新 Reflection 后,检查活跃 Observation 是否还超过目标,默认目标是 10k token,满足条件才尝试清理。

Drop 做的是把旧笔记存档,而不是销毁。

扩展只写下一条删除标记。原始 Observation 和它关联的消息仍留在本地会话文件中,后面需要时,还能回查。至于这条笔记什么时候从主 Agent 眼前消失,就要说到 Fold 了。

Fold:生成当前记忆

前面的记录、提炼、删除标记,都先保存在本地的记忆账本里。可以把账本理解为这套笔记的完整存档。

但存档里有,不代表模型现在就看得见。模型每次只能看到请求中真正发给它的上下文,并不会因为磁盘上的文件更新了,就自动知道里面多了什么。

第一次压缩前,主 Agent 仍然看原始对话;压缩过后,它看到的则是上次带过来的记忆,加上后续产生的新对话。后台继续整理自己的笔记,不会每写一条就重排主 Agent 的上下文。

等 Pi 触发 compaction,pi-om 才从账本中选出这次要带走的记忆,按固定格式放进摘要,并保留最近的原始消息:

压缩前:[上次的记忆][越积越长的对话和工具结果]
压缩后:[这次选出的记忆][保留的最近原始消息]

这个根据存档生成当前记忆的过程,叫 Fold(折叠)。

只要能生成非空的可用记忆,这一步就不调用 LLM,也不等后台任务结束;如果没有可用记忆,则退回 Pi 原生摘要。

这就解释了我为什么感觉 compaction 快得离谱:不是摘要模型突然变快了,而是需要模型做的整理工作,已经提前在后台完成了。

为了兼顾笔记的稳定性和旧记录的清理,Fold 分为两种方式。

Normal Fold:补入新记录

普通折叠会纳入符合本次压缩范围的新 Observation,但 Reflection 和 Drop 仍沿用上一次 Full Fold 时的状态。也就是说,后台可能已经提炼了新规则、标记了旧记录要移走,主 Agent 暂时却还看到旧版本。

为什么要故意慢半拍?这和缓存有关。模型服务通常可以复用请求中相同的前缀;如果前面的记忆段落频繁被改写、删除,哪怕意思差不多,这段内容也可能无法继续复用。Normal Fold 尽量保持既有记忆的文字和顺序稳定,减少这种扰动。实际还能命中多少缓存,则取决于供应商规则、有效期,以及请求具体变了哪里。

如果还没有发生过 Full Fold,那么 Normal Fold 只展示 Observation,暂时不应用 Reflection 和 Drop。

Full Fold:应用反思和清理

当一次 Normal Fold 准备展示的 Observation 笔记本身达到默认的 20k token,就会改做 Full Fold:把截至本次压缩范围的新 Reflection 纳入,同时应用相应的 Drop,移走不再需要随身带着的 Observation。

举个简化的例子:假设后台新增了事件笔记 C,从 A、B 中提炼出规则 R,并确认 A 可以移出活跃记忆。普通折叠时,只补入 C,A、B 仍然保留,R 暂不纳入;完整折叠时,才把 R 带进来、把 A 移出去,B、C 则继续保留。选择留什么的工作早已在后台完成,Full Fold 负责让结果生效。

Normal Fold 与 Full Fold 对比:前者补入 C 并保留 A、B;后者新增规则 R、移出 A,继续保留 B、C,归档的 A 仍可回查原文

Full Fold 同样不调用模型,也不重新改写保留下来的记录。两种折叠的区别,就在于这次允许哪些账本变化进入主 Agent 的上下文。

20k 决定何时触发 Full Fold,折叠后的大小则取决于能安全移走多少记录。Observation 可能仍然很多,Reflection 本身也会继续增长。

压缩时不等待后台,也带来了记忆缺口的风险:尚未记下的重要信息可能没被带走;记录按批次判断是否能进入摘要,跨过“保留最近原文”的切点时,也可能整批未被纳入。

Recall:回查原文

假设又聊了很多轮,Agent 看到笔记里写着“浏览器验收必须用 Orca Browser”,却想确认:这是用户明确要求的,还是我当时自己选的?

这时,记忆里的来源关系就派上用场了。主 Agent 可以调用 recall,传入一条已知记忆的 12 位 ID,沿着它找到来源。

前面那条真实记录的关系是:

Reflection:aec6a0749f17
    ↓ 由这条事件笔记支持
Observation:be70506dff69
    ↓ 来自原始用户消息
“不要使用 computer,用 orca browser”

所以,留下来的不只是“相信我,当时就是这样”,还有一条能追溯到原话的路径。需要核对命令、报错、决定和理由时,也可以这样回查。即使 Observation 已经被 Drop,只要 ID 已知、账本和来源还在,这条路径仍然可用。

Recall 提供的是按已知 ID 回查来源的能力,不包含关键词或语义搜索。没有被记录、也没有关联 ID 的信息,还需要从原始历史中另行寻找。

到这里,整条链路就连起来了:从对话里记下事件,从事件里留下长期规则;压缩时带走整理好的笔记,需要精确细节时,再沿着来源回查。

回到日常开发

研究完以后,我反而觉得,OM 最有价值的地方,是把日常记录、提炼和清理,与压缩时切换上下文这件事分开了。记忆可以慢慢整理,工作不必每次都停下来等它重新讲述过去。

它仍然有损:Observer 会漏记,Reflector 会过度概括,Dropper 也可能判断错误。回查来源让这些问题有机会被发现,记录本身仍需要核对。后台整理还会消耗 token 和费用,本地账本也需要保存和维护。

但和每次都重写整段过去相比,我更喜欢这种设计:保留的记录尽量不改写,需要遗忘时明确标记,需要细节时能够回查。

我仍然会把研究分支交给独立会话或 subagent,把重要决定写进项目文档。pi-om 维护的是当前会话分支的记忆;文档则让其他 Agent 和以后开启的会话也能接上这些决定。

只是,现在我不用那么频繁地担心:“聊到这里,它是不是已经忘了我们为什么出发?”

对于喜欢在同一个 session 里不断追问、推演、原型验证,再回到主线继续做事的我来说,这已经是很大的改变。我要的并不是一个永远记住所有聊天内容的 Agent,而是一个走了很远以后,仍然知道我们在做什么、哪些路已经走过、哪些决定还没有做出的合作者。



上一篇
DeepSeek V4.1 Flash 初体验