Files
DevFlow/docs/05-代码审查/上下文管理五维走查-2026-10-10.md
lxy 78dd079344 文档: 签名发布SOP与两份走查报告落档
- 签名与发布 SOP: 签名→打包→发布清单→自动升级全链可重复执行
- 上下文管理五维走查全文(瓶颈清单已登记 939)
- 版本升级链走查报告
2026-10-11 07:38:02 +08:00

16 KiB
Raw Permalink Blame History

上下文管理五维走查(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(根治设计 §四)。