From 3a1b8ccda9bb9af63b476795f8e9daacd6f5cf68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Sun, 11 Oct 2026 10:52:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86:=20=E6=92=A4=E9=94=80=20name?= =?UTF-8?q?space=20=E5=BC=95=E7=94=A8=E4=BD=93=E9=87=8F=E5=85=A5=E8=B4=A6?= =?UTF-8?q?=E4=BC=B0=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 方案重审: ref 尾段携带长度牵动协议格式/解析/测试多处, 收益与复杂度不成比例; 退回估算器原口径, 另寻不改协议的入账路径 --- crates/df-ai/src/context_helpers.rs | 9 -------- crates/df-ai/src/namespace_store.rs | 35 ++--------------------------- docs/todo.md | 2 +- 3 files changed, 3 insertions(+), 43 deletions(-) diff --git a/crates/df-ai/src/context_helpers.rs b/crates/df-ai/src/context_helpers.rs index 278066f..177b611 100644 --- a/crates/df-ai/src/context_helpers.rs +++ b/crates/df-ai/src/context_helpers.rs @@ -61,15 +61,6 @@ impl TokenEstimator { /// 这里把 parts 的文本/base64 按同一 chars_ratio 粗估累加(base64 视为密集字符,0.35 偏保守)。 pub fn estimate_message(&self, msg: &ChatMessage) -> u32 { let mut char_count = msg.content.chars().count(); - // 939① 估算入账修正:Tool 消息 content 为 namespace 引用时,按 ref 尾段携带的 - // 原文字节数入账(字节保守视作字符,中文会 3x 高估——方向安全:宁可高估触发 - // 裁剪/压缩,不可漏估致预算门失效,后者曾致 provider 超限 400)。旧格式/已淘汰 - // 内容(无 ?len= 尾段)按字面字符保守计,行为同前。 - if matches!(msg.role, crate::provider::MessageRole::Tool) { - if let Some(n) = crate::namespace_store::parse_ref_content_len(&msg.content) { - char_count = n; - } - } // url 图地板单独按 token 累加:地板语义是"每图至少 ~1600 token",必须生效于 // ×chars_ratio 折扣之后——若折进 char_count 会被 0.35 稀释成 560,1600 保底失效, // 含 url 图消息的 history_tokens 低估 → 预算裁剪漏触发 → provider 超限 400。 diff --git a/crates/df-ai/src/namespace_store.rs b/crates/df-ai/src/namespace_store.rs index f9a8674..f0938b5 100644 --- a/crates/df-ai/src/namespace_store.rs +++ b/crates/df-ai/src/namespace_store.rs @@ -60,20 +60,9 @@ pub fn is_namespace_ref(content: &str) -> bool { /// 从引用路径中提取内部 key pub fn parse_namespace_key(full_path: &str) -> Option<&str> { full_path.strip_prefix(NAMESPACE_REF_PREFIX) - // 939①: 先剥 ?len= 长度尾段(估算入账用),否则尾段并入 key 致全线查空 - .and_then(|s| s.split("?len=").next()) .and_then(|s| s.split('/').nth(1)) } -/// 从引用路径提取原文字节数(939①)。带 `?len=N` 尾段的引用返回 Some(N); -/// 旧格式/放弃入库的无尾段引用返回 None(调用方按字面字符保守估算)。 -pub fn parse_ref_content_len(full_path: &str) -> Option { - full_path.rsplit("?len=") - .next() - .and_then(|s| s.parse::().ok()) - .filter(|_| full_path.contains("?len=")) -} - /// namespace 条目 #[derive(Debug, Clone)] pub struct NamespaceEntry { @@ -149,12 +138,6 @@ impl NamespaceStore { fn store_inner(&mut self, tool_name: &str, content: &str, owner: Option<&str>, pinned: bool) -> String { let key = simple_hash(content); - // 939① 估算入账:ref 尾段携带原文字节数,TokenEstimator 解析即按真实体量入账 - // (旧口径 ref 仅 ~30 字符被记 ~15 token,预算/裁剪/压缩三重门对其失效)。 - // parse_namespace_key 取 nth(1) 不受尾段影响,展开/淘汰提示零改动。 - // 放弃入库(超容量/淘汰后仍放不下)返回**不带长度**的 path:原文不在池,展开点 - // 落占位提示,预算按轻量字符计——已淘汰内容不应继续虚占预算。 - let path_with_len = format!("{}{}/{}?len={}", NAMESPACE_REF_PREFIX, tool_name, key, content.len()); let path = format!("{}{}/{}", NAMESPACE_REF_PREFIX, tool_name, key); // 已存在 → 刷新 access_count,不重复占用空间;若本次要求 pin,升级既有条目 @@ -165,7 +148,7 @@ impl NamespaceStore { entry.pinned = true; } } - return path_with_len; + return path; } let new_bytes = content.len(); @@ -207,7 +190,7 @@ impl NamespaceStore { self.current_bytes += new_bytes; self.entries.insert(key, entry); - path_with_len + path } /// pin/取消 pin(事后)。pin 总量超 50% 容量时本调用拒绝并返回 false(防失控,拍板 C1)。 @@ -549,18 +532,4 @@ mod tests { assert!(ns.contains(&p_old) == false || ns.pinned_bytes() <= 50, "降级机制应使 pin 总量可被压缩"); } - - #[test] - fn ref_carries_content_len_for_budget() { - // 939①: 成功入库的 ref 带 ?len= 原文字节数,parse 往返一致; - // parse_namespace_key 不受尾段影响(展开点零改动) - let mut ns = NamespaceStore::new(100_000); - let content = "x".repeat(5000); - let path = ns.store("read_file", &content); - assert!(path.contains(&format!("?len={}", content.len())), "ref 应带 len: {path}"); - assert_eq!(parse_ref_content_len(&path), Some(content.len())); - assert_eq!(parse_namespace_key(&path), parse_namespace_key(&format!("namespace://read_file/{}", crate::namespace_store::simple_hash(&content)))); - // 旧格式/无尾段 → None(调用方保守按字面计) - assert_eq!(parse_ref_content_len("namespace://read_file/abc"), None); - } } diff --git a/docs/todo.md b/docs/todo.md index 65961e1..b4be5b1 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -701,7 +701,7 @@ 933. [ ] **输入框支持有序列表渲染(`1. ` 类飞书,2026-10-10 用户报)**:ChatInput 内输入 `1. `(或 `- `)后应有**列表预览/自动编号**效果(对齐飞书消息输入框),而非纯文本。**需设计**:触发判据(行首 `\d+. ` / `- ` / `* `)、渲染形态(实时预览块 or 仅高亮)、回车续写自动补序号、退格退出列表、与既有 `/` 技能联想、`@` 提及、代码围栏的优先级冲突处理。设计落 docs 后再实施。 935. [ ] **定时消息(多条 + 定时列表,2026-10-10 用户报)**:支持把消息设定为定时发送,可设多条,并有统一「定时列表」管理(查看/改期/取消)。**需设计**:存储(表 or KV)、调度器(前端定时器 or 后端 tokio 任务,需在应用关闭后仍生效→倾向后端)、与队列/生成中状态的关系、失败重试与通知、UI 入口(CHATIN 旁「定时」按钮 + 列表抽屉)。设计落 docs 后再实施。 939. [ ] **上下文管理瓶颈清单(2026-10-10 五维只读走查,源码取证)** — 按收益排序,⚠️ `crates/df-ai/**` 被并行写者占用(工具结果外置 WIP)→ 带 ★ 的可先在 src-tauri 侧落,其余待解禁: - - **HIGH ① 🟡 namespace 半边已修(2026-10-11)**:ref 尾段携带原文字节数(?len=),估算器识别后按真实体量入账(旧 ~15 token→真实万级);P1 句柄/落盘侧(tool_offload)与 D2a 审计侧句柄行入账待接。原描述::工具结果经 namespace 引用化入内存(`audit/mod.rs:1183-1190` + `namespace_store.rs:40-55` ALWAYS_LARGE_TOOLS 命中即引用,与体积无关),估算器只数字符(`context_helpers.rs:62-105`)→ 单条上万 token 的大结果只记 ~15 token;**预算/裁剪门(`context/mod.rs:185-205`)/压缩门(`context_lifecycle.rs:103-108`)三重门槛形同虚设**,每轮实际重发展开后的全量历史(223:1 结构浪费根因)。修:push/外置时按原文体量入账,或估算器识别 ref 取真实大小。 + - **HIGH ①** `history_tokens` 与真实请求体量**脱钩**:工具结果经 namespace 引用化入内存(`audit/mod.rs:1183-1190` + `namespace_store.rs:40-55` ALWAYS_LARGE_TOOLS 命中即引用,与体积无关),估算器只数字符(`context_helpers.rs:62-105`)→ 单条上万 token 的大结果只记 ~15 token;**预算/裁剪门(`context/mod.rs:185-205`)/压缩门(`context_lifecycle.rs:103-108`)三重门槛形同虚设**,每轮实际重发展开后的全量历史(223:1 结构浪费根因)。修:push/外置时按原文体量入账,或估算器识别 ref 取真实大小。 - **HIGH ②** 每轮 O(n) 全量深克隆 + sanitize + namespace 展开 + 摘要 + 估算(`context/mod.rs:198-205` / `agentic/mod.rs:1904-1921`,`1945-1957`,`1981-1984`)→ 会话内 O(n²) 累积(全量重发本体);治本=设计档 P2 分层组装。 - **★HIGH ③ ✅ 已修(两批)**:主会话=model_context.rs 按 resolved_model 窗口构造(b37279f,未知窗口保守回退 128K 零回归);子代理=sub_agent_context_config 继承父会话预算(10-11,同模型同窗口单一事实来源,恒 128K 缺口收口)。 - **HIGH ④(压缩)** 压缩**单向不可逆**:被压原文对 LLM 永久不可见(`context/mod.rs:664-699` 标 Compressed + `sanitize.rs:42` is_active 过滤出局),设计 C-r1「压缩不丢/可召回」未落地 → 方向:复用 P1 句柄模式,压缩写 pointer 可回读。