- 会话终态提炼: 删除前+停靠双挂点,快照与清水位同临界区 - 目标快照: 恢复引导按最终有效意图生成,旧目标不复活 - 知识检索项目维度软排序,WorkingContext 持久化迁移 - 脉络卡: published 知识按项目聚合纯读侧,带入上下文经输入框显式注入 - 压缩与缓存能力在主循环/子代理/提示词三面接线 - 两份专项设计文档与看板销账
20 KiB
压缩裁剪缓存友好化 — 设计
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 贪心循环,改「目标水位 + 轮边界吸附」:
- 触发条件不变:
history_tokens > available(硬安全门,mod.rs:214同式)。 - 目标水位
T × available(T 默认 0.8):累计淘汰单元 token,直到history_tokens − removed ≤ T×available且当前边界吸附到轮边界。 - 轮边界锚点:单元边界
unit.end处,若下一时刻消息(未淘汰首条)role=User,则该 end 是轮起点——整问整答同生同死,淘汰摘要按轮覆盖语义完整。 - 若扫到保护区(
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 缓存语义:为什么分批仍然值得
压缩从视图头部移除消息,新前缀=[摘要锚点]+[剩余],任何一次压缩都是历史段全量重建,分批不减小单次破坏面。分批的收益在别处:
- 单次信息损失有界(0.2A≈19K),摘要质量随输入变小而升,用户「丢历史」体感摊薄;
- 摘要锚点改为追加链(见§六 BP3):第 k+1 次压缩把新摘要接在摘要链尾,链之前的 [system+链头] 前缀仍命中——一次压尽则每次都从零重建;
- 两次压缩间隔拉长(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-256role 必须 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_typeprovider 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 项)
- 裁剪目标水位 T:推荐 0.8(块≈19K、稳定窗口 2–9 轮、命中 67–90%);备选 0.85(保守,少丢但裁剪频)——§4.2 表。
- 压缩双水位:推荐 C_high=0.75 / C_low=0.55(批≈0.2A,滞后 0.2A);备选维持现状 0.6 单水位(仅做分批不做提频调整)——§5.1。
- system 锚点 view 层转 user:推荐做(治 BP1 折叠根因);备选不做(则压缩/话题标记事件继续双断点全失配,BP1 收益打折)——§6.1。
- 总开关默认值:推荐默认开(对齐
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(病灶④/「裁剪以批量滞后发生」) |