新增: 工具结果落盘瘦身与空轮自愈
- 超阈值工具结果正文落盘+句柄可读回(治 read_file 29.6% 体量裸奔) - read_file 改小文件豁免(8KB), 超阈值压缩+落盘句柄, 无损可找回 - 空轮判据自愈: 连续空轮先 nudge 续轮, 达阈值停靠止损(开关可回退) - 零 token 收敛信号透传(非截断语义分离) - 上下文清单描述按字符截断, 防清单体量无限膨胀
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
//! 上下文侧工具结果正文外置(P1 工具结果瘦身)。
|
||||
//!
|
||||
//! 依据 docs/02-架构设计/上下文根治设计-20261006.md §二「工具结果瘦身(治最大头,先行)」:
|
||||
//! 超阈值工具结果**正文落盘**,上下文只留「摘要 + 句柄路径」;句柄可被 agent 主动展开
|
||||
//! (read_file 读句柄路径)——需要时找得回,不需要时不占位。与 src-tauri 审计侧 D2a 同构。
|
||||
//!
|
||||
//! 与 namespace_store 的区别(两者互补,不冲突):
|
||||
//! - namespace_store:内存态引用化(省进程内存),发 LLM 前展开为全文 → token 零收益。
|
||||
//! - 本模块:落盘 + 句柄,被省略的中段**不进 prompt 但可读回** → 真正减 token 且不丢信息。
|
||||
//!
|
||||
//! 降级契约:base 未注入(单测/未初始化)→ 不落盘返回 None,调用方退化为纯摘要(改造前行为)。
|
||||
//! 幂等:文件名取内容稳定哈希,同内容重复压缩只落盘一次。
|
||||
//! 保留策略:与全局「历史数据默认保留」一致,本模块自产文件只增不减(无清理调用点)。
|
||||
//!
|
||||
//! 测试隔离:核心逻辑 `offload_to(base, ...)` 显式传 base(零全局态),单测只测它;
|
||||
//! `init_base` 的 OnceLock 全局态一次性不可重置,故测试**绝不调用**它(避免污染同进程
|
||||
//! 其他用例——如 extract_key_info 系列断言会被意外注入的句柄行破坏)。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// 外置根目录(进程级一次注入;src-tauri lib.rs setup 调 init_base)。
|
||||
/// 与审计侧共用 {app_data},文件名前缀 `ctx-` 区分(tool-results/ 目录共存)。
|
||||
static BASE: OnceLock<PathBuf> = OnceLock::new();
|
||||
|
||||
/// 注入外置根目录(app data dir)。幂等,首次生效。
|
||||
pub fn init_base(base: PathBuf) {
|
||||
let _ = BASE.set(base);
|
||||
}
|
||||
|
||||
/// base 是否已注入(降级判定用)。
|
||||
pub fn is_enabled() -> bool {
|
||||
BASE.get().is_some()
|
||||
}
|
||||
|
||||
/// 内容稳定哈希(非密码学用途;碰撞后果仅同名文件互相覆盖,概率可忽略)。
|
||||
fn content_id(content: &str) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut h = DefaultHasher::new();
|
||||
content.hash(&mut h);
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// 落盘工具结果正文,返回可读句柄行(供摘要末尾追加)。base 未注入 → None。
|
||||
pub fn offload_with_handle(content: &str, tool_name: &str) -> Option<String> {
|
||||
offload_to(BASE.get()?, content, tool_name)
|
||||
}
|
||||
|
||||
/// 核心实现(显式 base,零全局态,可测)。
|
||||
///
|
||||
/// 建目录失败 / 写文件失败 → None(降级:调用方仅用摘要,不丢数据)。
|
||||
pub fn offload_to(base: &Path, content: &str, tool_name: &str) -> Option<String> {
|
||||
let dir = base.join("tool-results");
|
||||
if std::fs::create_dir_all(&dir).is_err() {
|
||||
return None;
|
||||
}
|
||||
let file = dir.join(format!("ctx-{:016x}.txt", content_id(content)));
|
||||
if !file.exists() && std::fs::write(&file, content).is_err() {
|
||||
return None;
|
||||
}
|
||||
Some(format!(
|
||||
"[工具 {} 原文 {} 字符已外置: {} —— 需完整内容用 read_file 读该路径]",
|
||||
tool_name,
|
||||
content.chars().count(),
|
||||
file.display()
|
||||
))
|
||||
}
|
||||
|
||||
/// 读回外置文件(查询侧/测试用)。文件缺失返回带语义错误(降级不 panic)。
|
||||
pub fn read_offloaded(path: &str) -> Result<String, String> {
|
||||
std::fs::read_to_string(path).map_err(|e| format!("外置结果读取失败: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tmp_base(tag: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!("df-ai-offload-{tag}-{}", std::process::id()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offload_to_writes_readable_file_and_is_idempotent() {
|
||||
let base = tmp_base("ok");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
let payload = "large tool output ".repeat(1000);
|
||||
let handle = offload_to(&base, &payload, "grep").expect("显式 base 应落盘");
|
||||
assert!(handle.contains("已外置"), "句柄应含提示文案: {handle}");
|
||||
assert!(handle.contains("read_file"), "句柄应指引读回方式: {handle}");
|
||||
// 幂等:同内容二次调用返回同一文件名
|
||||
let handle2 = offload_to(&base, &payload, "grep").expect("二次调用仍返回句柄");
|
||||
assert_eq!(handle, handle2, "同内容应幂等复用同一文件");
|
||||
// 落盘文件可读回原文(无损)
|
||||
let path = std::fs::read_dir(base.join("tool-results"))
|
||||
.expect("目录应存在")
|
||||
.flatten()
|
||||
.map(|e| e.path())
|
||||
.find(|p| p.extension().map(|e| e == "txt").unwrap_or(false))
|
||||
.expect("应有落盘文件");
|
||||
assert_eq!(
|
||||
read_offloaded(path.to_string_lossy().as_ref()).expect("应可读回"),
|
||||
payload
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_content_gets_different_file() {
|
||||
let base = tmp_base("diff");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
let h1 = offload_to(&base, &"a".repeat(3000), "grep").unwrap();
|
||||
let h2 = offload_to(&base, &"b".repeat(3000), "grep").unwrap();
|
||||
assert_ne!(h1, h2, "不同内容应落不同文件");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user