新增: 工具结果落盘瘦身与空轮自愈

- 超阈值工具结果正文落盘+句柄可读回(治 read_file 29.6% 体量裸奔)
- read_file 改小文件豁免(8KB), 超阈值压缩+落盘句柄, 无损可找回
- 空轮判据自愈: 连续空轮先 nudge 续轮, 达阈值停靠止损(开关可回退)
- 零 token 收敛信号透传(非截断语义分离)
- 上下文清单描述按字符截断, 防清单体量无限膨胀
This commit is contained in:
lxy
2026-10-11 07:33:27 +08:00
parent 18e50c5af7
commit 31510d3555
8 changed files with 494 additions and 17 deletions
+84 -5
View File
@@ -555,6 +555,24 @@ pub(crate) async fn build_system_prompt_with_excluded(
prompt
}
/// 项目/任务清单单条描述截断上限(字符数,中文一字一符)。
///
/// 防清单体量随数据无限增长:描述可粘贴长文档,20 项目 + 20 任务 × 长描述可达数万字符,
/// 截断后接「…」。技能清单同类截断见 agentic/mod.rs `SKILL_DESC_MAX_CHARS`(80)。
const CONTEXT_DESC_MAX_CHARS: usize = 120;
/// 清单描述截断(纯函数):超 `max` 字符按 Unicode 标量截断并追加「…」,否则原样返回。
///
/// 按字符而非字节计数(中文描述一字一符,按字节会截断 UTF-8 序列产生乱码)。
fn truncate_desc(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
let mut out: String = s.chars().take(max).collect();
out.push('…');
out
}
/// 项目/任务全局清单段(DRY:主构建 [`build_system_prompt_with_excluded`] 与 caching 易变段
/// [`build_system_prompt_parts`] 共用)。
///
@@ -578,7 +596,13 @@ async fn build_context_lists(
if excluded_project_ids.iter().any(|id| id == &p.id) {
continue;
}
out.push_str(&format!("- {} (id: {}) ({}): {}\n", p.name, p.id, p.status.as_str(), p.description));
out.push_str(&format!(
"- {} (id: {}) ({}): {}\n",
p.name,
p.id,
p.status.as_str(),
truncate_desc(&p.description, CONTEXT_DESC_MAX_CHARS)
));
if let Some(ref dir) = p.path {
out.push_str(&format!(" 目录: {}\n", dir));
}
@@ -597,7 +621,13 @@ async fn build_context_lists(
if excluded_task_ids.iter().any(|id| id == &tk.id) {
continue;
}
out.push_str(&format!("- {} (id: {}) ({}): {}\n", tk.title, tk.id, tk.status.as_str(), tk.description));
out.push_str(&format!(
"- {} (id: {}) ({}): {}\n",
tk.title,
tk.id,
tk.status.as_str(),
truncate_desc(&tk.description, CONTEXT_DESC_MAX_CHARS)
));
}
// 机制层注明语(中/英):仅最近 20 条,全量/按项目查询走 list_tasks
out.push_str(&tasks_listed_note(lang));
@@ -631,10 +661,23 @@ pub(crate) fn cache_split_provider_type(provider_type: &str) -> bool {
)
}
/// 读取 prompt caching 拆分开关(AppState settings KV `df-ai-anthropic-cache`,默认关)
/// 解析 prompt caching 拆分开关原始值为布尔(纯函数,默认开)。
///
/// 值语义(见 [`cache_enabled`]):缺省/空(从未设置过)→ 默认开;
/// 显式 "false"/"0"(设置页开关关)→ 关 = 用户回退;
/// "true"/"1" 沿用旧识别;其余杂值保守按关(与历史默认关行为一致)。
/// 前端落库为 JSON 布尔字符串("true"/"false",无引号),直接等值比较即可。
fn parse_cache_flag(v: &str) -> bool {
v.is_empty() || v == "true" || v == "1"
}
/// 读取 prompt caching 拆分开关(AppState settings KV `df-ai-anthropic-cache`,默认开)
/// 并同步 df-ai 进程级显式缓存开关。
///
/// 值语义:非 "true"/"1"(含缺省/空)一律视为关——默认关、用户对支持的端点显式开启。
/// 值语义见 [`parse_cache_flag`]:默认开——拆分后易变段(日期/清单)移出稳定前缀,
/// anthropic 系显式缓存与 openai_compat 自动前缀缓存命中率提升;调用方另按
/// provider_type gate(`cache_split_provider_type`),不支持的端点仍走逐字等价旧路径,
/// 默认开对它们零影响。回退:设置页「AI Prompt 缓存」开关关(落库 "false")即整链回旧行为。
/// 同步:anthropic_compat::convert_request 读取同一进程级开关决定 system 形态
/// (数组+cache_control / 纯字符串),保证开关关时非官方网关零风险。
/// 键名保持历史名(向后兼容,已有键不换名),语义已扩展覆盖 openai_compat 前缀稳定拆分。
@@ -646,7 +689,7 @@ pub(crate) async fn cache_enabled(state: &AppState) -> bool {
.ok()
.flatten()
.unwrap_or_default();
let enabled = v == "true" || v == "1";
let enabled = parse_cache_flag(&v);
df_ai::anthropic_compat::set_anthropic_cache_enabled(enabled);
enabled
}
@@ -1038,4 +1081,40 @@ mod tests {
assert!(en_task.contains("20 most recent tasks"));
assert!(en_task.contains("list_tasks"));
}
// 缓存拆分开关默认值断言(看板 939 ⑧:默认关→默认开)
#[test]
fn parse_cache_flag_defaults_on_and_honors_explicit_off() {
// 缺省/空(从未设置过)→ 默认开
assert!(parse_cache_flag(""), "缺省(未设置)应默认开");
assert!(parse_cache_flag("true"));
assert!(parse_cache_flag("1"));
// 显式回退:设置页开关关落库 JSON 布尔 "false"
assert!(!parse_cache_flag("false"), "显式 false 应为关(用户回退通道)");
assert!(!parse_cache_flag("0"));
// 杂值保守按关(与历史默认关行为一致,不因脏数据意外开启)
assert!(!parse_cache_flag("garbage"));
}
// 清单描述截断(纯函数):短原样 / 恰好上限不截 / 超长截断加 … / 中文按字符非字节
#[test]
fn truncate_desc_short_exact_and_cjk() {
// 短描述与空串原样
assert_eq!(truncate_desc("hello", 120), "hello");
assert_eq!(truncate_desc("", 120), "");
// 恰好等于上限不截断(无省略号)
let exact = "字".repeat(120);
assert_eq!(truncate_desc(&exact, 120), exact);
// 超长:截到 max 字符 + …
assert_eq!(truncate_desc("abcdef", 5), "abcde…");
// 中文一字一符(按 Unicode 标量计数,非 UTF-8 字节,不产生乱码截断)
let long = "汉".repeat(130);
let got = truncate_desc(&long, 5);
assert_eq!(got, "汉汉汉汉汉…");
// 常量绑定:实际清单截断上限 120(防止常量被误改无人知)
assert_eq!(CONTEXT_DESC_MAX_CHARS, 120);
let got120 = truncate_desc(&long, CONTEXT_DESC_MAX_CHARS);
assert_eq!(got120.chars().count(), 121);
assert!(got120.ends_with('…'));
}
}