忆时塔(Chronomem)设计笔记 · 上篇

扁平存储与一次性压缩之间,还缺一种记忆

这套设计我写成了 position 论文(AiraXiv 2610.0035),
对应的实证论文是下篇 的主题。
代码与完整技术设计:atomgit.com/ystyle/chronomem 。
本文是博客版,讲动机与设计取舍;严格定义与参考文献见论文。

一个跨周、跨月持续干活的 Agent——迭代一个代码库、写一篇论文、长期维护一个项目——最缺的能力很朴素:记住很久以前发生的事。

今天的外部记忆方案基本分两派,而且它们的失败方式正好相反:

  • 扁平存储:全都存下来,按近因或相似度检索。细节保全了,但没有任何压缩——过了几个月,没有任何一层能告诉你「这一阵子大概发生了什么」,除非把全部内容读一遍。
  • 一次性压缩:定期把整段历史压成一份报告。压缩有了,但证据毁了——决定被记住了,而「为什么选它、当时怎么做的」这些原始事件没了,且无法重新推导。

忆时塔(Chronomem)想占的是两者中间那块地。我把设计写成了 position 论文,把测量写成了实证论文;这篇讲设计。

一、日常里早就有答案了

这个设计不是我凭空想的——它抄的是两种早就存在的日常机制。

日报体系。 干活的记录 → 日报 → 周报 → 月报。每一层都比下一层简洁,而且每一层都能独立回答「这个尺度上发生了什么」:想知道今天干了啥,看日报;想知道这个月干了啥,看月报,不用把三十天的日报重读一遍。

财务汇总报表。 这个更严格,性质也更值得借:账本永不删除,报表是渐进重算出来的,而且每张报表都能审计回底层分录。报表不是账本的「有损替代」,而是账本之上的一个可再生成的视图。

忆时塔把这两件事工程化到 Agent 记忆上。从报表实践里,我刻意迁移的是纪律而不是聚合语义:

纪律 在忆时塔里的对应
明细层不可变 原始条目 append-only,永不改写
可审计的下钻 每条摘要都带 refs,沿引用链能走回原始条目
幂等的渐进重算 物化是幂等的,重复跑不会产生第二份

这里有个关键差异要说清楚,否则会低估难度:ERP 的合并报表是对结构化分录、按法定固定日历做确定性求和。而这里每一层要用 LLM 去总结任意的 JSON 事件和自然语言记录——汇总本身成了不确定的。粒度链要按记忆类型自己配,而「导航」与「压缩」之间的成本差异,也从实现细节变成了首要的设计变量。

二、塔长什么样

一句话:记忆按时间堆叠成塔,一层一个物化粒度,越久越粗、越久越高。

忆时塔架构:客户端经 MCP 接入单一守护进程,两个独立调度器分别物化引用链与语义摘要
忆时塔架构:客户端经 MCP 接入单一守护进程,两个独立调度器分别物化引用链与语义摘要

图 1 · 架构。客户端经 MCP 接入单一守护进程;两个独立调度器分别物化一个类型的两个 series——引用链(零成本、分钟级)与语义摘要(有 LLM 成本、按类型 opt-in)。摘要存于自己那条 append-only series,并通过 refs 指向更细层,这正是递归总结得以成立的原因。

粒度链(每个记忆类型可自定义)
    week   ──┐   越久越粗
    day    ──┤
    1h     ──┤
    30min  ──┤
    10min  ──┘   越久越高
       ↑
    原始条目(append-only,永不删除)

每一层同时干两件事:

  1. 导航结构:持有指向更细层的引用,一路走到底就是原始条目;
  2. 可选的语义摘要:一眼能读懂「这个尺度上发生了什么」。

粒度从 {10min, 30min, 1h, 3h, day, week, month} 里取,默认 10min → 30min → 1h → day → week。

粒度链是按类型配的,这点很重要。 高频通道(比如 agent 活动流,一分钟好几条)要保留分钟级层级,才能支持「当天内精确回想」;而稀疏通道(比如一天一条的工作日志)用 day → week 就够了——否则会建出大量几乎是空的细桶,白占空间。

week 是默认的语义总结封顶。理由很实在:一整年的周级摘要只有约 52 条,加载便宜,而「回忆一整年」这种需求极少需要更细。

三、核心主张:回想不是一种操作

这是整篇设计的支点。

大部分人把「回忆」当成一个动作:给个查询,返回内容。但我认为这里必须拆成两条成本结构完全不同的路:

精确回想 语义回想
做法 沿引用链下钻到原始条目 读 LLM 生成的窗口摘要
服务端 LLM 不调 调(生成或读取时)
保真度 无损 有损(压缩)
返回体量 原始体量,不压缩 摘要体量,真正变小
前提 无 摘要必须已存在

第一条路是「导航」,不是「压缩」。 这点很容易被误解,我特意在论文里写死:精确下钻能把粗窗口解析成原始条目,但返回的就是原始体量——它没有让任何东西变小,只是让你能按时间定位。

第二条路才是真正压缩粗窗口的形态,而且它有个硬前提:摘要必须先存在(之前生成过,或者现在花钱生成)。

一个必须说破的成本账

「精确回想不花 token」这句话,只在服务端一侧成立。真实账本是双侧的:

服务端 客户端
精确回想 不调 LLM 返回的原始条目要注入 prompt——随体量增长
语义回想 生成/缓存摘要时调 LLM 注入摘要文本——更小,但非零
结构物化 不调 LLM 无(客户端根本看不到桶)

所以精确回想并非「端到端免费」:它把 token 成本转移给了客户端,换来保真;语义回想则是把成本留在服务端,代价是丢细节。

这个区分回头看很平凡,但我认为现有系统把它模糊了,模糊的代价是三个具体问题:

  1. 触发策略:结构物化(引用链)是每分钟跑、零 token 的作业。语义总结正相反——没有护栏的调度器会在空窗口或低价值窗口上悄悄烧 token,还会对注定失败的窗口反复重试。
  2. 语义漂移:对摘要再总结很便宜,但会漂移;从原始数据重算保真,但贵。如果这两者不显式暴露,就成了一个静默的质量问题。
  3. 上下文卫生:服务应该只存和取,永不主动把内容塞回客户端上下文。取多少,由客户端决定。

四、摘要是时序条目,所以可以递归

这是整个设计里我最满意的一点。

mem/summarize 读一个窗口(按粒度+锚点,或按显式 from/to 处理「任务阶段」这种不规则窗口),让 LLM 产出结构化摘要——固定几个锚点章节:决策 / 结论 / 待办 / 状态变更 / 关键词——然后追加一条 SummaryRecord 到该类型的 .summary series。

关键在于:这条摘要是追加到一条时序 series 里的,和原始条目一样是一等公民。

于是它能被再次总结:

自底向上的递归总结:每层由下一层总结而来,recompute 可绕过链条
自底向上的递归总结:每层由下一层总结而来,recompute 可绕过链条

图 2 · 自底向上的递归总结。每一层由它下面一层总结而来;更细层缺失时按需现算;recompute 绕过链条、直接从原始条目重建窗口。每条摘要本身都是 append-only,链条上没有任何东西被销毁。

原始条目 → S(10min) → S(1h)
        → S(day) → S(week)

这就是结构塔在语义上的对应物,实现了递归、非破坏的压缩——链条上没有任何东西被销毁。

而且总结默认自底向上:在总结一个粗窗口之前,服务会先确保它内部每个更细子窗口都有摘要(缺的按原始条目先补出来),再用这些子摘要聚合。好处是粗粒度总结的成本是「每个分组一次调用」,而不是把整个窗口重读一遍。

摘要记录里除了内容,还存了生成口径:{strategy, chunks, inputTokens, clippedEntries}——记录这次是一步直连产出还是分块 map-reduce 聚合、以及为预算截断了多少条目。这类字段平时不起眼,出问题时是唯一能定位的东西。

想绕开链条也行:recompute 直接从原始条目重算该窗口,跳过整条链。链式转述便宜但会漂移,重算保真但费 token——把这个权衡交给调用方,而不是替它悄悄选一个。

五、语义总结的五道护栏

前面说语义总结会「悄悄烧 token」,这不是假想。所以忆时塔把它做成按类型 opt-in(新建类型默认关闭),开启后用五道护栏约束:

  1. 只总结最近一个已闭合窗口,且有沉降期——窗口还在被写入时,绝不总结;
  2. 按窗口、按分组幂等,成功即清空重试账;
  3. 空窗口零成本,直接跳过;
  4. 连续失败会退避,到达上限后登记为「已放弃」并留痕——而不是每十分钟永远重试;
  5. 每轮提交数有上限,积压不会变成无界突发。

内置的 worklog 类型开箱即用(每个已闭合的自然日、每个项目一条摘要),任意新建类型默认关闭。回想时遇到「摘要缺失」也是显式路径(summarize_on_miss),不偷偷替你算。

六、为什么做成 MCP 服务

忆时塔不是库,是一个常驻守护进程,只暴露一个 streamable-HTTP MCP 端点。任何支持 MCP 的客户端都能接入:

// opencode.json
{ "mcp": { "chronomem": {
    "type": "remote",
    "url": "http://127.0.0.1:8900" } } }

我认为把记忆做成协议而不是私有库,本身就是系统层面的贡献:异构工具可以共享同一个记忆底座,而它们之间零代码耦合。今天用 opencode 干活、明天换 Cherry Studio 聊天,记忆是同一份。

存储上是单一时序数据库,每个记忆类型占两个 series——原始条目一个,摘要一个。类型可以自己建,名字白名单 [A-Za-z0-9_/-](防路径穿越),比如:

worklog          工作日志
proj/decisions   项目决策
tool/errors      工具报错

工具面目前是 13 个 mem/*,覆盖类型管理、追加、结构回想、语义总结、以及总结任务的异步状态查询。

七、和别的记忆系统差在哪

这块得说实话:忆时塔的每个组件都借自别处。

  • 分层时间窗 + 物化聚合是时序数据库的标配(TimescaleDB 连续聚合、Prometheus 降采样),忆时塔基于自研的 tsdb 库复用了这套机制;
  • 分层记忆管理不是新想法(MemGPT 引入分层上下文;Generative Agents 把记忆流与反思结合;A-MEM、Mem0、HiMem 等各有发展);
  • 明确区分「精确回想 vs 语义回想」作为显式成本模型、摘要是一等公民时序条目、以及把整个底座标准化在 MCP 后面——这三条是我的差异点。

还有一条轴值得单独说:主题记忆 vs 时间记忆。

「LLM wiki」那类系统按主题/语义词条组织知识,类比维基百科——把来源编译成一份由 LLM 持续维护的持久 wiki。忆时塔按时间组织,类比账本。这两条轴是互补的,不是竞争的:

论文是「过程」(时间),参考手册是「知识体」(主题)。

我认为很多「记忆」系统把这两条轴混为一谈了,而轴的选择应该跟随用途——你要回忆的是「这件事怎么发生的」,还是「这个东西是什么」?

八、哪些做完了,哪些没做

写设计文章最容易犯的错,是把「设计」讲得像「已实现」。所以逐条标清楚:

组件 状态
记忆类型、类型级粒度链 已实现(运行时动态,加类型不用改码)
日历对齐的自然窗口(day/week/month,时区可配) 已实现
引用链物化 + exact 下钻(服务端零 LLM) 已实现
语义摘要 + 递归自底向上总结 已实现(recompute 可绕链条)
带护栏的定时自动总结 已实现(沉降/幂等/退避/每轮上限)
MCP 工具面(13 个 mem/*) 已实现
内嵌 Web 控制台(与 MCP 同端口) 已实现
会话活动自动采集 已实现(宿主插件形式随仓分发)
数值聚合(指标型零 LLM 概览层) 规划中(当前只有 count/latest/sample)
保留期 / 淘汰 规划中(类型可声明,但不生效,实际全量保留)
MCP resources 规划中(目前只有 tools)
鉴权、加密、多租户 规划中(当前单用户、仅本机)

两处我觉得必须主动交代的局限:

一致性受物化边界约束。 粗粒度视图只反映到它上次物化为止的记录,之后写入的条目不在其中。它会返回一个陈旧但自洽的窗口——看上去完全合理,不带任何错误信号。所以我把「物化视图的一致性」当成待测量的显式属性,而不是默认成立的前提。

保留期只是声明式的。 类型元数据里能声明保留窗口,但没有任何机制执行它——实际是全量保留。

九、下一步

position 论文以一份评估计划收尾,没有任何结果:受控的长周期 Agent 任务对照、沿摘要链的语义漂移研究、以及我自己部署上的工作日志用例。

那部分我已经执行完了——结果里最有意思的不是忆时塔表现多好,而是我最初的测量方法错了两次,且错到会把结论反转。

下篇讲实测:近因基线的失效为什么是断崖而非衰减、为什么「周摘要和天摘要逐字节相同」意味着根本没压缩、盲测 6/6 背后是什么、以及两个让结论反向的测量陷阱和一个关于 LLM 判定者的负面结果。