Files
DevFlow/docs/02-架构设计/专项设计/压缩裁剪缓存友好化-设计-2026-10-11.md
T
lxy cb23061088 新增: 跨会话召回四层架构与脉络卡
- 会话终态提炼: 删除前+停靠双挂点,快照与清水位同临界区
- 目标快照: 恢复引导按最终有效意图生成,旧目标不复活
- 知识检索项目维度软排序,WorkingContext 持久化迁移
- 脉络卡: published 知识按项目聚合纯读侧,带入上下文经输入框显式注入
- 压缩与缓存能力在主循环/子代理/提示词三面接线
- 两份专项设计文档与看板销账
2026-10-11 22:27:20 +08:00

20 KiB
Raw Blame History

压缩裁剪缓存友好化 — 设计

2026-10-11 · 只读勘察产物 主题:上下文压缩与裁剪以破坏 provider 前缀缓存为代价 → 在保住预算安全的前提下最大化缓存命中 行号快照:2026-10-11 工作区(context/mod.rs 与 context_helpers.rs 有并行代理在改,实施前须对齐最新行号)


〇、文档定位与既有工作衔接

本文档是「上下文分层组装器-落地方案-2026-10-10.md」病灶④(裁剪后前缀缓存整体失配)的机制细化,并新增覆盖压缩链路的一次压尽问题。该方案已给出验收口径:「稳态下请求前缀只追加、不位移;裁剪以批量滞后发生」——本文档给出达成该口径的具体算法、参数推理与断点布点。两文档不冲突:分层组装器治①(真值 token)②(O(n²))③(预算随窗口)⑤(召回),本文档专治④+压缩,可与 P2.1/P2.2 并行实施(见§八协调注)。

另:看板 AC-EFF-C1-2「每轮全量重发 O(n²) 部分缓解(cache_control+稳定前缀后第2轮起命中,增量滑窗留待后续批)」与「警示消息打断前缀缓存」两登记项,均由本文档方案覆盖。


一、两个缺陷的准确定性与证据

缺陷1:滑动裁剪致前缀缓存整体失配

build_for_request(crates/df-ai/src/context/mod.rs:185)超预算时走贪心最小裁剪:for unit in &units { if history_tokens - removed <= available { break } removed += unit.token_sum }(mod.rs:211-219)——每次只裁到刚好塞下为止。对话每增长一条消息就重新越过 available 一点,裁剪边界 trim_end 随之前移一个单元,发送视图的第一条消息每轮都变。前缀缓存(anthropic 显式 cache_control 与 openai_compat 自动前缀缓存同理)要求从第 0 字节起逐字节相同,首条即变 → 整段历史永远 miss。

已缓和但不治本的既有修正:

  • 淘汰补偿摘要从头部 insert(0) 改尾部 push(user 角色,mod.rs:250-261),注释明言治「命中率掉到个位数」;
  • push_system_anchor 尾部追加不位移前缀(agentic/helpers.rs:28-44、context_lifecycle.rs:309-313);
  • tool_view_decisions 冻结压缩决策防跨轮翻转(agentic/mod.rs:2004-2005),compress_cache 按内容哈希记忆化摘要结果(stall_detector.rs:368-379)→ tool_result 视图摘要跨轮字节稳定,已不构成缓存威胁。

残余根因:裁剪边界本身逐轮爬行。注意一个连带效应——淘汰摘要每轮即时重生成,其内容在两次裁剪之间是静态的(被淘汰集不变 → extract_top_keywords 排序确定性,context_helpers.rs:284-288);「摘要逐轮漂移」正是裁剪逐轮爬行的投影。块状化后漂移自动收敛到裁剪事件点。

缺陷2:一次压尽

压缩触发:available*6/10 < history_tokens(agentic/context_lifecycle.rs:129-136),后台 LLM 摘要成功后 compress_old_messages(protect_start) 把 [0, len-6) 的全部 active 消息一次性标 compressed(context_lifecycle.rs:308、兜底路径 :442;compress_old_messages 实现 mod.rs:670-699)。sanitize step0 按 is_active 白名单过滤(sanitize.rs:40-43;is_active= status None/Active,df-ai-core/provider.rs:91-99)→ 被压消息从发送视图整体蒸发,视图前缀从第 0 条起全变,一次压缩 = 历史段全量缓存重建。信息损失同样一次集中(用户体感「丢历史」)。


二、现状机制盘点(设计依赖的既有事实)

机制 位置 事实
预算公式 context_helpers.rs:154-178 budget = (max_tokens − output_reserve) × safety_ratio;默认 128K/8K/0.85 → budget ≈ 101,836;available = budget − sys_tokens(sys 5–10K → available ≈ 92–97K)
裁剪触发 mod.rs:198,214 history_tokens > available(100% 硬门)
保护区 context_helpers.rs:683 PROTECT_COUNT = 6(≈最近 2 轮);context_lifecycle.rs:53 另有一份本地副本 6,双源待统一
淘汰单元 mod.rs:612-642 三元组原子(头+尾+紧随独立 assistant),压缩定位/分段标记/裁剪共用
压缩触发 context_lifecycle.rs:129-136 0.6×available + has_compressible_messages + 非压缩中
压缩边界回退 mod.rs:676-683 compress_old_messages 内建单元边界回退(只少压不多压)——分批压缩可直接复用
摘要补偿 context_helpers.rs:332-364 淘汰摘要 ≥100 字符才产,硬顶 1000 字符(:692);关键词提炼确定性
system 断点 anthropic_compat.rs:127-145 开关开 → 顶层 system 包 blocks + cache_control
消息级断点 anthropic_compat.rs:243-247,339-376 打在末条 user 之前相邻一条(末条 user 是漂移边界:易变尾段/本轮新问题,打它永远不命中);注释明言预算 4 个 cache_control,现用 2 个
缓存观测 anthropic_compat.rs:723-731 usage 已映射 cache_read/cache_creation——命中率观测零成本可取
开关范式 prompt.rs:657-695 settings KV df-ai-anthropic-cache,parse_cache_flag:空/true/1=开,false/0/杂值=关;进程级 AtomicBool 同步;cache_split_provider_type 按 provider gate;关=逐字节旧行为
易变尾段 prompt.rs:709-736、agentic/mod.rs:1983-1990、stream_relay.rs:791-795 日期/清单/augmentation/知识在每次 send 时构建一次(commands/chat.rs:417,996,2209,2476),loop 内多轮稳定,以 user 角色追加消息流末尾
system 折叠 anthropic_compat.rs:128-133,153 关键发现:存储态所有 role=System 消息(不论位置)被抽离折叠进顶层 system(sys.join)。压缩摘要/关键词兜底/话题标记/催办全走 push_system_anchor(ChatMessage::system(...))(context_lifecycle.rs:313,449、agentic/mod.rs:1869,2510,2557)→ 每次此类事件都改写顶层 system → BP1 连带全失配。淘汰摘要已因同因改 user 角色(mod.rs:254-256 注释),压缩摘要等锚点未跟进

三、设计总览

三机制分层,各管一段:

                 历史水位轴(×available)
  0          0.55        0.75        1.0(=available)
  |           |           |            |
  |<-- 压缩目标水位 C_low  |            |
  |           |<-- 批次≈0.2A          |
  |           |           ↑ 压缩触发 C_high(深清,后台,保语义)
  |                       |            ↑ 裁剪触发 1.0(应急,view-only)
  |                       |            |←→| 裁剪块≈0.2A → 裁到 T=0.8
  • 裁剪=应急(build_for_request,view-only):只在压缩失灵(后台失败连续多轮/未及生效)时兜底,块状跳变;
  • 压缩=深清(后台 LLM,改存储 status):常态水位管理,分批渐进;
  • 稳态下裁剪永不触发(0.75+批内增长 < 1.0),历史段前缀在两次压缩事件之间逐字节稳定。

四、方案一:块状裁剪

4.1 算法

替换 mod.rs:211-219 贪心循环,改「目标水位 + 轮边界吸附」:

  1. 触发条件不变:history_tokens > available(硬安全门,mod.rs:214 同式)。
  2. 目标水位 T × available(T 默认 0.8):累计淘汰单元 token,直到 history_tokens − removed ≤ T×available 且当前边界吸附到轮边界。
  3. 轮边界锚点:单元边界 unit.end 处,若下一时刻消息(未淘汰首条)role=User,则该 end 是轮起点——整问整答同生同死,淘汰摘要按轮覆盖语义完整。
  4. 若扫到保护区(protect_start)仍无同时满足两条件的边界(病态:单轮 token 均值 > 块大小),回退现行为:取满足硬预算 available 的最后单元边界(任何边界,保证不破安全底线)。

两次裁剪之间:trim_end 不动 → 被淘汰集不变 → 淘汰摘要内容不变(extract_top_keywords 确定性)→ 视图前缀逐字节稳定 → BP2 每轮命中、openai_compat 自动前缀缓存每轮命中。

4.2 块大小取值推理

块大小不设独立常量,由水位差导出:块 = (1−T) × (budget − sys_tokens),随模型窗口自适应缩放(与 output_reserve/safety_ratio 的关系:二者先折算进 budget,块是其再切片,无双重调参)。

默认参数下 1−T=0.2 → 块 ≈ 18–19K tokens。设单轮增长均值 Δ(本应用工具密集,2–10K 常态):

T 块 稳定窗口 ⌊块/Δ⌋ 命中率≈窗口/(窗口+1) 单次裁剪信息损失
0.9 ≈9K 1–4 轮 50–80% 最小
0.8 ≈19K 2–9 轮 67–90% 中
0.7 ≈29K 3–14 轮 75–93% 大

T=0.8 是拐点:再往上窗口塌缩(裁剪频度回升、每次裁剪的「全量重建税」频度变高),再往下单次丢历史变狠。对照今天:超预算后命中率 ≈0%(首条每轮变)。估算误差 ±15%(context_helpers.rs:26 自述)被 20% 水位差吸收,不会刚裁完就再触。

4.3 块边界锚点选型对比

候选 评价
轮边界(推荐) 语义完整(问+全工具链+答同生死);淘汰摘要按轮覆盖;边界是稳定地标,只在裁剪事件移动;三元组原子性由 build_eviction_units 已保证,轮吸附只是再加一层
消息数 块 token 分布不可控(30 条可 5K 也可 80K),裁剪频度与稳定窗口忽大忽小;仅配作病态回退
纯 token 水位 作为停止条件必要(§4.1 第 2 步),但单独作边界会劈轮;与轮吸附组合使用

五、方案二:分批压缩

5.1 双水位滞后 + 批次

  • 触发 C_high = 0.75×available(现 0.6):提高压缩间隔——压缩事件=全量缓存重建税,频度直接决定命中率;0.75 仍留 0.25A 给后台 LLM 压缩完成并下轮生效的提前量(后台化设计 context_lifecycle.rs:30-31 的既有前提)。
  • 批次目标 C_low = 0.55×available:每批压最旧一段,压到 history − C_low×A 的 token 量为止,即每批 ≈ 0.2A ≈ 19K tokens(与裁剪块同刻度,共用推理)。
  • 批边界计算:从最旧起累计淘汰单元至批次 token 量,轮边界吸附(同§4.1 第 3 步);batch_end ≤ len − PROTECT_COUNT(保护区语义不变)。传入 compress_old_messages(batch_end)——其内部单元回退(mod.rs:676-683)与占位未闭合跳过(compress.rs:65-66)零改动复用。
  • 滞后再武装:压后 history=C_low < C_high,需再增长 0.2A 才触发下一批 → 天然防连发;is_compressing 重入标志、head_id 版本哨兵(context_lifecycle.rs:70-77)、L0-3 后台化全部不动。
  • 关键词兜底路径(context_lifecycle.rs:437-449)同步改传 batch_end:失败降级的损失也从「一次压尽」收敛为「压一批」。

5.2 缓存语义:为什么分批仍然值得

压缩从视图头部移除消息,新前缀=[摘要锚点]+[剩余],任何一次压缩都是历史段全量重建,分批不减小单次破坏面。分批的收益在别处:

  1. 单次信息损失有界(0.2A≈19K),摘要质量随输入变小而升,用户「丢历史」体感摊薄;
  2. 摘要锚点改为追加链(见§六 BP3):第 k+1 次压缩把新摘要接在摘要链尾,链之前的 [system+链头] 前缀仍命中——一次压尽则每次都从零重建;
  3. 两次压缩间隔拉长(0.75 触发+0.2A 滞后 vs 现 0.6 触发即压尽再爬坡),事件频度下降。

5.3 与块状裁剪的先后序(分工)

常态:  0.55A ←压缩批次— 0.75A 触发 … 增长 … 1.0A 裁剪(不该到)
失灵:  压缩连续失败(关键词兜底也失败/后台挂)→ 历史爬到 1.0A
       → 块状裁剪应急跳到 0.8A(丢真历史,无摘要)→ 给压缩再争取一批窗口

裁剪是唯一硬安全门,保持 100% 触发不动;压缩是语义保全的常态路径。二者水位关系(T=0.8 > C_high=0.75)保证:只要压缩机制活着,裁剪永不出场。


六、方案三:缓存断点布点

anthropic 预算 4 个 cache_control(anthropic_compat.rs:349),现用 2,建议用到 3:

断点 位置 什么变化会打断它
BP1 顶层 system(现有) system blocks 尾块(anthropic_compat.rs:137-141) ① custom_prompt 变更(稳定段含自定义段,prompt.rs:727);② 任何 System 角色锚点事件(压缩摘要/关键词兜底/话题标记/催办——折叠进顶层 system,见§二末行);③ 提示词文本随代码版本变更
BP2 消息级(现有) 末条 user 前相邻一条(:350-357);有淘汰摘要时落摘要上(摘要两裁剪间静态,反而更稳) ① 裁剪边界移动(块状化前每轮/块状化后仅裁剪事件);② 压缩事件;③ namespace LRU 淘汰的 notice 替换改写历史中段(agentic/mod.rs:1965-1982,低频);④ 新近度豁免滚动——末 2 条 tool_result 豁免过期转压缩时一次性改写(stall_detector.rs:321-330,每次新结果进来推移一次);⑤ sanitize 自愈(确定性,输入不变输出不变,不构成威胁)
BP3 摘要链尾(新增,可选) 摘要锚点链之后第一条消息 仅压缩事件失效;为 [system+摘要链] 小前缀保命中,配合§5.2 追加链语义

易变尾段(日期/清单/augmentation/知识)在 BP2 之后,不打断任何断点(既有设计正确);其每次 send 重建的变化被 BP2 挡住,只自伤尾段自身(本来也不可缓存)。

6.1 必修项:system 锚点的顶层折叠(打断 BP1 的根)

发现:convert_request 把存储态全部 role=System 消息抽离折叠进顶层 system(anthropic_compat.rs:128-133),消息流中 System => continue 剔除(:153)。因此每次压缩摘要/话题标记/催办事件:顶层 system 内容变(BP1 失配)+ 消息数组少一条(BP2 位置迁移)→ 双断点同时重置。

修法(view 层角色转换,存储不动):build_for_request 出口(sanitize 之后)把下标>0 的 role=System 消息映射为 role=User 发送。收益:

  • BP1 内容回到纯 system_prompt 稳定段,锚点事件不再打断顶层缓存;
  • 存储态仍是 System:has_compressible_messages 的 System 排除(mod.rs:656)、前端展示、压缩摘要定位语义全部不变——纯视图变更,与淘汰摘要先例(mod.rs:254-256 role 必须 user)同构收口;
  • openai_compat 侧同视图生效(system 留中原位,user 也合法,中性)。

细节:转换须在 sanitize step 之后(user 是最普通角色,不触发三元组/首条规则);首条 system_prompt(agentic/mod.rs:1983,组装层加入,不在 storage)不转换。


七、风险与回退

7.1 行为变更逐条

# 变更 风险 缓解
1 块状裁剪:单次多丢(裁到 T 水位而非刚好塞下) 裁剪时刻上下文损失变大(有界:≤0.2A) 换取两裁剪间全量命中;T 可调(拍板点1);病态回退保硬预算
2 分批压缩:C_high 0.6→0.75、批≈0.2A 后台压缩生效前留给裁剪的余量 0.4A→0.25A;压缩事件频度上升(批小了) is_compressing/版本哨兵/单元回退全复用;批失败下轮重试(滞后再武装);拍板点2 可保守维持 0.6
3 system 锚点 view 层转 user LLM 把摘要当作用户话语而非系统指令的语义差;多 provider 兼容面 纯 view-only 可瞬时回退;淘汰摘要已用同形态跑了数月无异常(mod.rs:259-261);拍板点3
4 BP3 新增 网关对 cache_control 数量/位置的兼容 默认关,随开关;预算 4 内

7.2 开关设计(照 df-ai-anthropic-cache 范式)

  • 新 settings KV df-ai-cache-friendly-context,parse_cache_flag 同语义(空/true/1=开;false/0/杂值=关),前端设置页一枚开关;
  • 总开关 gate 三机制:块状裁剪 + 分批压缩 + system 锚点 view 转换;BP3 挂在同一开关(anthropic 缓存总开关 df-ai-anthropic-cache 仍独立管 cache_control 形态,两层正交);
  • 开关关 = 三处全部旁路,行为与今日逐字节等价(与 cache_split_provider_type provider gate 叠加,非缓存受益 provider 不受 view 转换影响——不,view 转换与 provider 无关,应 gate 在新开关下全员生效或全员旁路,不由 provider 类型分叉);
  • 裁剪/转换是 view-only(逐请求),回滚瞬时;压缩是存储 status 单向(已 compressed 不回滚,与今天回滚语义一致,可接受并在开关文档注明)。

7.3 回退路径

开关关 → 全量回旧行为。已落库的 compressed 状态保留(软删可追溯,历史数据默认保留);水位常量均在常量层(T/C_high/C_low),极端情况改常量即再调参,无需动逻辑。


八、分期

快赢(下一小批可入,改动小、收益确定)

项 内容 落点 量级
W1 块状裁剪(T 水位+轮吸附+病态回退) context/mod.rs:211-219 替换 + 单测(水位/轮边界/病态三case) S
W2 system 锚点 view 层转 user build_for_request 出口 + 单测(首条不转/转换后 sanitize 不破) S
W3 命中率观测日志:cache_read/cache_creation 已在 usage(anthropic_compat.rs:728-730),每轮 info 输出命中率 stream 收尾处 XS
W4 BP3 摘要链尾断点(随 anthropic 开关) apply_message_cache_breakpoint 加一档 XS

W3 最先:先有数据,后面所有调参有据。协调注:W1/W2 落在 context/mod.rs(并行代理在改),实施前 rebase;若分层组装器 P2.2(组装器结构化)先行合入,块状裁剪改为在组装器出口实施,算法不变。

结构性(需更多验证)

项 内容 依赖
S1 分批压缩(C_high/C_low 双水位+批边界+兜底同批)+ PROTECT_COUNT 双源统一(context_helpers.rs:683 vs context_lifecycle.rs:53) W3 数据验证压缩事件频度;狗粮长会话
S2 水位再校准:T/C_high 按 W3 实测命中率-损失曲线微调 S1 上线后一轮狗粮
S3 namespace LRU 淘汰 notice 的前缀断治理(notice 文案确定化已天然成立,残余为位置断,收益小后置) 低优先

九、拍板点(4 项)

  1. 裁剪目标水位 T:推荐 0.8(块≈19K、稳定窗口 2–9 轮、命中 67–90%);备选 0.85(保守,少丢但裁剪频)——§4.2 表。
  2. 压缩双水位:推荐 C_high=0.75 / C_low=0.55(批≈0.2A,滞后 0.2A);备选维持现状 0.6 单水位(仅做分批不做提频调整)——§5.1。
  3. system 锚点 view 层转 user:推荐做(治 BP1 折叠根因);备选不做(则压缩/话题标记事件继续双断点全失配,BP1 收益打折)——§6.1。
  4. 总开关默认值:推荐默认开(对齐 df-ai-anthropic-cache 默认开先例,看板 939⑧;view-only 为主可瞬时回退);备选默认关观察一轮——§7.2。

附:证据坐标索引

论断 坐标
贪心最小裁剪逐轮爬行 crates/df-ai/src/context/mod.rs:211-219(触发 :198,214,切片 :239-246)
淘汰摘要尾追+缓存注释 mod.rs:250-261;确定性提炼 context_helpers.rs:284-288,332-364
一次压尽 agentic/context_lifecycle.rs:129-136(0.6 触发)、:308,442(compress_old_messages(protect_start))、mod.rs:670-699
is_active 白名单过滤 context/sanitize.rs:40-43;df-ai-core/provider.rs:91-99
system 全量折叠(打断 BP1) anthropic_compat.rs:128-133,153;锚点 system 入库 context_lifecycle.rs:313,449、agentic/mod.rs:1869,2510,2557
消息级断点(末条 user 前) anthropic_compat.rs:243-247,339-376;4 个预算 :349;usage 映射 :723-731
开关范式 commands/ai/prompt.rs:657-695;易变尾段拆分 :709-736、stream_relay.rs:791-795、每 send 构建一次 commands/chat.rs:417,996,2209,2476
tool_result 摘要已稳定(不威胁) 冻结 agentic/mod.rs:2004-2005、stall_detector.rs:352-363;记忆化 :368-379;新近度豁免滚动 :321-330
预算公式与保护区 context_helpers.rs:154-178,683,692;双源 PROTECT_COUNT context_lifecycle.rs:53
请求组装序 agentic/mod.rs:1143(sys_tokens)、:1957(build_for_request)、:1965-1982(namespace 展开)、:1983-1990(system+history+尾段)
上位目标与验收口径 docs/02-架构设计/专项设计/上下文分层组装器-落地方案-2026-10-10.md(病灶④/「裁剪以批量滞后发生」)