From 2fe1fe57c87de2faa861235cff4fe7dcb0bee4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 22 Jun 2026 01:45:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96:=20tool=5Fresult=E6=88=AA?= =?UTF-8?q?=E6=96=AD50KB=E2=86=928KB=E6=B2=BBGLM1214(conversation.rs?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=E8=A7=86=E5=9B=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/ai/conversation.rs | 37 ++++++++++++++--------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/commands/ai/conversation.rs b/src-tauri/src/commands/ai/conversation.rs index 5adfff5..2dc0c2f 100644 --- a/src-tauri/src/commands/ai/conversation.rs +++ b/src-tauri/src/commands/ai/conversation.rs @@ -57,12 +57,18 @@ pub(crate) fn accumulate_tokens(old: Option, add: u32) -> Option { /// /// 防 read_file 1MB 洞 / list_directory 大体量结果落库后每轮重发累积致 token 暴增 /// (Sprint 19 实测单对话 in=115万 / 消息体 1.6MB)。仅作用于持久化视图,不污染内存真相源。 -pub(crate) const TRUNCATE_THRESHOLD: usize = 50_000; -const TRUNCATE_HEAD: usize = 20_000; -const TRUNCATE_TAIL: usize = 20_000; +/// +/// B-260619-02:50KB → 8KB 收紧。治 GLM 1214 messages 非法根因——GLM anthropic 端点对 +/// 单条 tool_result content 限制更严(实测 ~10-20KB 即拒 1214),read_file 默认前 500 行源码 +/// 约 11-21KB,旧 50KB 阈值未触发→DB 存全量→reload/下轮重发→GLM 1214 整请求失败。 +/// 8KB 对齐 GLM 单条上限留余量;截断后 LLM 仍能据截断片段推理(优于 1214 直接拒收)。 +pub(crate) const TRUNCATE_THRESHOLD: usize = 8_192; +const TRUNCATE_HEAD: usize = 3_072; +const TRUNCATE_TAIL: usize = 3_072; -/// 落库前对超长 content 做截断(保留头尾各 ~20KB + 中段标注省略字符数)。 -/// 50KB 阈值以下原样返回(零开销);按字符而非字节切避免 UTF-8 切坏中文。 +/// 落库前对超长 content 做截断(保留头尾各 ~3KB + 中段标注省略字符数 + 总字节数)。 +/// 8KB 阈值以下原样返回(零开销);按字符而非字节切避免 UTF-8 切坏中文。 +/// 截断提示含原始字节数,让 LLM 明确感知非全文(防它误以为已看全)。 pub(crate) fn truncate_for_persist(content: &str) -> String { let chars: Vec = content.chars().collect(); if chars.len() <= TRUNCATE_THRESHOLD { @@ -72,15 +78,15 @@ pub(crate) fn truncate_for_persist(content: &str) -> String { let tail: String = chars[chars.len() - TRUNCATE_TAIL..].iter().collect(); let omitted = chars.len() - TRUNCATE_HEAD - TRUNCATE_TAIL; format!( - "{}\n\n[...省略 {} 字符(已截断,完整内容仅在内存态可读)...]\n\n{}", - head, omitted, tail + "{}\n\n[...省略 {} 字符(已截断,原 {} 字节,仅显示前 8KB,完整内容仅在内存态可读)...]\n\n{}", + head, omitted, content.len(), tail ) } /// 落库前对 `ChatMessage.parts` 做截断(F-260614-05 Phase 2a)。 /// -/// - `Text` 片:复用 `truncate_for_persist` 头尾截断语义(单 Text 片 > 50KB 才截)。 -/// - `Image` 片:base64 通常已 50KB+,落库前替换为占位 `Text` 片 +/// - `Text` 片:复用 `truncate_for_persist` 头尾截断语义(单 Text 片 > 8KB 才截)。 +/// - `Image` 片:base64 通常已 8KB+,落库前替换为占位 `Text` 片 /// ``,避免大体量图把对话 JSON 撑爆 /// (一张 1MB PNG 的 base64 ≈ 1.3MB 字符串)。原 Image 片仅在内存真相源(ContextManager) /// 保留,重发时仍带图——对齐现有「持久化视图不污染内存真相源」约定。 @@ -149,9 +155,10 @@ pub(crate) async fn save_conversation( touch_updated_at: bool, ) { // 取 messages + 懒创建首次落库所需的 provider_id/created_at - // 工具结果(content)超 50KB 时截断头尾各 ~20KB + 中段标注,防大体量结果(read_file 1MB 洞 / - // list_directory 13782 项)落库后每轮重发累积致 token 暴增。仅影响持久化视图,不污染 - // 内存真相源(ContextManager)——build_for_request 仍读全量 messages。 + // 工具结果(content)超 8KB(B-260619-02:50KB→8KB,治 GLM 1214)时截断头尾各 ~3KB + 中段 + // 标注(含原字节数),防大体量结果(read_file 1MB 洞 / list_directory 13782 项)落库后每轮重发 + // 累积致 token 暴增。仅影响持久化视图,不污染内存真相源(ContextManager)——build_for_request + // 仍读全量 messages。 // // F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源:本函数入参,与 save 落库的 conv 一致)。 // loop 内 save 由 run_agentic_loop 入参 conv_id 透传;IPC 路径(commands.rs)save 也传 conv_id。 @@ -354,9 +361,11 @@ mod tests { // 头尾各 20k 字符应在结果中 assert!(result.starts_with(&"x".repeat(TRUNCATE_HEAD))); assert!(result.ends_with(&"x".repeat(TRUNCATE_TAIL))); - // 中段标注存在 + 标注省略字符数(中段 = 总长 - 头 - 尾 = 51000 - 20000 - 20000 = 11000) + // 中段标注存在 + 标注省略字符数(中段 = 总长 - 头 - 尾 = 9192 - 3072 - 3072 = 3048) assert!(result.contains("已截断")); - assert!(result.contains("省略 11000 字符")); + assert!(result.contains("省略 3048 字符")); + // B-260619-02:截断提示含原字节数(9192 个 'x' = 9192 字节),让 LLM 感知非全文 + assert!(result.contains("原 9192 字节")); // 结果总长应远小于原长(20k 头 + 20k 尾 + 标注) assert!(result.chars().count() < TRUNCATE_THRESHOLD + 1000); }