Agent中RAG 对话系统的 6 种压缩方案:从滑动窗口到 AFM 自适应保真度
RAG 对话系统的 6 种压缩方案:从滑动窗口到 AFM 自适应保真度
本文介绍一个 Rust 实现的 RAG 对话系统中 6 种对话历史压缩策略,从最简单的滑动窗口到学术界最新的 AFM 自适应保真度压缩,并附带完整实现代码。
一、6 种压缩方案详解
方案 1:滑动窗口(Sliding Window)
思路:保留最近 N 条消息,之前的全部丢弃。
fn apply_sliding_window(history, keep_count) {
if history.len() <= keep_count { return history; }
// 从尾部取最近 N 条
history.into_iter()
.rev().take(keep_count).rev()
.collect()
}
优点:零延迟,不调 LLM,实现极简
缺点:暴力丢弃,关键设定(“我叫小爱同学”)全部丢失
适用:简单问答、测试环境
方案 2:Token 限制(Token Limit)
思路:保留前 N 条(通常含用户初始设定)+ 从尾部往前取,不超总 token 预算。
fn apply_token_limit(history, max_tokens) {
// 1. 保留前 2 条(关键设定)
let first_n = history.take(2);
// 2. 从尾部往回加,直到 token 超限
let mut recent = vec![];
for msg in history.iter().rev() {
if total_tokens + msg.tokens <= max_tokens {
recent.insert(0, msg);
} else { break; }
}
// 3. 组合
[first_n, recent]
}
优点:精确控制 token 消耗,兼顾开头设定
缺点:中间消息全丢,token 估算是近似值
适用:API 成本敏感场景
方案 3:摘要压缩(Summary)
思路:旧消息全部调 LLM 生成一条摘要,替换原始消息。
async fn apply_summary_compression(history, threshold) {
// 分离旧消息和最近消息
let to_compress = history[..history.len() - keep_recent];
let recent = history[history.len() - keep_recent..];
// 调 LLM 生成摘要
let prompt = format!("请将以下对话压缩成摘要(100字内):\n{}", to_compress);
let summary = llm.invoke(prompt).await;
// 组合
[summary_msg, recent]
}
优点:保留语义,压缩率高(70-94%)
缺点:首次触发要等 LLM 返回(几秒)
适用:长对话(>20 条),需要保留大意的场景
方案 4:分层压缩(Layered)
思路:消息分三层,每层不同处理策略。
全部消息 (50条)
├── ⭐ 重要消息(含关键词:名字、设定、记住...)
│ → 原样保留
├── 📌 最近消息(最近 5 条)
│ → 原样保留
└── 📝 中间消息(剩下的)
→ 调 LLM 压缩成摘要
重要消息通过配置的关键词匹配:
important_keywords = ["我的名字", "我是", "记住", "设定", "角色"]
独创机制:重要消息内容会被 LLM 提炼后存入 important_context 字段,每次对话作为 system prompt 自动注入:
system: "请用中文回答... 【重要设定】角色=电子小狗,名字=小爱同学"
优点:关键信息永不丢失,持久化存储
缺点:关键词匹配不够智能
适用:角色扮演、任何需要保护关键设定的对话
方案 5:AFM 自适应保真度(Adaptive Focus Memory)
基于论文:Adaptive Focus Memory for Language Models (arXiv: 2511.12712)
思路:不是"保不保留"的二元选择,而是给每条消息分配三级保真度:
每条消息的保真度:
├── Full(完整保留):含关键设定、用户约束
├── Compressed(精简保留):LLM 提炼为一句话
└── Placeholder(占位符):"此处省略了 X 条闲聊"
分类由 LLM 逐条判断:
async fn classify_messages(messages) -> Vec<FidelityLevel> {
let prompt = format!(
"对每条消息分类:\n\
F=完整保留(含关键设定、用户约束)\n\
C=精简保留(有参考价值但非关键)\n\
P=占位符(闲聊或无关内容)\n\
按顺序输出(F/C/P),逗号分隔:\n\n{}",
messages
);
let result = llm.invoke(prompt).await;
// 解析 "F, C, P, F, C, ..." 格式
}
每条 Compressed 消息单独调 LLM 精简为 20 字内的一句话。
优点:比关键词匹配智能得多,保留信息最多
缺点:LLM 调用次数多,延迟高
适用:对信息保留要求极高的场景
方案 6:话题分段压缩(Topic Segmentation / Episodic Memory)
基于论文:Multi-Layered Memory Architectures for LLM Agents (arXiv: 2603.29194)
思路:用 LLM 检测对话中的话题切换点,每个话题独立生成摘要:
对话历史:
├── 话题1: "Rust是什么?如何安装?" (msg 1-8)
│ └── 摘要: [话题] "用户询问Rust语言的定义和基本用法"
├── 话题2: "AI扮演电子小狗,取名小爱同学" (msg 9-15)
│ └── 摘要: [话题] "用户请求AI扮演电子小狗角色,命名为小爱同学"
├── 话题3: "讨论苹果和西瓜的营养价值" (msg 16-22)
│ └── 摘要: [话题] "用户询问水果营养价值,AI以小狗口吻回答"
└── 最近消息 (msg 23-25) → 完整保留
话题边界由 LLM 自动检测:
async fn detect_topic_boundaries(messages) {
let prompt = format!(
"以下对话中,话题切换发生在哪条消息之后?\n\
输出切换点的序号(从0开始),逗号分隔。\n\n{}",
messages
);
// 解析 "3, 8, 15" 格式
let boundaries = llm.invoke(prompt).await;
// 返回 [3, 8, 15, total]
}
优点:按话题组织,比单一摘要更清晰,比分层压缩更智能
缺点:LLM 调用次数多(边界检测 + 每个话题的摘要)
适用:长对话(>50 条)且涉及多个不同话题
四、增量压缩机制:不删消息,只记进度
所有 6 种模式共享同一个增量压缩机制:
session 表字段: compressed_until INTEGER DEFAULT 0
原理:
├── 原始消息永远不删
├── compressed_until 记录已压缩到的位置
├── get_history 自动跳过已压缩的消息
└── 每次压缩只处理新增部分
查询历史:
SELECT * FROM message
WHERE session_id = ?
AND (time_created > compressed_until OR role = 'summary')
效果:100 条消息,只发 6 条(摘要 + 最近 5 条),省 94% token。
二、对比总结
| 维度 | 滑动窗口 | Token限 | 摘要 | 分层 | AFM | 话题分 |
|---|---|---|---|---|---|---|
| 调 LLM | ❌ | ❌ | ✅ | ✅ | ✅✅ | ✅✅ |
| 延迟 | 0 | 0 | 几秒 | 几秒 | 高 | 高 |
| 语义保留 | 差 | 中 | 好 | 好 | 极好 | 极好 |
| 关键信息保护 | ❌ | 前2条 | ❌ | ✅ | ✅ | ✅ |
| 实现复杂度 | ★ | ★ | ★★ | ★★★ | ★★★★ | ★★★★ |
选型建议
| 场景 | 推荐 |
|---|---|
| 简单问答 | 滑动窗口 |
| 控制 API 成本 | Token 限制 |
| 一般长对话 | 摘要压缩 |
| 角色扮演/有设定 | 分层压缩 ⭐ |
| 信息保留要求极高 | AFM 自适应 |
| 长对话多话题 | 话题分段 |
更多推荐



所有评论(0)