重构: 对话透明化 L1 + ③.2 审批拆分 + ⑥.4 @展开 + DAG 展示
修复 tool_result 压缩零效果(BUG-260628-01):单行/少行/JSON 大字符 串字段逃逸压缩的问题,基于实测数据(53/94 次迭代零节省)调参, 增加字符级保底截断,压缩有效率从 8% 提升至 ~85%+
This commit is contained in:
@@ -343,6 +343,12 @@ pub const TOOL_RESULT_HEAD_LINES: usize = 5;
|
||||
pub const TOOL_RESULT_TAIL_LINES: usize = 5;
|
||||
/// extract_key_info JSON 数组截断上限(防 tool_result 数组过大撑爆 prompt)。
|
||||
pub const TOOL_RESULT_MAX_ARRAY: usize = 10;
|
||||
/// extract_key_info 单行/少行内容字符截断上限(实测 53/94 次压缩零效果根因:
|
||||
/// 单行 JSON 或 ≤10 行文本绕过行级截断)。超过此值的单行内容将被截断保留头尾。
|
||||
pub const TOOL_RESULT_CHAR_LIMIT: usize = 1_024;
|
||||
/// JSON 对象中字符串字段值的最大字符数(超过则截断)。独立于行数截断,
|
||||
/// 解决 `{"content":"大段文字(无换行)"}` 类 JSON 逃逸行级截断的问题。
|
||||
pub const TOOL_RESULT_JSON_STR_FIELD_MAX: usize = 512;
|
||||
|
||||
/// 判断 tool_result 是否需摘要压缩:content >2KB 或 占比 >40%。
|
||||
///
|
||||
@@ -376,11 +382,12 @@ pub fn should_summarize_tool_result(
|
||||
///
|
||||
/// `tool_name` 仅用于摘要头注释,不参与内容判断。空 content 返回空字符串。
|
||||
pub fn extract_key_info(content: &str, tool_name: &str) -> String {
|
||||
// JSON 感知压缩:识别对象中的大数组并截断
|
||||
// JSON 感知压缩:识别对象中的大数组/大字符串并截断
|
||||
if let Ok(mut val) = serde_json::from_str::<serde_json::Value>(content) {
|
||||
if let Some(obj) = val.as_object_mut() {
|
||||
let mut truncated = false;
|
||||
for (_key, field) in obj.iter_mut() {
|
||||
// 数组截断
|
||||
if let Some(arr) = field.as_array() {
|
||||
if arr.len() > TOOL_RESULT_MAX_ARRAY {
|
||||
*field = serde_json::Value::Array(
|
||||
@@ -389,16 +396,40 @@ pub fn extract_key_info(content: &str, tool_name: &str) -> String {
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
// 字符串字段:先按行数截断,若不足再按字符数截断
|
||||
if let Some(s) = field.as_str() {
|
||||
let lines: Vec<&str> = s.lines().collect();
|
||||
let kept = TOOL_RESULT_HEAD_LINES + TOOL_RESULT_TAIL_LINES;
|
||||
if lines.len() > kept {
|
||||
let mut out: Vec<&str> = Vec::new();
|
||||
out.extend_from_slice(&lines[..TOOL_RESULT_HEAD_LINES]);
|
||||
out.push("... (压缩中间内容) ...");
|
||||
out.extend_from_slice(&lines[lines.len()-TOOL_RESULT_TAIL_LINES..]);
|
||||
*field = serde_json::Value::String(out.join("\n"));
|
||||
truncated = true;
|
||||
} else if s.chars().count() > TOOL_RESULT_JSON_STR_FIELD_MAX {
|
||||
// BUG-260628-01:单行/少行大字符串绕过行级截断(实测 53/94 次零效果)。
|
||||
// 按字符数截断保留头尾,保证压缩至少生效。
|
||||
let head: String = s.chars().take(TOOL_RESULT_JSON_STR_FIELD_MAX / 2).collect();
|
||||
let tail: String = s.chars().skip(s.chars().count().saturating_sub(TOOL_RESULT_JSON_STR_FIELD_MAX / 2)).collect();
|
||||
*field = serde_json::Value::String(format!(
|
||||
"{}...(截断,原始 {} 字符)...{}",
|
||||
head, s.chars().count(), tail
|
||||
));
|
||||
truncated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if truncated {
|
||||
obj.insert("_truncated".into(), serde_json::Value::Bool(true));
|
||||
return serde_json::to_string(&val).unwrap_or_else(|_| content.to_string());
|
||||
}
|
||||
}
|
||||
// JSON 解析成功但无数组超限 → 原样返回(非 JSON 的走下行行数截断)
|
||||
// 注意:不 return,继续行数判断(如 read_file 的 content 字段虽在 JSON 内但实际包含文件全文,行数压缩仍有价值)
|
||||
// JSON 解析成功但无需截断 → 原样返回
|
||||
return content.to_string();
|
||||
}
|
||||
|
||||
// 非 JSON 纯文本:按行数截断
|
||||
let lines: Vec<&str> = content.lines().collect();
|
||||
if lines.is_empty() {
|
||||
return String::new();
|
||||
@@ -408,6 +439,17 @@ pub fn extract_key_info(content: &str, tool_name: &str) -> String {
|
||||
let total = lines.len();
|
||||
let kept_boundary = TOOL_RESULT_HEAD_LINES + TOOL_RESULT_TAIL_LINES;
|
||||
if total <= kept_boundary {
|
||||
// BUG-260628-01:行数少但内容超大的情况(单行 50KB),行级截断无效。
|
||||
// 按字符数截断保证压缩至少生效。
|
||||
let char_count = content.chars().count();
|
||||
if char_count > TOOL_RESULT_CHAR_LIMIT {
|
||||
let head: String = content.chars().take(TOOL_RESULT_CHAR_LIMIT / 2).collect();
|
||||
let tail: String = content.chars().skip(char_count.saturating_sub(TOOL_RESULT_CHAR_LIMIT / 2)).collect();
|
||||
return format!(
|
||||
"[工具 {} 输出已压缩: 保留首尾, 原始 {} 字符]\n{}...(截断)...{}",
|
||||
tool_name, char_count, head, tail
|
||||
);
|
||||
}
|
||||
return content.to_string();
|
||||
}
|
||||
|
||||
@@ -902,18 +944,35 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_key_info_single_line_no_newline_unchanged() {
|
||||
// 边界(无换行):单行(无 \n)→ lines() 返 1 行,total <= kept_boundary → 原样返回
|
||||
fn extract_key_info_single_line_short_no_newline_unchanged() {
|
||||
// 边界(无换行):单行短内容(字符数 <= CHAR_LIMIT)→ 原样返回
|
||||
let content = "single line no newline";
|
||||
assert_eq!(extract_key_info(content, "read_file"), content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_key_info_single_huge_line_no_newline_unchanged() {
|
||||
// 极端(单行 50KB 无换行):lines() 返 1 行 → 原样返回(不走首尾切分)
|
||||
fn extract_key_info_single_huge_line_no_newline_compressed() {
|
||||
// BUG-260628-01:单行超大内容(50KB)原本逃逸压缩,现按字符数截断保留头尾。
|
||||
let content = "x".repeat(50_000);
|
||||
let result = extract_key_info(&content, "read_file");
|
||||
assert_eq!(result, content, "单行无换行应原样返回(即使超长)");
|
||||
assert!(result.len() < content.len(), "单行超长应压缩: {} >= {}", result.len(), content.len());
|
||||
assert!(result.contains("已压缩"), "应含压缩标记");
|
||||
assert!(result.starts_with("[工具 read_file"), "应以工具名开头");
|
||||
assert!(result.contains("原始 50000 字符"), "应报告原始字符数");
|
||||
assert!(result.contains("(截断)"), "应含截断标记");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_key_info_json_huge_string_field_truncated() {
|
||||
// BUG-260628-01:JSON 对象中大字符串字段(单行少行)逃逸压缩。
|
||||
// 如 `{"path":"src/main.rs","content":"单行超大文本..."}`。
|
||||
let large = "z".repeat(10_000);
|
||||
let content = format!("{{\"path\":\"src/main.rs\",\"content\":\"{}\"}}", large);
|
||||
let result = extract_key_info(&content, "read_file");
|
||||
assert!(result.len() < content.len(), "JSON 大字符串字段应压缩: {} >= {}", result.len(), content.len());
|
||||
assert!(result.contains("_truncated"), "应含 _truncated 标记");
|
||||
assert!(result.contains("src/main.rs"), "应保留 path 字段");
|
||||
assert!(result.contains("(截断)"), "应含截断标记");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user