版本:1.0 | 日期:2026-09-13 | 姊妹篇之一(缓存侧)
文档定位:MFS(Memory Fusion System,单用户本地部署的个人 AI 记忆系统)的前缀缓存(KV-cache)命中优化专题,回答"按什么顺序发"。姊妹篇《摘要只配当导读:让 AI 长对话不失忆的分层压缩架构(附业界对比)》回答"发什么"(如何让 200 轮对话不丢事实、不失忆)。两篇是同一套上下文工程的两个正交侧面;交界问题(压缩事件对缓存的影响)以本文 §5 为权威。整理自内部设计文档《MFS-前缀缓存(KV-cache)命中优化与注入分层设计》(v2.5),所有数据均为生产实测。
0. 一句话总纲
大上下文"太贵"的原因不是上下文长,而是前缀形状变了几次。 前缀缓存让旧字节按 0.1 折续费,但任何一次前缀改动 = 改写点之后全部内容全额重付。MFS 的答案是:注入分层(每个注入源都有唯一合法落点)+ 显式计价(每次改动都过回本闸)+ 全路径同构(ws 与 REST 共享同一套前缀)。优化目标是消灭"变的那几次",不是缩短上下文。
1. 缓存命中的本质与损失公式
provider(如 DeepSeek)按请求输入前缀缓存:两次请求从开头起相同的 token 段命中,从第一个不同 token 起全部 miss。
缓存损失 ≈ Σ(每个变化注入段: 该段位置之后的所有 token)
推论:
1) 变化越靠前 → 损失越大(把后面整段稳定历史都拖下水)
2) 变化越频繁 → 损失次数越多
3) 段本身越长、其后内容越长 → 单次损失越大
唯一铁律:请求消息前部只放"跨轮不变或低频变化"的内容;一切每轮/每次 user 消息都会变的注入(含当前时间),一律放 user 之后(尾部)。
实证基线(E8,4,440 次生产请求、输入 2.9 亿 token):整体命中 81.1%,但 29.6% 的低命中请求贡献了 84.9% 的 miss token——花钱的不是上下文太长,而是前缀形状变了几次。单次形状变化实测连续 1.75 次低命中、每次 miss ≈26–27k token。优化目标是消灭那"几次",不是缩短上下文。
2. payload 七区结构:请求的真实形状
┌─ ① system[0] 静态基座 ──────────── 人格/规则 + 三个准静态源(AI 名/曾用名/扩展工具说明)
│ 逐字节比较守卫:内容没变不重写;DB 异常时 memo 兜底复用上次值(宁陈旧不抖动,§6)
├─ ② 知识槽 messages[1] ─────────── 用户画像 + 价值观 + 关系/自我认知 + 技能池 + 已验证教训
│ 会话首建即定格,哈希守卫(内容逐字节相同 → 零操作);此后永不再整槽重建
├─ ③ 段块(压缩产物)─────────────── 事件触发才变,一次变到底(§5)
├─ ④ 原话轨 ─────────────────────── 与③同窗口重建,只在压缩事件动(§4.3)
├─ ⑤ gap 拼回 ───────────────────── 压缩时轮间隙语义拼回
├─ ⑥ 尾部原文(append-only)──────── 每轮新增消息 + 工具结果;旧 summary 同窗剥离
├─ ⑦ volatile_tail(最后一条独立 system)─ 唯一每轮必变点:当前时间 + 联想命中/预测/感知
│ 三个 turn 级暂存 note,消费即清零、历史零累积
└─ tools(请求级前缀)────────────── 79 工具 ≈13.9k token,集合冻结同一份
时间注入专述——时间是系统里唯一每轮必变且必须存在的动态数据(粒度到分钟),实际有两处入口,待遇不同:
| # | 入口 | 位置 | 缓存效应 |
|---|---|---|---|
| T1 | 当前时间 + 后台任务进展 + 联想/预测/感知三个临时 note | ⑦ volatile_tail 末条 | 跨分钟的两轮必 miss 这一条,但只 miss 它自己 |
| T2 | user 消息正文前挂载的状态块头部时间 | ⑥ 区(append-only) | append 不破坏前缀;每轮 miss 增量 ≈ 一份新状态块,历史累积由压缩事件同窗剥离治理 |
时间三不铁则:不进 system[0](每跨分钟整段 miss);不进知识槽(槽一变,其后全部历史 miss);不就地改写历史本体——只能在构造层(发送视图组装时)注入尾部。
七区按铁律就位后,每轮 miss 增量的健康账 = 新消息 + 新 summary + volatile_tail 三段之和,此外零 miss。
3. 注入分层 L0–L3:每个注入源都有唯一合法落点
| 档 | 内容 | 落点规则 |
|---|---|---|
| L0 准恒定 | 静态基座 + tools 冻结集 | 不因 context/轮次/时间重写;逐字节守卫 |
| L1 低频 | 画像/价值观/关系/自我认知/技能池/已验证教训 | 仅当内容真变才重写槽(哈希守卫) |
| L2 中频 | 段块 + 原话轨 | 变化由压缩事件统一调度("少动、动到底") |
| L3 高频/每轮 | 任务进度/事件/话题/联想命中/预测/感知/时间 | 只准进尾部,变化只 miss 尾部新增段 |
动态源治理的四条经典战役(每条都是"内容本无辜、位置定生死"):
- 时间三不:时间绝不进 system[0](每跨分钟整段 miss);绝不进知识槽(槽一变,其后全部历史 miss);绝不就地写历史本体。只能在构造层注入到尾部。
- 活计数器出槽:任务进度
Turn 5 · step 2/8这类每步都变的活状态,曾无条件进知识槽——每步打碎其后全部历史。治理:出槽走尾部 turn 级暂存通道,消费即清零。 - 预测/联想出槽:预测结果曾整槽覆盖(顺带把画像也冲掉)——改为尾部临时通道后,槽首建定格,丢画像 bug 随之消失。
- 隐藏时钟识别:教训段曾带"相对年龄天数",按天静默变化——哈希守卫落地后被正确识别为"内容真变才重写槽"(天级一次 miss,语义正确);而感知提醒、自主任务进度这类读后即消费的数据则必须出槽走尾部。另一条路是零注入:抽象层产出搜索线索交给检索工具按需调,不进 payload。
两条红线(缓存契约,任何新注入内容过此关才能进 prompt):
- 随输入动态生成的内容(联想/预测/感知/未来新层)只准走尾部 volatile_tail 通道——禁止进 system[0]、知识槽、历史消息本体;
- 工具集合变化只准发生在重启边界——运行中禁止任何路径改写 tools 数组(一次并组 = 作废其后 ~13.9k token 前缀)。
4. 缓存经济学:把每一次失效当显式成本
4.1 KV-Cache 0.1 折续费账本
前缀缓存让旧字节按 0.1 折续费(Anthropic cache read = 0.1×,DeepSeek 同量级;本地模型的"收费"是 prefill 延迟):
payload(压缩后稳态,128k 档示例)
├─ 系统头 + 段块散文 ┐
├─ 原话轨 ~10-20k │ 这些内容字节不变(旧部分)
├─ 尾部逐字原文 ~21k │
├─ ←───────────────────────┘ KV-Cache 命中:每轮按 0.1 折续费
├─ 新消息 ~1-2k ← 每轮全价(省不掉的)
└─ 尾部新追加的字节 ← 全价,但只有新增量
每轮全价的只有:新消息 + 压缩事件当轮的 miss(改写点之后全部缓存失效,那次按全额付——即"过路费")。
4.2 过路费模型的推论
E8 数据(§1)说明总损失由"形状变化次数"主导,由此推出三条:
- 总过路费 ∝ 事件次数 × 单次过路费,与上下文长度无直接关系;
- 少动、动到底:要么不动(台阶式静止,0.1 折纯赚),要动就一刀动满预算(单次过路费摊到最大的 freed 上);
- 官方印证:"context engineering and prompt caching are inherently at odds"(OpenAI/Claude)——压缩与缓存天然对冲,必须显式计价。
4.3 台阶式淡出 vs 滚动窗口
原话轨的内容会随时间淡出,但淡出方式是台阶式不是渐进式:
❌ 渐进式(滚动窗口——明令禁止)
轨内容 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
───────────────────────────→ 轮次
每来一条新消息就挤出一条最老的
= 头部每轮都在变 = 每轮前缀缓存全断
✅ 台阶式(实际做法)
轨内容 ▓▓▓▓▓▓▓▓▓ ▓▓▓▓▓▓▓▓ ▓▓▓▓
─────────────────────────────────→ 轮次
↑压缩事件 ↑压缩事件
平地(几十轮一字不动)→ 一跳(一次性换血)→ 平地
三个关键点:
- "平地"有多长:两次压缩事件之间轨内容零变化(间隔约 15~30 轮)——这段日子每轮都是 0.1 折纯赚;
- "跳"发生在哪一刻:只在压缩事件当轮一次性完成整个换血——头部挤出最老几条、尾部收进新几条,没有中间过渡态;
- 为什么跳变不额外付费:跳变那轮恰好是段块改写轮——那轮 payload 中部变了,后面所有内容的缓存本来就全部失效。把原话轨的变动绑在段块改写的同一列车上——同一次全价,两份变动(搭便车)。
4.4 编号即寻址
每条原话带 [#uid] 编号、时间正序、块头声明取回通道:
【用户原话轨】…(块头:更早被挤出的原话用 recall_history(start_uid, end_uid) 按编号取回)
[#102] 帮我把连接池参数改成 30
[#109] 不对,是 30 个连接,不是 30 秒超时
编号双用途:既是分隔符又是淡出后的检索参数——模型读到 [#156],就知道更早被挤出的原话在编号轴哪个位置,可定点捞回。防前缀失效细节:块头刻意不带任何统计数字(段数/覆盖量都不带)——保证块头逐字节恒定。
4.5 为什么尾部不砍更狠
把尾部 21k → 10k,每轮省 1.1k 等效;代价是 payload 更快逼近压缩线 → 压缩事件提前变频繁 → 每次事件全额 miss(~50-60k 全价)摊回来基本打平,还丢逐字近场保真。便宜的 0.1 折续费 vs 昂贵的全额搬家费,永远偏向多留稳定前缀、少触发搬家。
5. 压缩 × 缓存交界:少动、动到底
压缩管线把"历史改写"从随机变成了有节奏的段块事件,交界规则:
一次压缩事件 = 段块改动点起全部 miss(128k 窗 ≈ 85k token 单次)
回本闸双门槛:freed ≥ max(0.35 × 压缩前体量, miss ÷ 40 轮)
(既要收缩比例过线,又要省量按 40 轮摊销能回本)
"少动、动到底":合并、收拢、原话轨重建、旧 summary 剥离——
全部同一窗完成,绝不连续多轮各动一点
(每动一次 = miss 一次 ≈85k)
静默窗口期:压缩挪到空闲时预压,避免与正常对话轮的 miss 叠加
账本分离:压缩操作本身的成本(LLM 调用)与它引发的 miss 损失
分开记账、分开可查
为什么不定期压缩:定期压缩(每 20 轮 / 每 20k)产生次满事件——每刀吃不到预算,事件次数翻倍而单次过路费几乎不变,总过路费 ∝ 频率。打满才压,越频繁越贵。频繁压也不提升保真——保真由四层兜底(事实轨/原话轨/尾部原文/定点取回)保证,与频率无关。
6. "宁陈旧不抖动":故障期的缓存保护
一个容易忽视的深水区:DB 抖动时的缓存行为。查询异常返回空段 → 本次请求 miss → 下次恢复内容变回 → 再 miss("一抖双 miss")。治理:段级 memo 兜底——异常时复用上次成功值(宁陈旧不抖动),正常路径每请求仍全量读库,memo 仅异常瞬间生效。实测 DB 抖动模拟:整槽与上次成功构建逐字节一致。
7. 双路径同构:ws 与 REST 必须共享同一套前缀
系统有 ws 长连接与 REST 单轮两条发送路径。审查发现的系统性缺陷:REST 路径的时间注入曾就地改写消息头部、工具集合逐轮并组、注记随滑窗漂移——每请求全 miss。治理后两条路径注入策略完全同构、共享同一套前缀缓存。其中最后一处隐蔽病灶:注入块内部慢变段前置、时间后置——修复前时间堵在注入块开口,跨分钟打碎其后全部慢变内容;修复后跨分钟只 miss 时间行附近的小尾,实测命中率从全 miss 救到 99.8%(§8.3)。教训:缓存优化必须覆盖全部发送路径,只治一条等于没治。
8. 可观测性:不靠感觉,靠账本
8.1 三层观测
- Token 账本:每次 LLM 调用记录输入/输出/缓存命中/缓存 miss/来源(chat / analysis / compact),压缩操作单独记
compact行——"操作成本"与"引发 miss 的损失成本"分开可查,180 天滚删。 - 变化率探针:随每轮发送记录四段指纹(system 哈希 / 知识槽哈希 / 段指纹 / 消息数)——首轮记基线,前部指纹变化 = warning,段变化 = info,稳定不记。跑一周即可发现隐藏的逐轮变化源。
- 前端透明度:实时占用百分比、一次性清理提示、压缩账目列表、压缩进行中状态条("正在整理上下文…" → "已整理")。
8.2 缓存字段解析契约(一个真实事故)
统计代码只认 DeepSeek 的扁平字段(prompt_cache_hit_tokens),而生产网关的 GLM 用嵌套报法(prompt_tokens_details.cached_tokens)→ 账本 hit/miss 记双零 → 前端显示"命中 0%"(修复后实测真实 86.1%)。
治理:三级兼容解析归口为唯一定义(扁平 → 嵌套推导 → 未上报),禁止各调用点内联;provider 名读运行时配置而非启动快照。双零 ≠ 命中 0%——"provider 未报缓存字段"必须显示"未上报",不得冒充"0%"。
8.3 量化验收(节选)
| 验收线 | 判据 |
|---|---|
| 触发带 | 估算触发压缩时真实占用 ∈ window × [0.85, 0.95) |
| 覆盖完整性 | 200 轮会话每一轮"被压缩的输入 == 被移除的轮次"可断言 |
| 缓存健康 | 普通轮前缀命中 ≥99%;全 miss 事件数 == 段创建次数 + 合并次数(多了 = 有隐藏变化源) |
| 事实保真 | 撒入的随机数字与路径逐字保留 100% |
| 确定性 | 同输入跑两遍 → 相同覆盖集与相同段文本 |
| 机制实测 | 段生成压缩比 5.4%;全链路排演 payload -44.1%;重复发送缓存命中 99.3%;写回体量 miss 128k 档 -84% / 1M 档 -99% |
缓存优化终验(生产网关实测):注入分层治理 + 时间后置修复后,跨 4 分钟的非流式请求命中率 99.82%(miss 仅 62 token = 尾部时间行)、流式跨 2 分钟 99.79%;修复前同样的跨分钟请求全量 miss。
9. 关键技术决策速览(缓存侧精选)
| # | 决策 | 选择 | 放弃的方案 | 核心理由 |
|---|---|---|---|---|
| D4 | 合并机制 | 只留整体锚定收拢 | 分层成组 | 后者是 20 倍的缓存破坏事件 + "摘要的摘要";有界性不依赖它 |
| D9 | 写回体量 | 一次压满输入预算 | 每次压固定 1,000 token | 段块在 payload 前部,碎事件 = 每次全额 miss |
| D10 | 召回通道 | uid 定点取回 | 盲目检索式召回 | 编号即寻址(§4.4),不依赖"模型主动查自己忘了什么"(实测失配率 26.5–54%) |
| —— | 时间落点 | 尾部 volatile_tail 末条 | system[0] / 知识槽 / 历史本体 | 时间是唯一每轮必变的数据,落前部 = 其后整段被拖下水(§2) |
10. 明确不做的事(边界即设计)
- 不做滚动窗口——原话轨/知识槽若每轮滚动更新,前缀每轮全断,省的钱全部赔回去;
- 不做递归层级合并——"摘要的摘要"放大幻觉,且是 20 倍的缓存破坏事件;
- 不做"每轮都在变的前部注入"——一切动态内容只准走尾部(§3 两条红线);
- 不做两套注入策略——ws 与 REST 必须同构,只治一条等于没治;
- 不让"双零"冒充"命中 0%"——provider 未上报缓存字段必须如实显示(§8.2)。
11. 结语:缓存工程的三条元经验
- 缓存是经济学问题,不是工程细节。损失公式是公理,"少动、动到底""宁陈旧不抖动""时间放尾部"这些看似琐碎的规则全部从它推导;回本闸、账本分离、变化率探针让每次改动都可计价、可验证。
- 位置定生死。内容本无辜——时间、计数器、预测结果放进槽里才是事故之源。检验标准是"每轮 miss 增量的健康账":新消息 + 新 summary + volatile_tail,此外零 miss。
- 全路径 + 账本对得上。只治一条发送路径等于没治;全 miss 事件数必须精确等于段事件数,命中率优化靠账本验证——而不是"感觉变快了"。
本文与《摘要只配当导读:让 AI 长对话不失忆的分层压缩架构(附业界对比)》互为姊妹篇:压缩决定"发什么",缓存决定"按什么顺序发"。
举手提问