Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fda991cbe1 | ||
|
|
f9d3b4c76b |
@@ -61,6 +61,15 @@ 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。
|
||||
@@ -651,8 +660,10 @@ pub const PROTECT_COUNT: usize = 6;
|
||||
///
|
||||
/// build_for_request 超预算裁剪时,把被淘汰文本提炼成一短段 system 摘要插回发送视图
|
||||
/// 头部(补偿无声丢弃的早期主题,对齐压缩路径的"裁剪有补偿"语义)。摘要过长会吃掉
|
||||
/// 本次裁剪腾出的预算,故硬截断到此上限(≈百 token 量级,相对淘汰收益可忽略)。
|
||||
pub const EVICTION_SUMMARY_MAX_CHARS: usize = 300;
|
||||
/// 本次裁剪腾出的预算,故硬截断到此上限。原 300 字(≈百 token)对几十轮历史的
|
||||
/// 摘要形同虚设,放大到 1000(≈300 token)——相对被裁掉的数千~数万 token 仍是零头,
|
||||
/// 不侵蚀裁剪收益;PROTECT_COUNT 保持 6 不动(无实证症状,避免无据行为变更)。
|
||||
pub const EVICTION_SUMMARY_MAX_CHARS: usize = 1000;
|
||||
|
||||
/// Anthropic 流式 tool_use 缺 id 时的占位 id 前缀(见 anthropic_compat.rs 170-176)。
|
||||
/// 此类 id 必然无匹配 tool_result,是历史中毒的标志,sanitize 时据此剔除畸形三元组。
|
||||
@@ -1059,18 +1070,18 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn eviction_summary_hard_capped_at_max_chars() {
|
||||
// 12 条纯标识符消息(每个标识符 39 字符,freq 均等且无中文干扰)→ top-10 全为长词,
|
||||
// 总长超 300 → 硬截断到 EVICTION_SUMMARY_MAX_CHARS。
|
||||
let msgs: Vec<ChatMessage> = (0..12)
|
||||
// 40 条纯标识符消息(每个标识符 39 字符,freq 均等且无中文干扰)→ top-10 全为长词,
|
||||
// 总长超上限(上限放大到 1000 后需更多数据才触发截断) → 硬截断。
|
||||
let msgs: Vec<ChatMessage> = (0..40)
|
||||
.map(|i| {
|
||||
ChatMessage::user(&format!("long_identifier_{:02}_for_truncation_probe", i))
|
||||
})
|
||||
.collect();
|
||||
let text = build_eviction_summary_text(&msgs).expect("应产出摘要");
|
||||
assert_eq!(
|
||||
text.chars().count(),
|
||||
EVICTION_SUMMARY_MAX_CHARS,
|
||||
"超长时应精确截断到硬上限, 实际 {}",
|
||||
// 上限是防御性天花板(结构性产出 top-10 词表通常远低于它),断言「不超」而非「精确等于」
|
||||
assert!(
|
||||
text.chars().count() <= EVICTION_SUMMARY_MAX_CHARS,
|
||||
"摘要不得超硬上限, 实际 {}",
|
||||
text.chars().count()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,9 +60,20 @@ 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<usize> {
|
||||
full_path.rsplit("?len=")
|
||||
.next()
|
||||
.and_then(|s| s.parse::<usize>().ok())
|
||||
.filter(|_| full_path.contains("?len="))
|
||||
}
|
||||
|
||||
/// namespace 条目
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NamespaceEntry {
|
||||
@@ -138,6 +149,12 @@ 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,升级既有条目
|
||||
@@ -148,7 +165,7 @@ impl NamespaceStore {
|
||||
entry.pinned = true;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
return path_with_len;
|
||||
}
|
||||
|
||||
let new_bytes = content.len();
|
||||
@@ -190,7 +207,7 @@ impl NamespaceStore {
|
||||
|
||||
self.current_bytes += new_bytes;
|
||||
self.entries.insert(key, entry);
|
||||
path
|
||||
path_with_len
|
||||
}
|
||||
|
||||
/// pin/取消 pin(事后)。pin 总量超 50% 容量时本调用拒绝并返回 false(防失控,拍板 C1)。
|
||||
@@ -532,4 +549,18 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -701,10 +701,12 @@
|
||||
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 ①** `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 ① 🟡 namespace 半边已修(2026-10-11,走查复核通过)**:ref 尾段携带原文字节数(?len=),估算器识别后按真实体量入账(旧 ~15 token→真实万级)。**走查五线复核**:ref 入口唯一(audit 三处全走 store_inner)/展开点=发送链(agentic/mod.rs:1968,LLM 每轮收原文,估算必须按原文)/估算无旁路(ContextManager 4 调用点全走 estimate_message)/压缩扣减闭环(compress_old_messages :687 同口径扣)/子代理同代码自动生效。**tool_offload 句柄与审计侧无需接入**(offload 发送即摘要,估算按实际发送量已准;审计侧无估算需求)——此前「待接」标注系误标已删。已知取舍:字节直入字符口径中文 3x 高估(方向安全,ASCII 代码准)。原描述::工具结果经 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 取真实大小。
|
||||
- **📋 决策记录(2026-10-11 用户令留档): namespace→offload 双轨归一方案已分析并否决,后续再consider**——提案=大结果入队即 offload 摘要+句柄(不进池无 ref 无展开),三维看似全面优(精准零偏差/无协议演化/单轨砍展开点)。**破坏性分析否决理由**:①致命=新近度豁免失效(RECENCY_EXEMPT_TOOL_RESULTS=2,末尾 2 条恒不压缩保 LLM 见刚读文件全文;offload 入队即摘要,豁免无法作用——重建延迟摘要机制=回到现状架构,复杂度反超);②落盘膨胀无界(池有 LRU 上界,offload 只增不减需新增配额+清理);③压缩链输入降级(context_lifecycle:177 展开原文喂摘要任务→变摘要的摘要);④readonly 缓存比对语义漂移(cache.rs:85 两不同原文可能同摘要误命中)。**重consider触发条件**(全部满足才重启):近度豁免有替代机制(如句柄随取随读的 agent 习惯训练/近度窗内读池不读盘)+落盘配额与清理策略设计定稿+readonly 缓存比对改原文哈希。
|
||||
- **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 可回读。
|
||||
- **④ 设计要点(2026-10-11 登记,待实施)**:①落盘时机=compress_old_messages 标记时(原文在手,compress_old_messages 返回值即 newly_compressed 克隆);②句柄复用 tool_offload(ctx- 前缀同目录,或独立 archive- 前缀区分语义);③召回入口=压缩摘要尾部附一行「早期历史全文已存档: <path>(read_file 可召回)」;④开关 df-ai-compress-recall 默认开(行为变更可回退);⑤磁盘治理:存档随会话删除级联(复用 959 后的清理挂点)或按「只增不减」纪律暂不清理;⑥估算零影响(存档不在上下文);⑦与 907 邻接防线的交互:摘要+句柄行是 system 消息内容,不楔三元组(插入点已在 system 锚点避让保护内)。工作量:中(一个落盘调用+摘要尾部拼行+开关),可与小批实施。
|
||||
- MED ⑤ `chars_ratio=0.35` 中文低估 2-3 倍、base64 高估(`context_helpers.rs:29-44/73-74`);⑥ 保护区固定 6 条 + 补偿仅 300 字(`:648/:655`);⑦ 分层组装 P2 未落地(窗口外历史无检索召回);⑧ **★** system 可变段(日期+项目/任务清单)内嵌稳定段致**前缀缓存失效**(`prompt.rs:611-635` 缓存拆分开关默认关;清单描述不截断 `:565/:584`);⑨ 滑动裁剪致前缀缓存整体失配(`context/mod.rs:239-246`);⑩ 一次压尽(windows 式分批未做);⑪ ✅ 已修(内容版本守卫,context_lifecycle 提交前比对快照);⑫ ✅ 已修(reset_compressing_after_panic)。
|
||||
- LOW ⑬ 估值双轨(estimated_prompt 准确用于路由 vs history_tokens 失真用于预算)从不互相校准;⑭ sys_tokens 未计工具 schema。
|
||||
- **已核良好**:裁剪三元组原子性正确(`context/mod.rs:612-642`);909 两处修复(锚点避让/单锁复位)现状有效,未见旧窗口复现。
|
||||
|
||||
Reference in New Issue
Block a user