AI Agent 上下文窗口管理:对话长了,怎么自动裁剪不丢关键信息
AI Agent 上下文窗口管理对话长了怎么自动裁剪不丢关键信息一、对话窗口是 Agent 的短期记忆天花板AI Agent 和普通聊天机器人有一个根本的区别Agent 需要记住之前做了什么。不能每轮对话都从零开始——它要知道上一轮调用了哪个 API、返回值是什么、推断出什么结论。这些信息构成了 Agent 的工作记忆——也就是上下文消息列表。但上下文窗口是有限的。不同模型窗口大小从 8K 到 128K Token 不等但无论多大当 Agent 连续运行十几轮推理之后早期消息总会把它们填满。一旦窗口满最简单的处理就是截断——把最早的消息丢弃。但如果你在第三轮让 Agent 找到了一个关键文件路径到了第十一轮需要引用这个路径时它却被截断了Agent 就只能凭空猜测。这就是上下文管理的核心矛盾怎么在有限的窗口里保留最重要的信息丢掉不太重要的同时保证丢掉的不会让 Agent 后续推理出错。下面从保留策略、消息重要性评分和摘要生成三个角度来梳理一个可用的方案。二、消息保留策略——不是简单裁前面如果只是从最前面删相当于假设越早的消息越不重要这在很多场景下是错的。一个有效的保留策略需要分层处理有的消息永远不删有的按时间衰减有的直接替换成摘要。flowchart TD A[新消息到达\n总 Token 窗口限制] -- B[A 层: 固定保留\n系统提示词、工具定义\n(约占总窗口 10%-20%)] B -- C[B 层: 近 N 轮原始对话\n保留最近 5-8 轮完整消息\n(约占窗口 50%-60%)] C -- D[C 层: 中间轮次摘要\n对 9-N 轮之前的消息\n生成压缩摘要存储] D -- E[D 层: 长期记忆槽\n提取关键事实(路径/结论)\n以结构化字段存储] E -- F{组装最终上下文} A -- F F -- G[A层 C层摘要 B层对话 D层事实\n发送给 LLM]这张图表达的核心设计是分层保留。A 层是绝对不丢的东西——系统 Prompt 里写的 Agent 角色、可用工具列表、输出格式要求。这些信息丢了 Agent 的行为就会完全走样。B 层是最近的原始对话保持完整形态因为 LLM 的推理质量在原始对话上比摘要高得多。C 层是摘要区域把很久之前的冗长对话压成一段精炼的文本告诉 Agent之前发生了什么。D 层是可选的在某些场景下关键的离散信息文件路径、API 返回的 ID、用户提到的配置参数应该被显式提取出来独立于对话流保存。三、实现一个基础的滑动窗口 摘要生成器use std::collections::VecDeque; /// 单条上下文消息 #[derive(Clone)] struct Message { role: String, // system | user | assistant | tool content: String, token_count: usize, // 近似 Token 数 (4 字符 ≈ 1 token) } /// 上下文管理器 — 滑动窗口 摘要 struct ContextManager { system_messages: VecMessage, // A 层 — 永不删除 recent_messages: VecDequeMessage, // B 层 — 滑动窗口 max_window_tokens: usize, // 模型支持的总 Token 上限 recent_turns: usize, // B 层保留的最近轮次数 } impl ContextManager { fn new(max_window_tokens: usize, recent_turns: usize) - Self { ContextManager { system_messages: Vec::new(), recent_messages: VecDeque::new(), max_window_tokens, recent_turns, } } /// 设置系统消息 (只初始化一次) fn set_system(mut self, prompt: str) { self.system_messages.push(Message { role: system.into(), content: prompt.to_string(), token_count: prompt.len() / 4, // 粗略估算 }); } /// 添加一条新消息, 如果超窗口则自动裁旧 fn add_message(mut self, role: str, content: str) { let msg Message { role: role.to_string(), content: content.to_string(), token_count: content.len() / 4, }; self.recent_messages.push_back(msg); // 检查总 Token 是否超限, 超了就弹出最旧的消息 while self.total_tokens() self.max_window_tokens { // 生成被移除内容的摘要, 存储到内存 let removed self.recent_messages.pop_front(); // 如果移除的是 assistant 的推理结果, // 就用一句话总结关键结论存到 system_messages 末尾 if let Some(msg) removed { if msg.role assistant { let summary Self::simple_summarize(msg.content); self.system_messages.push(Message { role: system.into(), content: format!([历史摘要] {}, summary), token_count: summary.len() / 4, }); } } } } /// 计算当前所有消息的总 Token 数 fn total_tokens(self) - usize { let system_tokens: usize self.system_messages.iter().map(|m| m.token_count).sum(); let recent_tokens: usize self.recent_messages.iter().map(|m| m.token_count).sum(); system_tokens recent_tokens } /// 简易摘要 — 实际场景替换为调用 LLM 生成摘要 fn simple_summarize(content: str) - String { // 生产环境应该调用摘要模型, 这里用截断做演示 if content.len() 200 { content.to_string() } else { format!({}...(共 {} 字), content[..200], content.len()) } } /// 组装最终发送给 LLM 的上下文 fn build_context(self) - VecMessage { let mut ctx self.system_messages.clone(); ctx.extend(self.recent_messages.clone().into_iter()); ctx } } fn main() { let mut ctx ContextManager::new(8000, 6); // 8K 窗口, 保留最近 6 轮 ctx.set_system(你是一个文件管理 Agent, 可以读取和编辑文件。); // 模拟 20 轮对话 for i in 1..20 { ctx.add_message(user, format!(请读取文件 report_{}.txt, i)); ctx.add_message( assistant, format!(文件 report_{}.txt 的内容: 这是第 {} 份报告..., i, i), ); } let context ctx.build_context(); println!(最终上下文共 {} 条消息, context.len()); println!( 其中系统消息 {} 条, 包含历史摘要, ctx.system_messages.len() ); }这个实现里有一个重要的设计选择摘要信息的存储位置。我把摘要放到了system_messages列表里以[历史摘要]前缀开头。这样做的原因是 LLM 对 system 级指令的注意力通常比对话消息更强——系统提示词中的信息更不容易被模型忽略。这是一个小但有效的策略。四、两个需要持续优化的边界第一个问题是摘要的质量。上面代码里的simple_summarize只是一个截断但在实际系统中你需要用 LLM 自己来生成摘要——发给一个轻量模型让它在极短的延迟内输出被删除内容的总结。但这个过程中有一个摘要精度的悖论摘要越精简压缩率越高关键信息遗漏的概率也越高。目前没有一个完美的数学公式来决定压缩到多短需要根据领域场景反复调整。第二个问题是关键信息的提取。有些信息是离散的、可以被结构化存储的——比如 Agent 找到的文件路径、解析出的用户 ID、调用的命令和返回值。这类信息不应该只以对话摘要的形式存在而应该被单独存为一个 key-value 字典长期记忆槽在每次构建上下文时拼接到 system prompt 末尾。这种做法比纯摘要更精确但因为需要额外设计提取和更新的逻辑增加了实现复杂度。五、总结上下文剪裁的核心不是删掉什么而是留下什么以及以什么形式留下。A 层系统和工具定义永不丢失B 层最近几轮对话保持完整C 层老对话用摘要替代——这套分层保留策略在实际 Agent 开发中已经被广泛验证。对于刚开始构建 Agent 的开发者建议先从最简单的近 N 轮截断开始跑通了基本逻辑再来加摘要层。过早引入复杂的上下文管理反而会让 Agent 行为更难调试。但最终当你的 Agent 需要执行超过 20 轮的任务时一套分层裁剪方案是绕不开的。