新增: 上下文裁剪淘汰补偿摘要
超预算视图裁剪时,被淘汰文本提炼关键词概要插回发送视图头部,补偿无声丢弃的早期主题 摘要以 user 角色注入:system 角色会被兼容层折叠进带 cache_control 的顶层段,纯视图每轮再生成会持续改写缓存指纹 纯视图语义不落库,被淘汰原文存储态不变;压缩失败兜底摘要与淘汰摘要共用提炼核心
This commit is contained in:
@@ -7,7 +7,9 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use super::*;
|
||||
use crate::context_helpers::{ContextConfig, TokenEstimator, PROTECT_COUNT};
|
||||
use crate::context_helpers::{
|
||||
ContextConfig, EVICTION_SUMMARY_MAX_CHARS, TokenEstimator, PROTECT_COUNT,
|
||||
};
|
||||
use crate::provider::ToolCall;
|
||||
|
||||
fn cfg(max_tokens: u32) -> ContextConfig {
|
||||
@@ -994,3 +996,160 @@ fn phase1_append_after_dirty_stays_dirty_until_save() {
|
||||
mgr.advance_persisted_count(2);
|
||||
assert!(!mgr.needs_full_rewrite());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 视图裁剪补偿摘要(淘汰区头部 user 概要,防无声丢失)
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn eviction_inserts_summary_message_at_view_head() {
|
||||
// 触发淘汰 → 视图头部应有一条 "[早期对话概要]" user 摘要(补偿无声丢弃的早期主题),
|
||||
// 概要 ≤ EVICTION_SUMMARY_MAX_CHARS,保护消息完好,且纯视图(不污染内存全量/不落库)。
|
||||
let mut mgr = ContextManager::new(cfg(200));
|
||||
for i in 0..20 {
|
||||
if i % 2 == 0 {
|
||||
mgr.push(ChatMessage::user(&format!(
|
||||
"审批流程与审批门禁需要适配 第{}条消息内容足够长确保淘汰文本合计超过一百字符", i
|
||||
)));
|
||||
} else {
|
||||
mgr.push(ChatMessage::assistant(&format!(
|
||||
"第{}条回复内容同样较长用于撑爆上下文预算触发视图裁剪逻辑", i
|
||||
)));
|
||||
}
|
||||
}
|
||||
let (msgs, trimmed) = mgr.build_for_request(0);
|
||||
assert!(trimmed, "超预算应触发裁剪");
|
||||
|
||||
// 视图头部 = 补偿摘要(role=user,"[早期对话概要]" 前缀 + 高频主题)
|
||||
let head = msgs.first().expect("视图不应为空");
|
||||
assert!(
|
||||
matches!(head.role, MessageRole::User),
|
||||
"视图头部应为 user 摘要, 实际 {:?}",
|
||||
head.role
|
||||
);
|
||||
assert!(
|
||||
head.content.starts_with("[早期对话概要] 高频主题: "),
|
||||
"摘要格式不符, 实际 {}",
|
||||
head.content
|
||||
);
|
||||
assert!(
|
||||
head.content.contains("审批"),
|
||||
"摘要应含被淘汰区高频主题词'审批', 实际 {}",
|
||||
head.content
|
||||
);
|
||||
assert!(
|
||||
head.content.chars().count() <= EVICTION_SUMMARY_MAX_CHARS,
|
||||
"摘要应 ≤ {} 字符, 实际 {}",
|
||||
EVICTION_SUMMARY_MAX_CHARS,
|
||||
head.content.chars().count()
|
||||
);
|
||||
|
||||
// 保护消息完好:末条(i=19 assistant)保留;摘要 user 与首轮真实 user 相邻,由
|
||||
// sanitize 的 ensure_sequence_legal 合并(摘要在前,换行接原文),故 msgs[1] 为 i=1 的 assistant
|
||||
assert_eq!(
|
||||
msgs.last().unwrap().content,
|
||||
"第19条回复内容同样较长用于撑爆上下文预算触发视图裁剪逻辑",
|
||||
"保护区最新消息被误裁"
|
||||
);
|
||||
assert!(
|
||||
matches!(msgs[1].role, MessageRole::Assistant),
|
||||
"摘要与首轮 user 合并后,第二条应为 assistant, 实际 {:?}",
|
||||
msgs[1].role
|
||||
);
|
||||
assert!(
|
||||
msgs[0].content.contains("审批流程与审批门禁需要适配"),
|
||||
"合并不应丢失被保留的首轮真实 user 文本, 实际 {}",
|
||||
msgs[0].content
|
||||
);
|
||||
|
||||
// 纯视图:摘要不进内存全量,被淘汰原文存储态不变
|
||||
assert_eq!(mgr.all_messages_clone().len(), 20, "裁剪污染了内存全量");
|
||||
assert!(
|
||||
mgr.all_messages_clone()
|
||||
.iter()
|
||||
.all(|m| !m.content.starts_with("[早期对话概要]")),
|
||||
"补偿摘要不应写入存储(纯发送视图)"
|
||||
);
|
||||
}
|
||||
|
||||
fn role_tag(m: &ChatMessage) -> &'static str {
|
||||
match m.role {
|
||||
MessageRole::System => "system",
|
||||
MessageRole::User => "user",
|
||||
MessageRole::Assistant => "assistant",
|
||||
MessageRole::Tool => "tool",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_eviction_output_unchanged_no_summary() {
|
||||
// 预算内 → 输出与改动前一致:消息序列逐条原样,无补偿摘要插入(回归护栏)。
|
||||
let mut mgr = ContextManager::new(cfg(100_000));
|
||||
mgr.push(ChatMessage::user("第一条问题"));
|
||||
mgr.push(ChatMessage::assistant("第一条回复"));
|
||||
mgr.push(ChatMessage::user("第二条问题"));
|
||||
mgr.push(ChatMessage::assistant("第二条回复"));
|
||||
let (msgs, trimmed) = mgr.build_for_request(0);
|
||||
assert!(!trimmed);
|
||||
assert_eq!(msgs.len(), 4);
|
||||
let pair: Vec<(&str, &str)> = msgs
|
||||
.iter()
|
||||
.map(|m| (role_tag(m), m.content.as_str()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
pair,
|
||||
vec![
|
||||
("user", "第一条问题"),
|
||||
("assistant", "第一条回复"),
|
||||
("user", "第二条问题"),
|
||||
("assistant", "第二条回复"),
|
||||
],
|
||||
"预算内输出应与历史序列逐条一致"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eviction_tiny_text_no_summary() {
|
||||
// 被淘汰内容极短(单字消息,合计 <100 字符)→ 不值得产摘要,不插补偿摘要消息。
|
||||
// 20 条消息(约 5 token/条),cfg(40) 触发裁剪,淘汰前缀合计仅二十余字符。
|
||||
let mut mgr = ContextManager::new(cfg(40));
|
||||
for i in 0..20 {
|
||||
if i % 2 == 0 {
|
||||
mgr.push(ChatMessage::user(&format!("问{}", i)));
|
||||
} else {
|
||||
mgr.push(ChatMessage::assistant(&format!("答{}", i)));
|
||||
}
|
||||
}
|
||||
let (msgs, trimmed) = mgr.build_for_request(0);
|
||||
assert!(trimmed, "超预算应触发裁剪");
|
||||
assert!(
|
||||
msgs.iter()
|
||||
.all(|m| !m.content.starts_with("[早期对话概要]")),
|
||||
"极短淘汰文本不应产生补偿摘要, 实际 {:?}",
|
||||
msgs.iter().map(|m| format!("{:?}", m.role)).collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(mgr.all_messages_clone().len(), 20, "裁剪污染了内存全量");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eviction_no_user_text_no_summary() {
|
||||
// 被淘汰区无任何 user 消息(仅 assistant 长文本)→ 提炼核心只认 user → 无关键词 → 不插摘要。
|
||||
// 布局:10 条 assistant 长文本(淘汰候选)+ 6 条短 user(保护区),预算卡在淘汰 4 条 assistant。
|
||||
let mut mgr = ContextManager::new(cfg(150));
|
||||
for i in 0..10 {
|
||||
mgr.push(ChatMessage::assistant(&format!(
|
||||
"这是一条没有主题价值的 assistant 长文本第{}条,用于撑爆预算但提炼不出关键词", i
|
||||
)));
|
||||
}
|
||||
for i in 0..6 {
|
||||
mgr.push(ChatMessage::user(&format!("问{}", i)));
|
||||
}
|
||||
let (msgs, trimmed) = mgr.build_for_request(0);
|
||||
assert!(trimmed, "超预算应触发裁剪");
|
||||
assert!(
|
||||
msgs.iter()
|
||||
.all(|m| !m.content.starts_with("[早期对话概要]")),
|
||||
"无可提炼关键词(无 user)时不应插补偿摘要"
|
||||
);
|
||||
assert_eq!(mgr.all_messages_clone().len(), 16, "裁剪污染了内存全量");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user