- 人工智能
- AI Agent
- 代码智能体
- AI 应用
- CLI
- 开发工具
【免费下载链接】forgecode
AI enabled pair programmer for Claude, GPT, O Series, Grok, Deepseek, Gemini and 300+ models
本文以 plans/agent-context-compaction-2025-03-24.md 实现方案为骨架,结合 forgecode(AI enabled pair programmer for Claude、GPT、Grok、Deepseek、Gemini 等 300+ 模型)仓库中的实际源码与测试,完整讲解上下文自动压缩的设计目标、配置结构、触发判定、执行链路与迁移路径。读完本文,你将掌握如何通过
Agent.compact配置为智能体开启基于 token 数、轮数、消息数的自动上下文压缩,并理解其底层实现原理与调试手段。
一、背景与目标:为什么需要上下文自动压缩
在长会话的 AI Agent 应用中,每一轮对话都会把历史消息、工具调用与工具结果持续追加进上下文。随着会话推进,上下文不断膨胀,会带来三个直接后果:
- Token 成本持续上升:每轮请求都会把全部历史发送给模型,输入 token 线性增长;
- 超出模型上下文窗口:一旦超过模型 context window,会触发
context_length_exceeded类错误,导致会话中断; - 指令遵循质量下降:过长的上下文中,早期系统约束与用户意图容易被淹没。
为此,该实现方案提出:在Agent结构上新增一个字段,使上下文压缩能够基于可配置的触发条件(token 数量、轮数、消息数量)自动执行,并受一个最大 token 上限约束。目标是以一种“灵活、自动”的方式替代原先更复杂的基于 transform 的压缩方案。
从当前仓库代码看,这一设计已经落地为核心配置类型Compact(位于 crates/forge_domain/src/compact/compact_config.rs),并被Agent结构持有(见 crates/forge_domain/src/agent.rs)。
二、配置结构设计:Compaction / Compact 字段全解
2.1 方案文档中的原型设计
方案文档首先定义了Compaction结构体,包含三个可选触发阈值与一个必填的压缩后上限:
/// Configuration for automatic context compaction #[derive(Debug, Clone, Serialize, Deserialize, Setters)] #[setters(strip_option, into)] pub struct Compaction { /// Maximum token count before compaction is triggered #[serde(skip_serializing_if = "Option::is_none")] pub token_threshold: Option<usize>, /// Maximum number of turns before compaction is triggered #[serde(skip_serializing_if = "Option::is_none")] pub turn_threshold: Option<usize>, /// Maximum number of messages before compaction is triggered #[serde(skip_serializing_if = "Option::is_none")] pub message_threshold: Option<usize>, /// Maximum allowed token count after compaction pub max_tokens: usize, }并配套提供new(max_tokens)构造函数与should_compact(context, turn_count, message_count)判定方法——任一阈值被满足即返回true,否则返回false。
2.2 仓库中的最终实现:Compact结构体
实际仓库将配置结构命名为Compact,字段比方案原型更丰富,增加了eviction_window、retention_window、token_threshold_percentage、model、on_turn_end等控制项:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
retention_window | usize | 0 | 压缩时保留最近 N 条消息,不参与摘要;与eviction_window取更保守值(可压缩消息更少者优先) |
eviction_window | f64 | 0.2 | 可被摘要化的上下文比例上限,取值范围0.0~1.0,0.0表示不压缩,1.0允许全部摘要 |
max_tokens | Option<usize> | None | 压缩后保留的最大 token 数 |
token_threshold | Option<usize> | None | 触发压缩的绝对 token 上限,与token_threshold_percentage取较小值 |
token_threshold_percentage | Option<f64> | None | 以模型上下文窗口百分比推导的触发阈值,与token_threshold取较小值 |
turn_threshold | Option<usize> | None | 触发压缩的最大对话轮数 |
message_threshold | Option<usize> | None | 触发压缩的最大消息数 |
model | Option<ModelId> | None | 执行压缩使用的模型 ID(可用更便宜/更快的模型);不设置则回退到 Agent 根级模型 |
on_turn_end | Option<bool> | None | 当最后一条消息来自用户时触发压缩 |
值得注意的默认行为:Compact::new()将eviction_window初始化为0.2(即默认压缩 20% 的上下文),其余触发阈值均为None(表示不触发)。百分比字段在反序列化时会被校验,非法值(超出0.0~1.0)会直接报错,例如"percentage must be between 0.0 and 1.0, got 1.5"——这一校验逻辑见compact_config.rs中的deserialize_percentage与deserialize_optional_percentage。
2.3 触发判定:should_compact的四个维度
实际实现中should_compact的签名与方案原型不同:不再由调用方传入turn_count和message_count,而是直接从Context内部推导(源码见 compact_config.rs):
pub fn should_compact(&self, context: &Context, token_count: usize) -> bool { self.should_compact_due_to_tokens(token_count) || self.should_compact_due_to_turns(context) || self.should_compact_due_to_messages(context) || self.should_compact_on_turn_end(context) }四个维度为“或”关系,任一满足即触发:
- Token 维度:
token_count >= token_threshold(注意是>=,等于阈值即触发;未配置阈值则不触发); - 轮数维度:统计上下文中角色为 User 的消息条数
>= turn_threshold——也就是说“轮数”按用户消息数计,系统消息与助手消息不计入; - 消息维度:
context.messages.len() >= message_threshold,直接按总消息条数判定; - 回合结束维度:当
on_turn_end == true且最后一条消息来自用户时触发,适用于“用户提问后立即压缩”的场景。
三、Agent 结构扩展:compact 字段与安全阈值
3.1 字段挂载
方案文档要求为Agent增加compact字段,并遵循已有的序列化与合并模式:
/// Configuration for automatic context compaction #[serde(skip_serializing_if = "Option::is_none")] #[merge(strategy = crate::merge::option)] pub compact: Option<Compaction>,实际仓库中该字段已落地为非可选类型,默认值为Compact::default()(见 agent.rs 与Agent::new中的compact: Compact::default()):
/// Configuration for automatic context compaction pub compact: Compact,Compact本身派生Merge,其中retention_window、eviction_window使用merge::std::overwrite策略,其余可选字段使用merge::option策略——即“对方有值则覆盖,对方无值则保留本值”,与方案测试中预期的合并语义一致。
3.2 安全阈值兜底:compaction_threshold
仅靠用户配置的绝对token_threshold并不安全:如果阈值与模型上下文窗口之间预留余量不足,上下文可能先于压缩触发而撑爆窗口。仓库在Agent上提供了compaction_threshold(selected_model)方法(agent.rs)做兜底:
- 绝对上限来自
compact.token_threshold,未配置时默认100,000tokens; - 上下文窗口上限来自
compact.token_threshold_percentage,未配置时默认取模型上下文窗口的70%;模型元数据缺失时使用默认上下文窗口128K; - 最终取两者较小值写入
compact.token_threshold,为工具输出与后续消息预留安全余量。
例如模型上下文窗口为 80K 时,即使配置了token_threshold: 100_000,实际阈值也会被压到80_000 × 0.7 = 56_000。该逻辑在 agent.rs 中有 9 个针对性测试用例覆盖,其中 3 个是修复历史缺陷的回归测试(见下文“测试验证”一节)。
3.3 Token 估算与模型回退
Agent::set_compact_model_if_none():若compact.model未设置,则复制 Agent 根级模型作为压缩模型(agent.rs);estimate_token_count(count):按“约 4 个字符 ≈ 1 个 token”做粗略估算(count / 4),并注明生产环境应使用对应 LLM 的 tokenizer(agent.rs)。
四、触发与执行链路:从 Orchestrator 到 Hook + Compactor
4.1 方案文档中的 Orchestrator 设计
方案文档计划在Orchestrator中新增compact_context方法,并改造init_agent_with_event,在把上下文发送给 provider 之前完成压缩判定与执行:
async fn compact_context( &self, agent: &Agent, context: &mut Context ) -> anyhow::Result<()> { if let Some(config) = &agent.compact { let max_tokens = config.max_tokens; let mut summarize = Summarize::new(context, max_tokens); while let Some(mut summary) = summarize.summarize() { let content_to_summarize = summary.get(); // TODO: 未来增强可用 AI 模型生成更好的摘要 summary.set("Summary of previous conversation"); } } Ok(()) }即:先在init_agent_with_event中读取会话turn_count与context.messages.len(),调用config.should_compact(...)判定,需要压缩则执行compact_context并把压缩后的上下文回写。
4.2 仓库中的实际实现:Hook + Compactor 服务
从当前源码结构看,最终实现把“触发”与“执行”解耦为两层:
第一层:触发钩子CompactionHandler(crates/forge_app/src/hooks/compaction.rs)
它实现EventHandle<EventData<ResponsePayload>>,在响应事件到达时检查会话上下文:
if let Some(context) = &conversation.context { let token_count = context.token_count(); if self.agent.compact.should_compact(context, *token_count) { info!(agent_id = %self.agent.id, "Compaction triggered by hook"); let compacted = Compactor::new(self.agent.compact.clone(), self.environment.clone()) .compact(context.clone(), false)?; conversation.context = Some(compacted); } else { debug!(agent_id = %self.agent.id, "Compaction not needed"); } }第二层:执行器Compactor(crates/forge_app/src/compact.rs)
Compactor::compact(context, max)的核心流程为:
- 由
eviction_window构造CompactionStrategy::evict(percentage),由retention_window构造CompactionStrategy::retain(n); - 非强制模式(
max == false)取两者较小值eviction.min(retention)(更保守);强制模式取retention; - 调用
strategy.eviction_range(&context)定位需要压缩的消息区间(start, end); - 对区间内的消息(跳过可丢弃消息)生成
ContextSummary,经SummaryTransformer清洗后,渲染进模板 templates/forge-partial-summary-frame.md,替换原区间。
CompactionStrategy定义在 crates/forge_domain/src/compact/strategy.rs,包括Evict(f64)、Retain(usize)、Min、Max四种变体:百分比策略会先把 token 预算换算成“保留前 N 条消息”的等价形式(to_fixed),再交由find_sequence_preserving_last_n找出从第一条助手消息开始、止于保留窗口之前的连续区间。
4.3 摘要生成与两个关键保护
compress_single_sequence中值得关注的三个细节:
- 清洗管线:
SummaryTransformer(来自 crates/forge_app/src/transformers/)会依次执行“丢弃 system 角色消息 → 去重连续用户消息 → 每个文件路径只保留最后一次操作 → 去重连续助手内容块 → 剥离工作目录前缀”; - 推理链保持:若压缩区间的消息带有
reasoning_details,会取最近一条的推理细节注入压缩后第一条助手消息,避免 extended thinking 场景下推理链断裂(代码注释给出了示例:[U, A+r, U, A+r, U, A] → compact → [U-summary, A+r, U, A]); - 用量累积:压缩前会把区间内所有消息的
usage累加并转交给摘要消息,保证 token 用量统计不因压缩而丢失。
摘要结果以一条User 角色的文本消息(内容为 summary frame)替换原消息区间,模板forge-partial-summary-frame.md会把历史中的文件操作、搜索模式、shell 命令、Skill、MCP 调用、任务清单等以结构化 Markdown 呈现给模型,让后续轮次仍能“记住”关键操作。
五、Token 计数机制
方案文档要求增强 token 计数函数:
fn token_count(text: &str) -> usize { text.split_whitespace().count() * 75 / 100 }并注明这是占位实现,生产环境应使用真正的 tokenizer。仓库的最终实现比该占位函数更完善(见 crates/forge_domain/src/context.rs):
pub fn token_count(&self) -> TokenCount { let actual = self.messages.last().as_ref() .and_then(|u| u.usage) .map(|u| u.total_tokens) .unwrap_or_default(); match actual { TokenCount::Actual(actual) if actual > 0 => TokenCount::Actual(actual), _ => TokenCount::Approx(self.token_count_approx()), } }即:优先使用 provider 返回的最后一条消息的真实usage.total_tokens(TokenCount::Actual);不可用时才回退到逐消息近似估算token_count_approx()(各消息的token_count_approx之和)。ContextMessage::token_count_approx对文本、工具调用、工具结果、推理内容、图片等不同类型分别估算,相关单测覆盖了 Unicode、空内容、图片消息等边界情况(context.rs)。
六、旧方案迁移:Transform::Assistant 的弃用
方案文档明确要求淘汰基于 transform 的压缩方式,步骤包括:
- 为
Transform::Assistant变体增加弃用注释:
pub enum Transform { /// Compresses multiple assistant messages into a single message /// /// DEPRECATED: Use the new `compact` field on the Agent struct instead. #[deprecated( since = "next_version", note = "Use the compact field on Agent instead" )] Assistant { // existing fields... }, // Other variants... }- 在
execute_transform中对该变体输出警告日志:
Transform::Assistant { .. } => { tracing::warn!( "Transform::Assistant is deprecated. Use the compact field on Agent instead." ); // Existing implementation... }- 在文档中说明迁移路径:新配置集中在
Agent.compact,由 Hook/Compactor 统一处理,无需再手写 transform 链。从当前仓库的 transformers 目录(crates/forge_app/src/transformers/)看,该目录仍保留着compaction.rs、dedupe_role.rs、trim_context_summary.rs等转换器——其中SummaryTransformer已被Compactor内部复用,也就是说压缩能力从“外部 transform 管线”收敛为“Agent 配置驱动的内置机制”,但清洗转换器本身得到了复用,而不是被简单删除。
七、测试验证:从方案测试到仓库回归测试
7.1 方案文档规划的测试
方案文档在crates/forge_domain/src/agent.rs中规划了三类测试:
#[test] fn compact_config() { let config = Compaction::new(1000) .token_threshold(2000) .turn_threshold(5) .message_threshold(20); assert_eq!(config.max_tokens, 1000); assert_eq!(config.token_threshold, Some(2000)); assert_eq!(config.turn_threshold, Some(5)); assert_eq!(config.message_threshold, Some(20)); }compact_config:验证 setter 链式构造正确写入各字段;test_should_compact:构造超过 token 阈值的上下文断言触发,构造极高阈值断言不触发;compact:验证Agent.merge对compact字段的三态合并语义(Base 无值取 Other、双方有值被覆盖、Other 无值保留 Base)。
7.2 仓库中的实际测试
方案中的测试思路在仓库中演化为两套更系统的用例:
Compact触发判定测试(compact_config.rs)使用MessagePattern工具用字符串模式快速构造上下文('u'=用户消息、'a'=助手消息、's'=系统消息,例如ctx("uau")),逐一验证:
- token 维度:超过/等于阈值触发、低于阈值与未配置不触发;
- 轮数维度:
ctx("uauau")对turn_threshold=2触发,且只统计用户消息(ctx("uasa")不算轮数); - 消息维度:超过/等于阈值触发、低于阈值不触发;
on_turn_end:最后一条为用户时触发,助手/系统消息结尾不触发;- 组合场景:多阈值“任一满足即触发”与“全部未达不触发”、空上下文不触发。
compaction_threshold安全阈值回归测试(agent.rs)记录了 3 个已修复缺陷:
- BUG 1:
token_threshold为None时旧逻辑提前返回,导致压缩永不触发、最终出现context_length_exceeded;修复后无配置也会默认设为上下文窗口的 70%(如 128K 窗口 → 89_600); - BUG 2:codex-spark(128K 窗口)默认阈值 100K 时余量仅 28K,上下文涨到约 110K 时未触发压缩,而 API 请求叠加工具输出后超过 128K 上限;修复后阈值被压到 70% 安全线;
- BUG 3:模型元数据缺失时旧逻辑不设默认阈值导致上下文无界增长;修复后即使
context_length未知也会设置合理默认阈值。
CompactionResult指标测试(crates/forge_domain/src/compact/result.rs)验证压缩后 token/消息的缩减百分比计算,以及原始或压缩后数量为 0 的边界情况(返回0.0)。
CompactionResult结构本身记录了压缩前后original_tokens、compacted_tokens、original_messages、compacted_messages四项指标,并可由token_reduction_percentage()得到压缩率,可用于观测与埋点。
八、配置示例:forge.yaml 中的 compact 用法
方案文档给出的最小示例(可直接写入forge.yaml):
# Example in forge.yaml agents: myAgent: id: myAgent model: gpt-4-turbo system_prompt: "You are a helpful assistant." compact: max_tokens: 4000 token_threshold: 6000 turn_threshold: 10 message_threshold: 30结合仓库实现的完整字段,一个更贴近生产实践的配置可以是:
agents: myAgent: id: myAgent model: gpt-4-turbo system_prompt: "You are a helpful assistant." compact: model: gpt-4o-mini # 用更便宜的模型生成摘要 max_tokens: 4000 # 压缩后保留约 4000 tokens token_threshold: 6000 # 上下文超过 6000 tokens 时触发 token_threshold_percentage: 0.7 # 同时按窗口 70% 兜底,取较小值 turn_threshold: 10 # 超过 10 个用户轮次时触发 message_threshold: 30 # 超过 30 条消息时触发 retention_window: 5 # 保留最近 5 条消息不压缩 eviction_window: 0.2 # 最多摘要 20% 的上下文 on_turn_end: true # 用户发完消息即压缩配置要点归纳:
- 触发条件为或关系,任一阈值达到即触发,未配置的维度不参与判定;
token_threshold与token_threshold_percentage同时配置时取较小者,且最终会被compaction_threshold按模型上下文窗口二次收敛;eviction_window与retention_window同时生效时取更保守的(可压缩消息更少者);model不配置时回退到 Agent 根级模型;on_turn_end适合交互型场景,turn_threshold适合长任务场景。
九、验证标准与实施步骤
方案文档给出了 6 条验证标准,可作为任何新压缩实现的自检清单:
Compaction结构体正确定义,包含全部必需字段与方法;Agent结构体新增compact字段并遵循既有模式;- Orchestrator 在配置条件满足时自动执行上下文压缩;
- 基于 transform 的旧方案被正确弃用并给出警告;
- 全部测试通过,证明压缩逻辑符合预期;
- 文档与示例清楚说明新特性的用法。
对应实施步骤为:
- 在
agent.rs中添加Compaction/Compact结构体; - 为
Agent添加compact字段; - 在 Orchestrator/Hook 中实现压缩方法;
- 改造
init_agent_with_event(或等价触发点)加入压缩检查; - 增强 token 计数机制;
- 弃用
Transform::Assistant变体并输出警告; - 为新功能补充测试;
- 更新文档与示例。
十、从方案到实现的演进小结
对照方案文档与当前仓库代码,可以梳理出这条演进脉络:
- 配置结构:原型
Compaction(4 个字段)→ 落地Compact(9 个字段),新增eviction_window/retention_window控制“压缩多少”、token_threshold_percentage做窗口比例兜底、model支持低成本摘要模型、on_turn_end支持回合结束触发; - 触发判定:由调用方传入
turn_count/message_count→ 直接从Context内部统计,签名收敛为should_compact(&Context, token_count); - 执行链路:Orchestrator 内联
compact_context→ 独立的CompactionHandler(事件钩子)+Compactor(服务)分层,CompactionStrategy统一百分比/固定保留/取最值策略; - 摘要质量:占位符
"Summary of previous conversation"→SummaryTransformer清洗管线 +forge-partial-summary-frame.md结构化模板,并保留推理链与用量统计; - 安全兜底:新增
compaction_threshold按模型上下文窗口收敛触发阈值,配套 3 个 BUG 回归测试防止上下文撑爆窗口。
对于需要在 forgecode 中自定义 Agent 压缩策略的开发者,核心落地位置就是 crates/forge_domain/src/compact/compact_config.rs(配置与判定)、crates/forge_app/src/hooks/compaction.rs(触发)、crates/forge_app/src/compact.rs(执行),而 crates/forge_domain/src/compact/strategy.rs 与 crates/forge_domain/src/compact/result.rs 则分别承载压缩策略与结果指标——理解了这四个文件的配合方式,就掌握了整个上下文压缩子系统的全貌。
- 人工智能
- AI Agent
- 代码智能体
- AI 应用
- CLI
- 开发工具
【免费下载链接】forgecode
AI enabled pair programmer for Claude, GPT, O Series, Grok, Deepseek, Gemini and 300+ models
相关推荐
oh-my-openagent 压缩上下文注入器(compaction-context-injector)源码解析:上下文窗口压缩后的 Agent 配置自动恢复
oh my openagent 压缩上下文注入器(compaction context injector)源码解析:上下文窗口压缩后的 Agent 配置自动恢复
人工智能AI Agent代码智能体多智能体MCP ClientsAgent 编排3小时定制一只属于自己的虚拟桌宠:VPet零基础完整教程
3小时定制一只属于自己的虚拟桌宠:VPet零基础完整教程 VPet 是一款基于 WPF 的开源虚拟桌宠模拟器。你不用写一行代码,只要会换图片、改几行配置文件,大
桌面应用游戏开发OpenCore Legacy Patcher 完整指南:让 2008–2015 款 Intel Mac 安装 macOS 11–15
OpenCore Legacy Patcher 完整指南:让 2008–2015 款 Intel Mac 安装 macOS 11–15 OpenCore Leg
操作系统固件驱动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考