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 不断上涨,会出现两个问题:

  1. 超过模型上下文窗口,直接抛 context_length_exceeded 报错
  2. 大量冗余历史消息占用 Token,推理变慢、成本变高

对话摘要压缩专门处理对话太深、历史消息过多的场景,属于周期性主动压缩,提前控长,避免走到溢出报错兜底逻辑。达到消息/Token 阈值后,调用 LLM 将对话早期历史压缩为结构化摘要,保留尾部N条最新原始消息,将[摘要]+[最新消息]写回上下文。

核心工作流程

  1. 判断是否达到压缩阈值,两种阈值二选一 / 同时配置满足其一即触发:
    • triggerMessages:消息条数阈值(例:30 条消息就压缩)
    • triggerTokens:预估 Token 总量阈值
  2. 分割消息:
    • 尾部:保留最近 N 条完整原始消息(keepMessages/keepTokens),不做任何修改,保证最新交互细节完整
    • 前缀:前面所有更早的历史消息全部丢弃原文,交给 LLM 做摘要
  3. 调用 LLM 生成结构化摘要,默认摘要 Prompt 固定四段式,适配工程、代码、编排类 Agent
    • SESSION INTENT:本次会话整体目标
    • SUMMARY:完整历史行为简述
    • ARTIFACTS:产出文件、工具输出、关键变量等成果
    • NEXT STEPS:之前规划的待执行动作
    • 支持单独指定摘要专用模型,不指定则复用 Agent 主模型。
  4. 重写上下文:把「一段结构化摘要 + 尾部最新 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 之前完成处理。

核心工作流程

  1. 判断工具返回文本长度是否超过阈值(默认 80K 字符,约等价 20K tokens);
  2. 超过阈值则执行卸载:
  3. 将完整原始文本持久化写入工作区独立文件;
  4. 上下文内不再存放全文,只保留首尾各约 2K 字符作为预览;
  5. 追加一行文件路径提示:完整内容可通过 read_file {路径} 读取;
  6. 把精简后的预览片段替换掉原超大工具结果,存入 AgentState 上下文。

配置示例:

ToolResultEvictionConfig.builder()
    .maxCharLength(100000) // 自定义触发字符阈值
    .offloadRoot("./tool_offload") // 自定义文件存放目录
    .build()

默认规则

  1. 触发阈值:超过 80K 字符自动卸载;
  2. 默认排除工具:read_file(防止刚读取完整文件又被卸载),另有write_file/edit_file/grep_files/glob_files/list_files/memory_*/session_search,这类工具本身就是用来读写 / 检索文件,卸载会造成循环依赖,默认跳过卸载逻辑;
  3. Shell execute 不排除,命令输出极易超大;
  4. 自定义阈值、自定义文件存储根目录:使用ToolResultEvictionConfig.builder()自定义构建配置。

2.3 预压缩参数截断

write_fileedit_file 这类文件修改工具入参巨大:

  • write_file(path, content)content 是一整段完整代码、长文档、配置文本;
  • edit_file(path, replace_content):传入大段替换代码。

这里的 content / replace_content 就是工具入参文本,经常一次性传入几千、上万字符代码。大量无效文本会塞进摘要 LLM,浪费 Token、拉高摘要成本、加快触发压缩的频率。

后续几乎不会回看这段原始入参

  • Agent 写完 / 改完文件后,完整内容存在工作区文件里,后续要看完整代码,直接调用 read_file 读取本地文件,不需要依赖上下文里存的原始入参;
  • 对话往后推进,关注点变成运行、调试、新增逻辑,极少需要回头看当初写入时那一大段原始代码;
  • 摘要只需要留存关键行为:已向xx文件写入代码,不需要把几千行代码全部塞进摘要里。

预压缩参数截断对话摘要压缩的附属前置预处理逻辑,仅在执行 LLM 摘要之前运行,纯字符串裁剪,不调用 LLM,几乎无性能损耗。

核心工作流程

  1. 遍历所有待压缩消息里的工具调用入参;
  2. 匹配目标工具(文件写入编辑类工具);
  3. 若入参文本长度超过 maxArgLength,直接截断;
  4. 中间内容替换为占位符 … [truncated] …,仅保留前后部分内容;
  5. 截断后的短参数替代原始长参数,再送入摘要模型。

配置示例:

CompactionConfig.builder()
    .triggerMessages(80)
    .truncateArgs(CompactionConfig.TruncateArgsConfig.builder()
        .maxArgLength(2000)                    // 单条参数最大保留字符长度
        .truncationText("... [truncated] ...") // 截断占位文本
        .build())
    .build();

和大工具结果卸载的区分

  1. 处理对象不同
    • 参数截断:工具入参(传给工具的内容,如 write_file 的文件内容)
    • 工具卸载:工具返回结果(工具执行输出、shell 日志、文件读取内容)
  2. 执行链路不同
    • 参数截断:依附摘要压缩,只在要做摘要时才执行;
    • 工具卸载:工具执行完立刻执行,独立中间件,和摘要无关。
  3. 实现方式
    • 参数截断:内存字符串裁剪,不落地文件;
    • 工具卸载:超长内容写入磁盘文件,上下文只留预览。

2.4 上下文溢出兜底

这是一套被动应急容错机制,区别于前面三种主动提前压缩策略(摘要、工具卸载、参数预截断),前面三者都是在达到 Token 上限前主动缩减上下文,而兜底机制是已经硬闯模型窗口、模型抛出超限报错后,自动抢救重试

触发条件

  • LLM 调用直接返回报错:context_length_exceeded /maximum context /token limit 一类上下文超限异常;
  • Agent 开启了 .compaction() 对话摘要压缩(没开摘要则兜底功能不生效,直接抛异常给上层)。

核心工作流程

  1. 捕获到 Token 超限异常,进入恢复逻辑 recoverFromOverflow
  2. 执行极端强制压缩:把阈值拉到极致,等价于 triggerMessages=1
    • 只保留当前最新 1 条消息完整原文;
    • 前面所有历史对话全部丢原文,一次性调用 LLM 合并生成极简摘要;
  3. 用「极简摘要 + 最后 1 条消息」覆盖更新 AgentState 上下文;
  4. 自动重新发起一次 LLM 调用重试;
  5. 重试成功则正常继续流程;重试仍超限,才把异常抛出给业务层。

3. 压缩与长期记忆联动

对话摘要压缩会把早期大量历史消息删掉、替换成简短摘要,如果直接丢弃原始对话,会话里的关键事实、需求、结论会丢失,后续 Agent 无法回忆久远信息。

Memory 联动就是在压缩销毁历史消息前,先把关键信息落地到长期记忆,实现 “上下文瘦身,但知识不丢失”。

这套联动依附于对话摘要压缩(Compaction),由两个配置开关控制(默认均为 true):

  • flushBeforeCompact
  • offloadBeforeCompact

压缩、Memoryflush/offload 三组开关互不绑定:

  • 可以开启压缩,关闭 flush:压缩后不抽取事实,久远信息无法通过记忆查询;
  • 可以开启压缩,关闭 offload:无完整会话日志,只能靠摘要 + 记忆回溯;
  • Memory 长期记忆可单独启用,不开启压缩也能正常抽取事实。

分层兜底,信息不丢失:

  • 短期上下文:仅保留摘要 + 最新几条消息,控制 Token
  • 中长期事实:存在 Memory 记忆文件,供工具检索关键结论;
  • 完整原始会话:存在 .log.jsonl 日志,可完整回溯全部交互。

压缩负责缩减上下文 Token 避免超限,Memory 联动负责在删除旧消息前,把关键事实、完整对话分别落地记忆文件与会话日志,实现 “上下文轻量化,历史信息可追溯”。

3.1 flushBeforeCompact

压缩前通过 MemoryFlushMiddleware 抽取对话事实写入长期记忆 MEMORY.mdmemory/*.md ,旧消息被摘要丢弃后,信息可通过 memory_search/memory_get 检索,不会丢失。

执行时机:正式调用 LLM 生成摘要之前,先执行记忆落盘。

核心工作流程

  1. 扫描即将被压缩丢弃的早期历史消息;
  2. 抽取会话里的核心事实:需求、配置、文件变更、业务结论、约束条件等;
  3. 增量写入持久化长期记忆文件:根目录 MEMORY.md、分条记忆 memory/*.md
  4. 完成事实持久化后,才执行摘要压缩、删掉原始历史消息。

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 日志文件

开启压缩不会丢失历史对话,仍可回溯完整原始对话

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐