# 上下文管理五维走查(2026-10-10) > 走查方式:五路只读代理并行(组装/预算 · 压缩 · 自愈清洗 · 记忆召回 · 工具结果),全程源码取证;瓶颈清单摘要登记看板 `docs/todo.md` #939,本文为全文落档。 > ⚠️ 行号快照:`crates/df-ai/**` 与 `src-tauri` 部分文件当日有并行改动(工具结果外置 P1 WIP / Wave J 修复),行号为 2026-10-10 走查时点,批次开工以现场源码为准再核对。 > 关联设计:`docs/02-架构设计/上下文根治设计-20261006.md`(223:1 根治总纲)· `docs/02-架构设计/专项设计/上下文分层组装器-落地方案-2026-10-10.md`(五病灶锚点实测 + 接口化落地方案)。 > 状态标注:【已修-工作区】= 修复已落工作区待提交(Wave J/守夜期产出);【待解禁】= 修复点在 `crates/df-ai`,需并行写者完工后接手;★ = 可先在 src-tauri 侧落。 --- ## 零、背景与铁证 - **223:1 出处**:84cb104c 会话 2.2 天烧 4.3 亿 tokens,prompt:completion = **223:1**,日均 1.9 亿(`上下文根治设计-20261006.md` §量化预期)。 - 走查定位出该结构性浪费的共同根因:**记账(history_tokens)与真实发送体量脱钩 → 三重门槛(预算门/裁剪门/压缩门)恒不触发 → 窗口永不收敛 → 每轮全量重发**。 --- ## 一、组装/预算维度 ### HIGH① history_tokens 与真实请求体量脱钩(根因,最重)【待解禁】 机制链四步,环环实证: 1. **引用化发生在 push 前**:工具结果在审计层先经 `should_use_namespace` 判定(`audit/mod.rs:1184-1191`;`namespace_store.rs:47-53`),`ALWAYS_LARGE_TOOLS = read_file/list_directory/grep/diff_files`(`namespace_store.rs:38`)**命中即引用、与实际体积无关** → `store()`(`namespace_store.rs:124-131`)入内存池,消息里只留短串 `namespace://tool/{hash}`。 2. **记账只数短串**:`ContextManager::push`(`context/mod.rs:101-143`)对这条短串跑 `estimate_message`(`context_helpers.rs:62-105`),`history_tokens += tokens` 在 `:108-110` → **单条上万 token 的大结果只记 ~15 token(~30 字符)**。 3. **展开发生在发送时**:`agentic/mod.rs:1927-1944` 才把引用展开为真实体量(单条 max 150KB,见 P1 注释 `context_helpers.rs:427-432`)。 4. **三重门槛双双恒不触发**: - 预算门 `context/mod.rs:198`:`history_tokens <= available` 恒真 → 直接返回全量; - 裁剪门 `context/mod.rs:239-246`:永不达阈值; - 压缩门 `context_lifecycle.rs:133-135`:`available*6/10 < history_tokens` 恒假。 → 每轮实际重发展开后的全量历史 → 223:1。 - **修**(二选一或组合):push/外置时按**原文体量**入账(记账基点 = 发送视图尺寸单一真值,见组装器方案 §3.1,兼解 HIGH② 的自愈重算);或估算器识别 namespace ref 取池内真实大小。修复点在 `crates/df-ai`(估算器)+ `src-tauri`(入账点),df-ai 侧待解禁。 - **本质澄清**:namespace_store 是**内存态引用化**,只省进程内存,发 LLM 前展开全文 → **token 零收益**(与 P1 落盘外置的对比见维度五)。 ### HIGH② 每轮 O(n) 全量克隆 + sanitize + 展开 + 摘要 → 会话累计 O(n²)【待解禁】 - 未超预算分支 `context/mod.rs:198-205` 走 `all_messages_clone()`(`:294-296`,逐条深克隆)+ sanitize + 出口断言; - 展开 `agentic/mod.rs:1927-1944` 对**全部**历史逐条 `read_only` + String 克隆; - 摘要 `summarize_tool_results`(`stall_detector.rs:313-385`)再遍历全部消息做内容变换。 - 三者每轮各 O(n)(调度点 `agentic/mod.rs:1904-1921/1945-1957/1981-1984`),会话累计 **O(n²) = 全量重发本体**。根因与①同源:门失效 → 窗口永不收敛 → n 随会话单调涨。**治本 = 组装器方案 P2 分层组装**(接口化,见关联设计)。 ### ★HIGH③ 预算上限恒 128K,不随模型真实窗口【已修-工作区(主会话);子代理残留】 - 原状:主会话构造点 `commands/ai/mod.rs`(走查时点 :1239)`ContextManager::new(ContextConfig::default())`;`context_helpers.rs:128-136` `max_tokens=128_000`,`budget_limit` 由它算(`:138-143`);子代理同病 `sub_agent.rs:117-127`。真实窗口在 `ModelConfig.context_window`(`df-ai-core/src/model.rs:138-139`)却只被 router 使用,代码自陈该维度「恒过、失效」(`agentic/mod.rs:2012` 注释)。 - 影响:1M 窗口模型被当 128K 裁(**能力降级**);小窗口模型(如 deepseek-chat 64K)预算高于实际窗口(**超发风险**)。上界取 1M 而非 128K 的理由:`max_tokens` 只是**裁剪预算**——历史不足则不裁、不会撑大实际发送,卡 128K 等于不治根因。 - 修复(Wave J,工作区):`commands/ai/model_context.rs` 新增 `context_config_for_model` 三级优先(预设表/用户手填可信窗口/未知回退 128K)+ 仅按窗口下调或持平、绝不放大 + 安全顶挡脏预设值;主会话收口单一入口 `apply_model_window`(`commands/ai/mod.rs:1274-1284`,构造仍 default、发送入口按 resolved_model 重设)+ 7 单测。 - **残留缺口**:子代理未接——`sub_agent.rs:119`(`SUB_AGENT_CONTEXT_MAX_TOKENS=128_000`)、`:123-126`、`:761` 仍固定 128K,待改走 `context_config_for_model` 统一口径。 ### MED⑤ 估算器 chars_ratio=0.35 系统性偏差【待解禁】 `context_helpers.rs:29-44/73-74`:中文低估 2-3 倍、base64 高估(注释自陈 OpenAI 按像素计而非字节,0.35 偏保守)。多模态回归修正已并入(parts 文本/base64 计入 + url 图 1600 token 地板折算在 ×ratio 之后),但系数本身未分语种/分载荷。方向:分系数校准,需独立评估裁剪边界。 ### MED⑥ 保护区固定 6 条 + 淘汰补偿仅 300 字【待解禁】 `context/mod.rs:648/:655`:保护区条数硬编码;被淘汰内容的补偿摘要极短,早期主题对 LLM 快速失忆。与⑦(无召回)叠加后**窗口外内容实质永久丢失**。 ### LOW⑬ 估值双轨从不互相校准 `estimated_prompt`(路由用,较准)与 `history_tokens`(预算用,失真)两轨并存,无任何校准/对账点——①的失真因此长期无告警。修①时可顺带加「双轨偏差超阈即 warn」的可观测。 ### LOW⑭ sys_tokens 未计工具 schema system 侧估算不含工具 schema 体积,预算口径偏乐观。 ### 前缀缓存两处失效源(组装侧) - **⑨ 滑动裁剪位移前缀【待解禁】**:超预算时 `context/mod.rs:239-246` 切 `messages[trim_end..]`——头部被切,整个请求前缀位移,provider 自动前缀缓存(DeepSeek/Anthropic)整段失效。已有部分缓解:淘汰摘要改尾部追加(`:259-261`)、`push_system_anchor` 避让悬空头(`:582-595`)、`tool_view_decisions` 冻结压缩决策防跨轮翻转(`stall_detector.rs:352-363`);但**基础裁剪本身仍位移前缀**,且淘汰摘要内容逐轮漂移(`:259` 每轮即时重生成,被淘汰区单调增长)。 - **⑧ system 可变段内嵌稳定段【已修-工作区】**:可变段(日期 + 项目/任务清单)混在稳定前缀内致前缀缓存失效;清单描述不截断(走查时点 `prompt.rs:565/:584`)加剧体量。修复(Wave J):`df-ai-anthropic-cache` 拆分开关**默认开**(`prompt.rs:653-687`,键名保留历史名、语义扩展覆盖 openai_compat 自动前缀缓存——易变段挪到消息流末尾,开关关时非官方网关零风险)+ 清单描述截断 `CONTEXT_DESC_MAX_CHARS=120`(`prompt.rs:562/:604/:629`)。 --- ## 二、压缩维度 ### HIGH④ 压缩单向不可逆,被压原文对 LLM 永久不可见【待解禁】 - `compress_old_messages`(`context/mod.rs:664-699`)标记 Compressed(幂等:已压跳过、token 只扣首次)→ 发送视图 sanitize step0 按 `is_active` 过滤出局(`context/mod.rs:107-108` 统一剔除)。 - 后果:压缩后 LLM 对早期细节**不可回读**;窗口外内容(见⑥⑦叠加)实质永久丢失。设计 C-r1「压缩不丢/可召回」未落地。 - 方向:复用 P1 句柄模式——压缩写 pointer 可回读(压缩摘要 + 「原文句柄路径」,agent 需要时 read_file 找得回),与工具结果外置同构。 ### MED⑩ 一次压尽,无分批【待解禁】 触发压缩时一次压完可压区,无 windows 式分批(分批可平摊摘要质量波动与 token 峰值)。 ### HIGH⑪ 后台压缩无 epoch/内容版本守卫【已修-工作区】 - 原状(`context_lifecycle.rs:206-323` 走查时点):后台 task 无任何守卫——`clear()` 保留 per_conv 条目且复位 is_compressing → **压缩窗口内旧摘要可注入已清空的新上下文**(跨代污染)。loop 侧有 F1 epoch,task 侧没有。 - 修复(Wave J,工作区):**内容版本哨兵 = 首条消息全局唯一 id**(`context_lifecycle.rs:63-68`)——否掉 len 方案:压缩期间 loop 正常 append 会使 len 增长,以 len 相等为守卫永不生效;重建后首条必换新 id,是「上下文被整体替换」的准确且零成本哨兵。快照触发时首条 id,后台 task 提交前比对(`:74/:153`),不匹配即丢弃本次摘要。 ### HIGH⑫ 后台压缩无 panic 兜底【已修-工作区】 - 原状:后台 task panic → is_compressing 永真 → **该会话压缩永久停摆**。 - 修复(Wave J,工作区):整个后台 body 包 `catch_unwind`(复用 `mod.rs::unwind_to_result` 薄包装,`context_lifecycle.rs:236-244`、复位句柄 `:79-82`),panic 以 Err 返回 → 复位 is_compressing + warn。依赖 panic=unwind(tokio 默认);panic=abort 构建下退化为进程终止(尽力兜底,已在注释声明)。 --- ## 三、自愈清洗维度(sanitize) 本维整体结论**良好**,一处缺陷已修: - **已核良好**:裁剪三元组原子性正确(`context/mod.rs:612-642`);909① 两处修复现状有效——`push_system_anchor` 锚点避让悬空头(`:582-595`)、压缩重入单锁复位,未见旧窗口复现。 - **出口防御在位**:畸形配对自愈后 `assert_placeholder_pairing` 出口断言(`context/mod.rs:198-205` 路径内),失败降级 `TOOL_MISSING_PREFIX` 自愈,防 400 orphan 复发。 - **已修【已修-工作区】多夹层归位有损**(对抗审查③):原 `reorder_interlopers_after_tool_run` 只把头与「首个连续 tool 段」间的夹层后移 → `[head(c1,c2), X, tool(c1), Y, tool(c2)]` 时 c2 仍被判未闭合剥除(非回归,但「无损」宣称不成立)。守夜期重写为**聚拢该头全部响应(保序)到夹层之前**,边界语义不变(遇下一个头即停/响应集合耗尽即停),更名 `gather_tool_responses`(`sanitize.rs:223`,调用点 `:50`,描述做的事、短一半);新增多夹层 + 幂等单测。「无损归位」至此对多夹层成立。复验:`cargo test -p df-ai --lib context` 157 passed / 0 failed。 --- ## 四、记忆召回维度 - **⑦ 窗口外历史零召回【待解禁(属 P2 分层组装)】**:组装是隐式的——system 由 `build_runtime_prompt`(`agentic/mod.rs:3042`)拼、历史由 `build_for_request` 拼、易变尾由 `merge_volatile_tail` 拼,无统一组装器对象、无可观测统计;检索**仅覆盖知识库**:`hybrid_search`(`knowledge_inject.rs:421`)→ `build_knowledge_context`(`:508`)→ `build_knowledge_context_for_conv`(`:587`),**窗口外对话历史与压缩摘要无处可召回**。与⑥(补偿仅 300 字)叠加 = 「裁掉即失忆」。 - **方向(组装器方案 §3.3 已定)**:检索召回复用 `knowledge_inject` 单入口——把窗口外历史/压缩摘要作为第三类召回源挂进既有管线,不另造第二套检索;L3 记忆卡(`上下文三层记忆-设计-2026-10-04.md` C-m1)是召回的内容来源,组装器只提供入口。 - **跨会话召回底座查证(Wave L)**:文件租约与跨会话召回 **HEAD 已实现**(`src-tauri/src/commands/ai/lease.rs`,cb260ab;召回链 cb260ab+9e81182)——未另造双轨,只补纯函数与单测(lease 11 / knowledge_inject 29 通过),W-h 已销账。新需求只接既有底座,勿重建。 --- ## 五、工具结果维度 - **namespace 引用化机制(HIGH① 的机制载体)**:判定 `should_use_namespace`(`namespace_store.rs:47-53`)两条路——`ALWAYS_LARGE_TOOLS` 四工具命中即引用(与体积无关)或超 `NAMESPACE_BYTE_THRESHOLD`/`NAMESPACE_LINE_THRESHOLD`;入池 `store()`(`:124-131`),pin 占比上限 0.5(`:44`),EvictionLog 容量 50 条(`:41`)。**定位澄清**:它治的是进程内存,不治 token——发送时展开回全文。 - **P1 工具结果外置(真正治 token)【并行写者 WIP,未提交】**:`crates/df-ai/src/tool_offload.rs`(新,~117 行)+ `context_helpers.rs`(`READ_FILE_OFFLOAD_BYTES=8KB` / `extract_key_info` 句柄追加)+ `lib.rs`(pub mod)+ `tests/**`——超阈值结果**正文落盘**、上下文只留「摘要 + 句柄路径」,agent 可 read_file 展开回读:被省略的中段不进 prompt 但找得回 → 真正减 token 且不丢信息。与 namespace_store 互补不冲突;降级契约(base 未注入返回 None)+ 幂等(内容稳定哈希只落盘一次)+ 只增不减保留策略齐备。该批静置归属待用户拍板(含 `src-tauri/src/lib.rs`、`stall_detector.rs` 挂点)。 - **HIGH① 的入账修复与外置的衔接**:外置落地后,push 侧按**落盘前原文体量**入账即可同源治掉①(记账基点 = 发送视图尺寸);两者是同一根因(记账失真)的内容侧与账本侧修复,须协同验收。 --- ## 六、修复状态总表(截至 2026-10-10 收盘) | # | 瓶颈 | 级别 | 状态 | 落点 | |---|------|------|------|------| | ① | history_tokens 与真实体量脱钩 | HIGH | 待解禁(依赖 df-ai) | 入账点 src-tauri + 估算器 df-ai | | ② | 每轮 O(n) 全量重发 → O(n²) | HIGH | 待解禁(P2 分层组装) | 组装器方案 | | ③ | 预算恒 128K | ★HIGH | **主会话已修-工作区**;子代理残留 | `model_context.rs` + `mod.rs:1274`;子代理 `sub_agent.rs:761` 待接 | | ④ | 压缩不可逆无重载 | HIGH | 待解禁(C-r1 句柄化) | df-ai | | ⑤ | chars_ratio 偏差 | MED | 待解禁 | df-ai | | ⑥ | 保护区 6 条 + 补偿 300 字 | MED | 待解禁 | df-ai | | ⑦ | 窗口外零召回 | MED | 待解禁(复用 knowledge_inject 入口) | src-tauri | | ⑧ | system 可变段毁前缀缓存 | ★ | **已修-工作区**(开关默认开 + 描述截断 120) | `prompt.rs` | | ⑨ | 滑动裁剪位移前缀 | MED | 待解禁 | df-ai | | ⑩ | 一次压尽无分批 | MED | 待解禁 | df-ai | | ⑪ | 后台压缩无版本守卫 | HIGH | **已修-工作区**(首条消息 id 哨兵) | `context_lifecycle.rs:63-74` | | ⑫ | 后台压缩无 panic 兜底 | HIGH | **已修-工作区**(catch_unwind 复位) | `context_lifecycle.rs:79-82,236-244` | | ⑬ | 估值双轨无校准 | LOW | 随①顺带 | — | | ⑭ | sys_tokens 无工具 schema | LOW | 待解禁 | df-ai | | — | 多夹层归位有损(自愈维) | 缺陷 | **已修-工作区**(`gather_tool_responses`) | `sanitize.rs:223` | > 收敛路线:★ 项与守卫类已落;①④⑨ 等 df-ai 侧修复待并行写者(工具结果外置 P1)完工解禁后接手,届时以「记账基点 = 发送视图尺寸单一真值」一条主线串起 ①→②自愈,量化目标 223:1 → ≤50:1(根治设计 §四)。