# MosaicMemoryCompress — 基于自然遗忘曲线的马赛克式对话压缩 > 版本: v1.0.0 | 状态: 稳定 | 最后更新: 2026-08-14 --- ## 一、问题 多轮对话中,LLM 上下文窗口随对话增长而膨胀。传统做法是: - **Session 管理**:让用户手动"开始新对话" → 丢失历史细节,用户认知负担重 - **滑动窗口截断**:只保留最近 N 轮 → 早期关键信息丢失 - **摘要压缩**:全量历史压缩为一团摘要 → 对话结构丢失,细节不可追溯 三者都有一个共同问题:**用户需要理解并管理 "Session" 这个概念**。对于非技术用户(如作家、创作者),这是不必要的认知负担。 --- ## 二、核心思想 > **让 Session 对用户不可见。通过模拟人类"自然遗忘曲线",实现一个逻辑上永不结束的对话。** 人类的记忆不是"要么全记得,要么全忘记"。越近的事记得越清晰,越远的事越模糊——但重要的事件即使久远,也能留下印象。 MosaicMemoryCompress 用大模型自身来模拟这个过程: - **近期的对话**:保留完整原文(用户最近的交互,细节最重要) - **稍远的对话**:保留消息骨架,内容做"去水分"(Light Compress——结构化截断,零 LLM) - **更远的对话**:多轮合并为一条叙事摘要(Heavy Compress) - **极远的对话**:压缩为长期摘要,细节随遗忘自然消散 对话窗口永不撑爆,用户永远感知不到 Session 的存在。 --- ## 三、参数模型 ```typescript interface MosaicMemoryConfig { /** 从第几轮开始触发 Light Compress,默认 10 */ lightStart: number; /** Light Compress 每次压缩多少轮,默认 30(与 heavy 对齐) */ lightWindow: number; /** 从第几轮开始触发 Heavy Compress,必须 > lightStart,默认 40 */ heavyStart: number; /** Heavy Compress 触发间隔。建议与 lightWindow 一致,默认 30 */ heavyWindow: number; } ``` ### 防抖动 压缩不在每轮触发,而是在阈值轮次集中触发: ``` 触发条件: 当前轮次 >= lightStart 且 当前轮次 % lightWindow == 0 → Light Compress 触发条件: 当前轮次 >= heavyStart 且 当前轮次 % heavyWindow == 0 → Heavy Compress 建议 heavyWindow = lightWindow,使 Light 和 Heavy 在同一轮次触发,保持结构简洁。 若两者不同,Heavy 触发时将合并多个累积的 Light 块,仍能正常工作但会有临时膨胀。 ``` 用户感受:light 压缩为结构化截断(毫秒级、零 LLM);仅 heavy 折叠时有一次 LLM 摘要调用(约 1-2 秒)。 --- ## 四、两层压缩 ### 4.1 Light Compress(轻量压缩) **触发**:轮次达到 `lightStart`,且为 `lightWindow` 的整数倍。 **操作**:对目标区间的 `lightWindow` 轮对话,做**纯结构化截断**—— 消息数量不变、消息结构不变、**零 LLM 调用**(2026-08-16 起,数据驱动: 真实对话 token 构成中 reasoning 33% + 工具参数 33% + 工具结果 24%, 文本仅 ~5%;结构化截断净省 ~46%,而 LLM 蒸馏 254 次调用仅省 5.6%): - reasoning_content:头尾各 30 字符(字段保留;DeepSeek 在工具轮回放它, 截断经 API 实测安全) - 工具参数 arguments:保留 JSON 壳,字符串字段截断到 120 - 工具结果:文本头 30 + 尾 30 - 系统注入:截断到 30 - 用户/助手文本:不动——留给 Heavy 区统一压缩 ``` 压缩前(工具结果示例): tool: "[2026-08-16] Code run failed: ReferenceError: require is not defined (eval at worker.cjs:887:31)...<数百行堆栈>..." (数万字) 压缩后(结构不变): tool: "头部30字符...…[truncated]…尾部30字符" (~70 字) ``` **关键设计**:消息的 `role` 不变、顺序不变、条数不变、工具配对 (tool_call_id)不变。文本内容零损失(留给 Heavy);结构化负载精确截断。 增量:已脱水消息带 `_distilled` 标记,重复触发自动跳过。 ### 4.2 Heavy Compress(重压缩) **触发**:轮次达到 `heavyStart`,且为 `heavyWindow` 的整数倍。 **首次 Heavy Compress(R = heavyStart)**:将最早被 Light Compress 过的 `heavyWindow` 轮对话(`heavyWindow × 2` 条消息)压缩为 1 条 user + 1 条 assistant 消息。 **后续 Heavy Compress(R > heavyStart)**:**递归合并**——将上一轮 Heavy 的输出(2 条消息)与最新的 Light 块合并,重新压缩为 2 条消息。因此 Heavy 块永远只有 1 个,覆盖范围持续扩大但消息数恒定。 ``` 对齐设计(默认 10/40/30/30): Light 区 = 恰好一个窗口宽([R-40, R-10),30 轮)——进入 Heavy 区的内容 必然已脱水 Light 首跑 R=40(脱水 [0,30));Heavy 首折 R=70(折叠已脱水的 [0,30)) 此后 70/100/130… 同轮触发(同一次 pre-step、同一次缓存 miss) 稳态:摘要对 1 + 40 轮(≤41 用户消息/轮) ``` **进入 Heavy 区的永远是已脱水内容**:Light 脱水领先 Heavy 折叠一个窗口 (Light 在 R=40 处理的那批轮次,正是 Heavy 在 R=70 要折叠的那批)——Heavy 的 LLM 输入更小、聚焦文本要点,折叠质量与成本双优。 --- ## 五、行为时间线 以默认配置 `lightStart=10, lightWindow=30, heavyStart=40, heavyWindow=30` 为例: ``` 第 1-39 轮: R < 40 → 无任何动作(零成本;light 首跑点在 R=40) 第 40 轮: Light 首脱 [0,30)(30 轮结构化脱水,条数不变;Heavy 区为空,不折) 用户感知:毫秒级(纯截断,零 LLM) 第 41-69 轮: 无触发(防抖窗口内);light 区持续平移,新成员等下次统一脱 第 70 轮: Light 脱 [30,60) + Heavy 折叠 [0,30)(已脱水的轮次 → 摘要对) 同一次 pre-step、同一次缓存 miss;用户感知:heavy ~1-2 秒 LLM 稳态达成:摘要对 1 + 40 轮 第 100/130…: 同轮 Light 增量脱 + Heavy 折叠,节奏永远对齐,稳态 41 条 第 60+ 轮: 每 30 轮折叠一次(heavyWindow=30),消息数在 62~122 之间振荡(light 区 20 轮脱水内容随折叠重置),永不膨胀。 第 15,000 轮和第 60 轮的消息总数完全相同。 ``` ### 第 60 轮后的消息结构(稳态) ``` [0] system prompt(始终不变,大小取决于宿主) [1-2] Heavy(1-10) ← 2条(递归合并起点,覆盖第 1-10 轮) [3-42] Light(11-30) ← 40条(最近被 Light 压缩的 20 轮) [43-62] Raw(51-60) ← 20条(最近 10 轮完整保留) [103] user_61 ← 新消息开始追加... ``` **无论对话进行到多少轮,三层结构始终是 2 + 40 + 20 = 62 条消息(31 个用户轮)**(纯对话每轮 2 条;含工具轮时更高但恒定)。 --- ## 六、设计哲学 1. **信任大模型的判断力**:不靠规则截断、不靠 TF-IDF 评分。什么重要、什么可以省略——让 LLM 自己判断 2. **消息骨架不丢**:Light Compress 保留 user/assistant 交替结构,对话的因果链条始终可追踪 3. **自然遗忘,而非暴力截断**:越远的越模糊,越近的越清晰。重要信息由宿主的外部持久化机制按需保留(见 README 的 Architecture Boundaries) 4. **对用户零感知**:不需要理解 Session,不需要手动管理上下文窗口 --- ## 七、参数约束 仅做一条运行时检查: ```typescript if (config.heavyStart <= config.lightStart) { throw new Error('heavyStart 必须大于 lightStart'); } ``` 其余边界场景(如 `heavyWindow < lightWindow`、`lightStart < 1`、压缩区间重叠等)**不做运行时检查**,由文档说明推荐值和约束,使用者自行承担配置选择。 > 性能与信息保持的**实测数据**见 [benchmark/README.md](../benchmark/README.md)(确定性模拟 + 真实 LLM 抽查)。本设计文档不再重复数字,避免与实测矛盾。 --- ## 八、形式化模型与工程简化(理论支撑) > 本节是算法的**理论支撑**,不是实现计划。它解释了 MosaicMemoryCompress 为什么采用 > 两段式(Light + Heavy),以及 Heavy 的递归合并为什么在数学上成立。 > 精确的多段式模型(下文"精确模型")不实现——详见 8.3 的工程选择。 ### 8.1 精确模型:位置即年龄、节点可压缩 把消息数组视为**记忆单元序列**: - **每个 user 节点 = 一个记忆单元**(工具轮不增加 user 节点,不破坏不变量) - **位置即年龄**:从数组尾部(最新)向头部(最古)计数,位置越深 = 越久远 - 每次防抖动窗口(`window`,默认 10)触发一次压缩: - 窗口结束时恰好有 `window` 个新节点滚入压缩区 - 因此**任何颗粒度 g ≤ window 都能在当次窗口凑满**(10 个节点按 2 压 1 → 5 个节点;按 5 压 1 → 2 个节点;按 10 压 1 → 1 个节点) - 颗粒度 > window 的档位没有独立意义——"更多个压成 1 个"总可以由"window 压 1"增量重复实现 - **最深层档 = 增量 Heavy**:每次窗口把最老的 `window` 个节点并入一个大小恒定的摘要;摘要不再增长,数组大小振荡收敛、**永不突破上界**(微积分式的极限逼近) 颗粒度分段与人类记忆的对应: | 记忆阶段 | 人类对应 | 颗粒度 | |---|---|---| | 数秒-数分钟前 | 清晰如新 | raw(原样) | | 数小时前 | 开始模糊 | g=1(逐节点脱水) | | 数天-数周前 | 细节丢失 | g=2 / g=5(多节点合并) | | 数月前 | 只留关键 | g=10 / g=20 | | 多年前 | 只剩印象 | 增量 Heavy(永不增长) | 人的大脑容量有限却从不"爆掉"——年老时觉得时间越过越快,正是旧记忆被持续 压缩的体感对应。这个模型是对人类遗忘的离散模拟。 ### 8.2 V1 两段式 = 精确模型的 2 档特例 - **Light(g=1)** = 精确模型的第一档:逐节点脱水,节点数不变 - **Heavy(g=∞)** = 增量 Heavy 的具体实现:递归合并(摘要的摘要),每次窗口 只把新滚入的 `heavyWindow` 轮并入旧摘要,输出恒定 2 条消息——这正是 8.1 所述"window 压 1 的增量式",只是把 10 压 1 换成了任意 heavyWindow 压 1 因此 V1 的稳态推导(消息数、token 恒定)是 8.1 精确模型的直接特例。 ### 8.3 工程选择:为什么停留在两段式(奥卡姆剃刀) 真实的人类-AI 对话轮次分布: | 场景 | 轮次 | 三段式覆盖 | |---|---|---| | 简单事务 | 3-5 轮 | 不触发(<30),零开销 | | 复杂功能点 | 20-30 轮 | 边界触发 | | 大型项目 | 40-70 轮 | Light + Heavy 各一次 | | 千轮对话 | 现实中不存在 | 多段式为之设计,无意义 | 多段式(g = 1,2,5,10,20...)需要为不存在的千轮场景付出:更高的实现复杂度、 更多次的 LLM 压缩调用(每次合并都是一次模型调用,成本随分段数上升)。收益与 成本不成比例。**两段式(V1)是成本与效果的平衡点**:覆盖真实场景的全部形态, 同时保持实现简单、压缩调用次数最少。 **结论**:精确模型是 V1 的数学依据与理论支撑;V1 是精确模型在真实工程场景下的 正确选择。若未来出现真正的千轮对话需求,多段式可作为配置级扩展(颗粒度分段 表)启用,无需改动算法主体——但当前不实现。 ### 8.4 未来:渐进遗忘分档(理论,未实现) 结构化截断使 Light 区**零成本**——两段式简化不再被 LLM 调用成本所迫, 而是运行中的选择。更细的"位置即年龄"阶梯随时可用,例如: - 20–30 轮:结构化负载前后各保留 200 行 - 30–40 轮:前后各保留 100 行 - 40–50 轮:前后各保留 30 行 每一档遗忘更多结构化负载(reasoning、参数、结果、注入)——正是模型的 推理越来越不会用到的内容。**任何档位都不压缩用户/助手文本**,对话核心 含义始终完整;进入 Heavy 区后每轮折叠把远古区域摘要成"永不过时的内核", 对话得以无限延续。 V1 采用两段式(Light + Heavy)先观察真实效果,再做更细的分档。 ### 8.5 成本模型:缓存断点税(2026-08-16 实测,2026-09-06 真实数据修正) **机制(2026-08-16 实测,仍有效)**:在自动前缀缓存的 provider(DeepSeek、 OpenAI)上,任何对已发送历史的就地修改都会断裂缓存前缀。两种实测形态: Light 的中间 1:1 替换保留到替换点的前缀(下一次请求仍 ~97% 命中); Heavy 的头部折叠整段破坏前缀(结构性——请求按时间序、折叠目标必然在 头部、前缀从第 0 token 匹配——布局无法规避,详见下方实证)。 **实证(2026-09-06,生产会话 85cd44e7,对齐 10/40/30/30 配置)**: - Light 脱水:706 节点替换,surface 553K→313K(占用 55%→39%);一次性 miss ≈ 79.8K tokens ≈ $0.015-0.04;后续 ~30 轮每轮省 ~240K surface ≈ $0.20/窗口 → **净赚约 10 倍** - Heavy 折叠:1000 节点/247,829 tokens → 摘要(~10 秒 LLM);下一次请求 cacheRead 490K→2.7K、miss 165K ≈ $0.046 + LLM 估算 $0.05;surface 491K→168K(49%→~25%)→ 30 轮窗口省 ≈ $0.27 → **净 +$0.15-0.17/窗口** - **稳态结论:成本下降而非上升**——生产中未观察到"成本约 2 倍"区间; 早期倍数推算是 heavyStart=30 单次折叠时代的外推,已被真实数据取代 (零税替代形态:重置时刻增强——见 ROADMAP M5。) ## 九、实证:同一事件在三种记忆载体下的对照 > 本节记录一次真实发生的对照实验,细节已抽象化(参与者、讨论主题均不还原), > 仅保留方法论层面的结论;实验方式可复现——任意"多轮设计讨论 + 两种压缩方式" > 的组合均可重演同一对照。 **事件**:两位参与者——人类设计者与 AI 助手——围绕"压缩粒度设计"进行了多轮讨论: 粒度档位(2 压 1、5 压 1、10 压 1)、各档位与人类记忆阶段的对应关系,以及 "先实现两级、更细档位留作配置级扩展"的工程结论。讨论成果最终写入设计文档。 **处理**:数轮之后,同一段对话历史被两种方式分别压缩: - **存档型**(行业通用):阈值触发的一次性全量摘要,压缩后仅存一份结构化简报; - **记忆型**(本算法,实验时配置——早期 30 轮 raw 窗口):最近轮次原样保留、其后逐条脱水、更早汇入恒定大小摘要。 **对照结果**: | 记忆载体 | 对该事件的记忆 | 形态 | |---|---|---| | 人类设计者 | 档位、对应关系、过程与结论,鲜活准确 | 近记忆天然逐字——对该参与者而言这是"最近"的事 | | AI(存档型压缩后) | 仅一句结论:"多段式存在但刻意不实现" | 档位、过程、实验数据全部丢失,须翻阅设计文档方可恢复——"看过笔记的新人" | | AI(记忆型压缩后) | 十余条消息原样保留,含设计者原话与实验数据 | 讨论位于 raw 区,未被压缩触碰,无需任何外部文档 | **结论**: 1. 人对"最近发生的事"记忆逐字鲜活,对古早事件只留教训与规则——这是 §8.1 档位表的行为学证据; 2. 存档型压缩留存语义(规则/结论)、丢失事件(过程/细节),且损失不可见—— 模型不知道自己忘了什么,也无法补偿; 3. 记忆型压缩与人脑行为一致,因为近期窗口从未被触碰:同一事件下,AI 凭上下文 即可复述讨论,无需翻笔记; 4. 因此"最近 N 轮不压缩"不是保守,而是对话连续性的真正来源——连续性不依赖 任何单次摘要的质量,而依赖近期保真。