16 KiB
上下文管理五维走查(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 与真实请求体量脱钩(根因,最重)【待解禁】
机制链四步,环环实证:
-
引用化发生在 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}。 -
记账只数短串:
ContextManager::push(context/mod.rs:101-143)对这条短串跑estimate_message(context_helpers.rs:62-105),history_tokens += tokens在:108-110→ 单条上万 token 的大结果只记 ~15 token(~30 字符)。 -
展开发生在发送时:
agentic/mod.rs:1927-1944才把引用展开为真实体量(单条 max 150KB,见 P1 注释context_helpers.rs:427-432)。 -
三重门槛双双恒不触发:
- 预算门
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-136max_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 context157 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.mdC-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(根治设计 §四)。