Agent Scope Java 2.x 系列【22】Harness:上下文压缩
文章目录
1. 概述
上下文压缩(Context Compression)是大模型 LLM 专用前置优化技术:在把文本送入模型推理前,在尽量不丢失当前任务关键信息的前提下,剔除冗余、浓缩语义,大幅减少输入 token 数量,用更少 token 承载同等有效信息。
解决三大底层痛点:
- 窗口超限:模型上下文长度有上限,长对话 / 长文档直接截断会丢失关键信息;
- 成本 & 延迟爆炸:
Transformer注意力复杂度随token长度平方上涨,token越多计费越贵、响应越慢、显存占用越高; - 长文本性能衰减:海量冗余信息稀释注意力,模型容易遗忘早期关键约束、混淆逻辑。
2. 四大压缩策略
HarnessAgent 内置全套压缩链路,默认全部关闭,四大压缩策略互相正交,支持任意组合启用。
| 策略名称 | 解决问题 | 触发时机 | 对应中间件/能力 |
|---|---|---|---|
| 对话摘要压缩 | 上下文消息条数/总Token过多(上下文过深) | 每次模型推理前 | CompactionMiddleware |
| 大工具结果卸载 | 单条工具返回内容体积巨大(上下文过宽) | 工具执行完成后 | ToolResultEvictionMiddleware |
| 预压缩参数截断 | 文件类工具入参内容冗余、后续无读取需求 | 摘要压缩前置轻量预处理 | CompactionConfig.TruncateArgsConfig |
| 上下文溢出兜底恢复 | 直接命中模型context_length_exceeded超限报错 |
call()抛出超限异常时 |
HarnessAgent.recoverFromOverflow |
2.1 对话摘要压缩
Agent 持续多轮对话后,消息条数、总 Token 不断上涨,会出现两个问题:
- 超过模型上下文窗口,直接抛
context_length_exceeded报错 - 大量冗余历史消息占用
Token,推理变慢、成本变高
对话摘要压缩专门处理对话太深、历史消息过多的场景,属于周期性主动压缩,提前控长,避免走到溢出报错兜底逻辑。达到消息/Token 阈值后,调用 LLM 将对话早期历史压缩为结构化摘要,保留尾部N条最新原始消息,将[摘要]+[最新消息]写回上下文。
核心工作流程:
- 判断是否达到压缩阈值,两种阈值二选一 / 同时配置满足其一即触发:
triggerMessages:消息条数阈值(例:30条消息就压缩)triggerTokens:预估Token总量阈值
- 分割消息:
- 尾部:保留最近
N条完整原始消息(keepMessages/keepTokens),不做任何修改,保证最新交互细节完整 - 前缀:前面所有更早的历史消息全部丢弃原文,交给
LLM做摘要
- 尾部:保留最近
- 调用
LLM生成结构化摘要,默认摘要Prompt固定四段式,适配工程、代码、编排类Agent:SESSION INTENT:本次会话整体目标SUMMARY:完整历史行为简述ARTIFACTS:产出文件、工具输出、关键变量等成果NEXT STEPS:之前规划的待执行动作- 支持单独指定摘要专用模型,不指定则复用
Agent主模型。
- 重写上下文:把「一段结构化摘要 + 尾部最新 N 条消息」合并,写回
AgentState可变上下文,旧的长历史消息被替换掉,整体Token大幅下降。
配置示例:
HarnessAgent.builder()
.compaction(CompactionConfig.builder()
.triggerMessages(30) // 累计30条消息触发压缩
.keepMessages(10) // 压缩后保留最新10条完整消息
.flushBeforeCompact(true)
.offloadBeforeCompact(true)
.model(miniLlm) // 轻量模型专门做摘要,省钱提速
.truncateArgs(TruncateArgsConfig.builder()
.maxArgLength(2000)
.build())
.build())
.build();
CompactionConfig 常用配置项:
| 参数 | 默认值 | 说明 |
|---|---|---|
| triggerMessages | 50 | 对话消息条数阈值触发,0=关闭条数触发 |
| triggerTokens | 80_000 | 上下文估算Token阈值触发,0=关闭Token触发 |
| keepMessages | 20 | 压缩后保留尾部原始消息条数 |
| keepTokens | 0 | 非0时按Token预算截取尾部消息,优先级覆盖keepMessages |
| flushBeforeCompact | true | 压缩前抽取对话事实写入长期记忆流水账 |
| offloadBeforeCompact | true | 压缩前完整原始对话存入永久日志,永不压缩 |
| summaryPrompt | DEFAULT_SUMMARY_PROMPT | 摘要模板,必须包含{messages}占位符 |
| model | null | 摘要专用独立模型,null则复用Agent主模型 |
2.2 大工具结果卸载
Agent 调用工具后,单条返回内容体量巨大(比如 shell 命令打印几万行日志、读取超大文件),单一条消息就占大量 Token,哪怕对话轮次很少,也会直接撑满上下文窗口。
对话摘要压缩是处理「多轮消息堆积」,而大工具结果卸载专门处理单条工具返回超大文本,二者相互独立、可以同时开启。
触发时机:工具执行完成、拿到工具返回结果后立刻执行,在消息送入 LLM 之前完成处理。
核心工作流程:
- 判断工具返回文本长度是否超过阈值(默认
80K字符,约等价20K tokens); - 超过阈值则执行卸载:
- 将完整原始文本持久化写入工作区独立文件;
- 上下文内不再存放全文,只保留首尾各约
2K字符作为预览; - 追加一行文件路径提示:完整内容可通过
read_file{路径} 读取; - 把精简后的预览片段替换掉原超大工具结果,存入
AgentState上下文。
配置示例:
ToolResultEvictionConfig.builder()
.maxCharLength(100000) // 自定义触发字符阈值
.offloadRoot("./tool_offload") // 自定义文件存放目录
.build()
默认规则:
- 触发阈值:超过
80K字符自动卸载; - 默认排除工具:
read_file(防止刚读取完整文件又被卸载),另有write_file/edit_file/grep_files/glob_files/list_files/memory_*/session_search,这类工具本身就是用来读写 / 检索文件,卸载会造成循环依赖,默认跳过卸载逻辑; Shell execute不排除,命令输出极易超大;- 自定义阈值、自定义文件存储根目录:使用
ToolResultEvictionConfig.builder()自定义构建配置。
2.3 预压缩参数截断
write_file、edit_file 这类文件修改工具入参巨大:
write_file(path, content):content是一整段完整代码、长文档、配置文本;edit_file(path, replace_content):传入大段替换代码。
这里的 content / replace_content 就是工具入参文本,经常一次性传入几千、上万字符代码。大量无效文本会塞进摘要 LLM,浪费 Token、拉高摘要成本、加快触发压缩的频率。
后续几乎不会回看这段原始入参:
Agent写完 / 改完文件后,完整内容存在工作区文件里,后续要看完整代码,直接调用read_file读取本地文件,不需要依赖上下文里存的原始入参;- 对话往后推进,关注点变成运行、调试、新增逻辑,极少需要回头看当初写入时那一大段原始代码;
- 摘要只需要留存关键行为:已向xx文件写入代码,不需要把几千行代码全部塞进摘要里。
预压缩参数截断 是对话摘要压缩的附属前置预处理逻辑,仅在执行 LLM 摘要之前运行,纯字符串裁剪,不调用 LLM,几乎无性能损耗。
核心工作流程:
- 遍历所有待压缩消息里的工具调用入参;
- 匹配目标工具(文件写入编辑类工具);
- 若入参文本长度超过
maxArgLength,直接截断; - 中间内容替换为占位符 … [
truncated] …,仅保留前后部分内容; - 截断后的短参数替代原始长参数,再送入摘要模型。
配置示例:
CompactionConfig.builder()
.triggerMessages(80)
.truncateArgs(CompactionConfig.TruncateArgsConfig.builder()
.maxArgLength(2000) // 单条参数最大保留字符长度
.truncationText("... [truncated] ...") // 截断占位文本
.build())
.build();
和大工具结果卸载的区分:
- 处理对象不同
- 参数截断:工具入参(传给工具的内容,如
write_file的文件内容) - 工具卸载:工具返回结果(工具执行输出、
shell日志、文件读取内容)
- 参数截断:工具入参(传给工具的内容,如
- 执行链路不同
- 参数截断:依附摘要压缩,只在要做摘要时才执行;
- 工具卸载:工具执行完立刻执行,独立中间件,和摘要无关。
- 实现方式
- 参数截断:内存字符串裁剪,不落地文件;
- 工具卸载:超长内容写入磁盘文件,上下文只留预览。
2.4 上下文溢出兜底
这是一套被动应急容错机制,区别于前面三种主动提前压缩策略(摘要、工具卸载、参数预截断),前面三者都是在达到 Token 上限前主动缩减上下文,而兜底机制是已经硬闯模型窗口、模型抛出超限报错后,自动抢救重试。
触发条件:
LLM调用直接返回报错:context_length_exceeded/maximum context/token limit一类上下文超限异常;Agent开启了.compaction()对话摘要压缩(没开摘要则兜底功能不生效,直接抛异常给上层)。
核心工作流程:
- 捕获到
Token超限异常,进入恢复逻辑recoverFromOverflow; - 执行极端强制压缩:把阈值拉到极致,等价于
triggerMessages=1- 只保留当前最新
1条消息完整原文; - 前面所有历史对话全部丢原文,一次性调用
LLM合并生成极简摘要;
- 只保留当前最新
- 用「极简摘要 + 最后 1 条消息」覆盖更新
AgentState上下文; - 自动重新发起一次 LLM 调用重试;
- 重试成功则正常继续流程;重试仍超限,才把异常抛出给业务层。
3. 压缩与长期记忆联动
对话摘要压缩会把早期大量历史消息删掉、替换成简短摘要,如果直接丢弃原始对话,会话里的关键事实、需求、结论会丢失,后续 Agent 无法回忆久远信息。
Memory 联动就是在压缩销毁历史消息前,先把关键信息落地到长期记忆,实现 “上下文瘦身,但知识不丢失”。
这套联动依附于对话摘要压缩(Compaction),由两个配置开关控制(默认均为 true):
flushBeforeCompactoffloadBeforeCompact
压缩、Memory、flush/offload 三组开关互不绑定:
- 可以开启压缩,关闭
flush:压缩后不抽取事实,久远信息无法通过记忆查询; - 可以开启压缩,关闭
offload:无完整会话日志,只能靠摘要 + 记忆回溯; Memory长期记忆可单独启用,不开启压缩也能正常抽取事实。
分层兜底,信息不丢失:
- 短期上下文:仅保留摘要 + 最新几条消息,控制
Token; - 中长期事实:存在
Memory记忆文件,供工具检索关键结论; - 完整原始会话:存在
.log.jsonl日志,可完整回溯全部交互。
压缩负责缩减上下文
Token避免超限,Memory联动负责在删除旧消息前,把关键事实、完整对话分别落地记忆文件与会话日志,实现 “上下文轻量化,历史信息可追溯”。
3.1 flushBeforeCompact
压缩前通过 MemoryFlushMiddleware 抽取对话事实写入长期记忆 MEMORY.md、memory/*.md ,旧消息被摘要丢弃后,信息可通过 memory_search/memory_get 检索,不会丢失。
执行时机:正式调用 LLM 生成摘要之前,先执行记忆落盘。
核心工作流程:
- 扫描即将被压缩丢弃的早期历史消息;
- 抽取会话里的核心事实:需求、配置、文件变更、业务结论、约束条件等;
- 增量写入持久化长期记忆文件:根目录
MEMORY.md、分条记忆memory/*.md; - 完成事实持久化后,才执行摘要压缩、删掉原始历史消息。
3.2 offloadBeforeCompact
压缩前完整原始对话写入永久 *.log.jsonl 日志文件,供会话检索工具读取。
执行时机:正式调用 LLM 生成摘要之前,和记忆抽取同步,摘要生成前执行。
永久会话日志文件:
workspace/agents/{agentId}/sessions/{sessionId}.log.jsonl
4. 压缩完全不处理的状态字段
上下文压缩(Compaction)仅作用于 AgentState.contextMutable() 对话消息列表,只会裁剪、摘要、替换聊天消息。
AgentState 中其他独立业务状态字段、外部持久化文件、子任务存储均完全隔离,压缩逻辑不会读取、修改、删除这些内容,开启压缩不会丢失计划、任务、权限等业务数据。
不受压缩影响的四类状态,它们自有存储/恢复逻辑:
| 独立模块 | 存储位置 | 作用 | 与压缩隔离说明 |
|---|---|---|---|
| Plan Mode 规划状态 | AgentState.getPlanModeContext();计划文件存 workspace/plans/ |
记录当前是否处于规划阶段、计划文件路径 | 对话压缩只处理聊天流,不修改规划标记与计划文件;Plan Mode 自有独立生命周期与恢复逻辑 |
| 子Agent异步后台任务 | 文件:workspace/agents/{父AgentId}/tasks/{sessionId}.json,由 TaskRepository 管理 |
保存子任务 task_id、运行状态、执行结果 | 子任务结果不走对话消息流,靠 system reminder 注入上下文;压缩不访问任务存储,不会清理/丢失后台任务 |
| todo_write 待办清单 | AgentState.getTasksContext() | 存储任务清单、代办事项 | 属于独立状态字段,跟随 AgentState 持久化,但不参与对话消息压缩流程 |
| 权限规则上下文 | AgentState.getPermissionContext() | 保存工具调用、文件操作权限配置 | 独立持久化字段,压缩通路完全透明,无任何修改逻辑 |
开启压缩不会丢失计划、未完成后台任务、权限配置。
5. 历史会话查询能力
历史会话检索工具专门用来读取落地磁盘、永不压缩的原始会话日志,让 Agent 能自主翻看完整历史对话,不受上下文压缩策略影响。
日志文件路径:
workspace/agents/{agentId}/sessions/{sessionId}.log.jsonl
会话能力默认开启,只要启动 Agent ,三个检索工具会自动注入工具列表,无需手动配置:
| 工具名称 | 核心能力 | 入参说明 | 使用场景 | 数据来源 |
|---|---|---|---|---|
| session_list | 查询指定Agent下全部历史会话清单 | agentId:目标Agent唯一标识 | 1. 不清楚有哪些历史会话 2. 需要筛选目标会话ID |
磁盘会话日志目录索引 |
| session_history | 获取单条会话完整原始对话消息 | agentId:Agent标识 sessionId:指定会话ID lastN:读取最近N条消息 |
1. 已知会话ID,复盘某次完整对话 2. 查看已被压缩丢失的原始上下文 |
{sessionId}.log.jsonl 完整原始日志 |
| session_search | 关键词全局检索所有会话历史片段 | query:检索关键词/语句 agentId:限定检索范围Agent |
1. 遗忘会话ID,仅记得关键内容 2. 跨会话查找历史需求、操作记录 |
遍历该Agent全部 .log.jsonl 日志文件 |
开启压缩不会丢失历史对话,仍可回溯完整原始对话
更多推荐

所有评论(0)