AgentScope Java Harness:2. 上下文压缩:让长期 Agent 永不“失忆“

目录:

1. 如何优雅地驾驭长期运行的 AI Agent

2. 上下文压缩:让长期 Agent 永不"失忆"

3. 工作区(Workspace)文件即真理,目录即架构

4. 双层记忆系统 让 Agent 拥有真正的"长期大脑"

5. 文件系统一套代码,三种部署,零改动切换

6. 沙箱(Sandbox)让 Agent 在安全笼子里自由奔跑

7. 子 Agent 编排 文件驱动的多智能体协作架构

8. Skill技能让 Agent 从"会说话"进化为"会做事"

9. Plan Mode 让 Agent 先想清楚再动手

10. Channel Agent 通信的"神经系统"设计

LLM 的 token 预算是有限的。一段对话越跑越长,要么主动压缩、要么撞到模型的硬上限报错。AgentScope Harness 给出了四种正交策略,让你按需组合、从容应对。

一、问题背景:为什么需要上下文压缩?

在构建长期运行的 AI Agent 时,上下文窗口是最根本的物理约束。一个持续对话的 Agent 会面临两类"膨胀":

膨胀类型 表现 典型场景
太"深" 消息条数 / token 累计过多 多轮对话、反复调用工具
太"宽" 单条工具结果体量巨大 grep 返回大量匹配、execute 输出冗长日志

如果不做任何处理,Agent 迟早会撞上 context_length_exceeded 错误,轻则中断任务,重则丢失关键上下文。

AgentScope Java 2.0 的 Harness 模块内置了一整套压缩链路,默认关闭、按需开启,通过 .compaction(...) 或 .toolResultEviction(...) 一行配置即可激活。

二、架构定位:压缩在整体链路中的位置

理解压缩机制,首先要明确它在 Harness 架构中的位置:

文本 复制代码
┌──────────────────────────────────────────────────────┐
│                   HarnessAgent.call()                │
│                                                      │
│  ┌────────────┐    ┌──────────────────────────────┐  │
│  │ 压缩链路    │───▶│ AgentState.contextMutable()  │  │
│  │ (内存操作)  │    │ (对话消息列表)                 │  │
│  └────────────┘    └──────────────┬───────────────┘  │
│                                   │                  │
│                                   ▼                  │
│                    ┌──────────────────────────────┐  │
│                    │ AgentStateStore (持久化)      │  │
│                    │ call() 结束时整体写入          │  │
│                    └──────────────────────────────┘  │
└──────────────────────────────────────────────────────┘

关键设计 :压缩在内存中更新 AgentState.contextMutable(),状态存储在 call 结束时把更新后的 AgentState 整体持久化。两条路径独立但按顺序执行------状态存储拿到的永远是压缩后的版本。

这意味着压缩是"有损但可控"的:你选择压缩,就接受前缀消息被摘要替代;但框架保证压缩前的关键信息不会凭空消失(后文详述 Memory 联动)。

三、四大正交策略详解

Harness 提供四种策略,彼此正交、可任意组合、默认全部不开。这种"按需装配"的设计哲学贯穿整个 Harness 架构。

策略一:对话摘要压缩(CompactionMiddleware)

**解决的问题:**上下文太"深"------消息条数或 token 累计过多。

**触发时机:**每次模型推理前。

工作原理:

文本 复制代码
原始对话: [msg1, msg2, ..., msg25, msg26, msg27, msg28, msg29, msg30]
                                                    ↑ 触发阈值(30条)

压缩后:   [summary] + [msg21, msg22, ..., msg30]
              ↑                ↑
         LLM生成的摘要     保留最近10条原文

配置示例:

java 复制代码
HarnessAgent.builder()
    .compaction(CompactionConfig.builder()
        .triggerMessages(30)     // 30 条消息触发压缩
        .keepMessages(10)        // 压缩后保留最近 10 条原文
        .build())
    .build();

**摘要结构:**默认摘要 prompt 会将内容组织为四个小节:

  • SESSION INTENT:本次会话的意图和目标
  • SUMMARY:已完成的工作摘要
  • ARTIFACTS:产出的关键产物(文件、配置等)
  • NEXT STEPS:下一步计划

这种结构化摘要特别适合工程/编排类 Agent,确保压缩后 Agent 仍能"接住"上下文继续工作。

进阶配置:

字段 说明
triggerTokens 按估算 token 数触发(与 triggerMessages 二选一或并用)
keepTokens 按 token 数保留尾部消息
flushBeforeCompact 摘要前是否先抽取事实到长期记忆(默认 true)
offloadBeforeCompact 摘要前是否将原始消息写入日志文件(默认 true)
model 为压缩摘要指定独立模型(不设则用 Agent 主模型)

💡 实践建议:如果主模型是昂贵的旗舰模型(如 GPT-4o),可以通过 .model(...) 指定一个轻量模型专门做摘要,大幅降低压缩成本。

策略二:大工具结果卸载(ToolResultEvictionMiddleware)

**解决的问题:**上下文太"宽"------单条工具结果体量过大。

**触发时机:**工具执行后。

工作原理:

文本 复制代码
工具返回 120K 字符的结果
        │
        ▼ 超过阈值(默认 80K 字符 ≈ 20K tokens)
        │
        ├──▶ 全文写入工作区文件: /workspace/tool-results/xxx.txt
        │
        └──▶ 上下文中替换为:
             [前 2K 字符] + "... [内容已卸载到文件] ..." + [后 2K 字符]
             + read_file 路径提示

配置示例:

java 复制代码
HarnessAgent.builder()
    .toolResultEviction(ToolResultEvictionConfig.defaults())
    .build();

**智能排除列表:**以下工具默认不触发卸载------它们要么自带分页、要么返回值天然很小:

  • read_file / write_file / edit_file
  • grep_files / glob_files / list_files
  • memory_* / session_search

而 Shell execute 默认不排除,因为命令输出可能非常大(如 find / 或 cat 大文件)。

💡 设计亮点:Agent 想看被卸载的全文时,只需自己调用 read_file 读取对应路径即可。这是一种"延迟加载"思想------不预先占满上下文,需要时再取。

策略三:上下文溢出兜底

**解决的问题:**真的撞到了模型的 context_length_exceeded 硬上限。

**触发时机:**call() 抛错时。

工作原理:

文本 复制代码
模型返回 context_length_exceeded / maximum context / token limit 错误
        │
        ▼
HarnessAgent.recoverFromOverflow()
        │
        ├──▶ 强制执行 triggerMessages=1 的极端压缩
        │    (只保留最近 1 条消息 + 摘要)
        │
        └──▶ 自动重试一次

**关键前提:**必须已配置 .compaction(...),否则错误原样抛回上层。

java 复制代码
// 只要 compaction 开了,溢出恢复就自动开------无需额外配置
HarnessAgent.builder()
    .compaction(CompactionConfig.builder()
        .triggerMessages(30)
        .keepMessages(10)
        .build())
    .build();

这是一条"最后防线"------正常情况下策略一和策略二应该已经足够,但如果真的溢出了,框架不会静默失败,而是尝试自救。

策略四:预压缩参数截断(可选)

**解决的问题:**工具调用参数(如 write_file 的文件内容)体量大但后期没人再看。

**触发时机:**摘要之前的轻量预处理(不走 LLM)。

配置示例:

java 复制代码
CompactionConfig.builder()
    .triggerMessages(80)
    .truncateArgs(CompactionConfig.TruncateArgsConfig.builder()
        .maxArgLength(2000)           // 参数超过 2000 字符就截断
        .truncationText("... [truncated] ...")
        .build())
    .build();

**为什么有效:**write_file、edit_file 这类工具的入参往往包含完整的文件内容(可能几万字符),但写入完成后,这些参数在后续对话中几乎没有参考价值。提前截断它们,可以显著推迟摘要触发的时机。

💡 性价比之王:这一步几乎零成本(纯字符串操作,不调 LLM),却能大幅降低触发摘要的频率。强烈建议与策略一搭配使用。

四、压缩与 Memory 的联动:信息不会凭空消失

这是 Harness 压缩设计中最精妙的部分。很多人担心:压缩把前缀消息摘要掉了,那些信息岂不是丢了?

答案是:不会丢,因为框架在压缩前做了两件"保底"操作。

4.1 flushBeforeCompact(默认 true)

摘要发生前,MemoryFlushMiddleware + MemoryFlushManager 会先把对话前缀中的有价值事实抽取到长期记忆中:

文本 复制代码
压缩前的对话前缀
        │
        ▼ MemoryFlushMiddleware
        │
        ├──▶ 读取 <workspace>/MEMORY.md(已有记忆)
        ├──▶ 读取 memory/*.md(日志层)
        └──▶ 增量写入新事实

之后即使前缀消息被摘要替代,Agent 仍可通过 memory_search / memory_get 工具回头查阅这些事实。

4.2 offloadBeforeCompact(默认 true)

摘要前把原始消息整段写到永不压缩的 *.log.jsonl 文件:

文本 复制代码
原始消息 ──▶ <workspace>/agents/<agentId>/sessions/<sessionId>.log.jsonl

这份日志供 session_search 工具检索,是真正的"全量备份"。

联动流程图

文本 复制代码
触发压缩
    │
    ├── ① flushBeforeCompact: 事实 → MEMORY.md / memory/*.md
    │
    ├── ② offloadBeforeCompact: 原始消息 → *.log.jsonl
    │
    ├── ③ truncateArgs: 截断大参数(可选)
    │
    └── ④ LLM 摘要: 前缀 → [summary]    
结果: [summary] + [recent tail] 写回 AgentState

五、压缩不会触碰的内容

ConversationCompactor 只处理 AgentState.contextMutable() 里的对话消息列表。以下组件完全不受压缩影响:

组件 存储位置 为什么不受影响
Plan Mode 状态 AgentState.getPlanModeContext() + 工作区 plans/ 独立字段,生命周期由 Plan Mode 自己管理
子 Agent 后台任务 /agents//tasks/.json 由 TaskRepository 维护,通过 system reminder 注入,不进入对话流
todo_write 任务清单 AgentState.getTasksContext() 独立字段,跟着 AgentState 持久化但不参与压缩
权限规则 AgentState.getPermissionContext() 独立字段,自带持久化

✅ 结论:你可以放心开启 .compaction(...),不用担心丢 Plan、丢未完成的后台 Task、丢权限配置。压缩通路对它们是透明的。

六、用 Agent 自己查历史会话

启用会话能力时(默认开),三个查询工具会自动注册,Agent 可以自主调用:

工具 功能 示例
session_list 列出某个 Agent 的历史会话 session_list agentId="coder"
session_history 查看某次会话最近 N 条消息 session_history agentId="coder" sessionId="abc" lastN=20
session_search 在历史会话中关键词搜索 session_search query="数据库迁移" agentId="coder"

关键点:这些工具读的是永不压缩的对话日志 (*.log.jsonl),所以即使当前上下文已经被压缩成摘要,Agent 也能查到任意历史消息的原文。

这形成了一个优雅的闭环:

文本 复制代码
压缩丢掉细节 → Agent 需要回忆 → 调用 session_search → 找回原始信息

七、最佳实践与配置建议

场景一:轻量对话 Agent(对话轮次少)

java 复制代码
// 不需要任何压缩配置,默认即可
HarnessAgent.builder()
    .workspace("/path/to/ws")
    .build();

场景二:工程编码 Agent(长对话 + 大文件操作)

java 复制代码
HarnessAgent.builder()
    .compaction(CompactionConfig.builder()
        .triggerMessages(50)
        .keepMessages(10)
        .truncateArgs(CompactionConfig.TruncateArgsConfig.builder()
            .maxArgLength(2000)
            .build())
        .build())
    .toolResultEviction(ToolResultEvictionConfig.defaults())
    .workspace("/path/to/ws")
    .build();

场景三:成本敏感场景(用便宜模型做摘要)

java 复制代码
HarnessAgent.builder()
    .compaction(CompactionConfig.builder()
        .triggerMessages(40)
        .keepMessages(8)
        .model(cheapModel)  // 用轻量模型做摘要,主模型只做推理
        .build())
    .build();

配置决策树

文本 复制代码
你的 Agent 会跑多少轮?
├── < 20 轮 → 不需要压缩
├── 20~100 轮 → 开启 compaction + truncateArgs
└── > 100 轮或不确定 → compaction + truncateArgs + toolResultEviction
                         (溢出兜底自动生效)

八、设计哲学总结

回顾整套压缩机制,可以提炼出几个核心设计原则:

  1. 正交组合:四种策略解决不同维度的问题,互不干扰、自由搭配;
  2. 默认关闭:不预设用户需要压缩,避免不必要的 LLM 调用开销;
  3. 有损但可恢复:压缩是有损的,但通过 Memory 联动和日志备份,确保信息可追溯;
  4. 零成本优先:参数截断这种"免费"操作放在 LLM 摘要之前,能省则省;
  5. 透明隔离:压缩只碰对话消息,Plan、Task、权限等状态完全不受影响。

九、结语

上下文压缩不是一个"锦上添花"的功能,而是长期 Agent 走向生产的必备基础设施。AgentScope Harness 的设计给出了一个教科书级的答案:

  • 分层策略应对不同维度的膨胀;
  • Memory 联动保证信息不丢失;
  • 正交设计保持系统的可组合性;
  • 零成本预处理降低整体开销。
相关推荐
Aurorar0rua1 小时前
CS50 x 2024 Notes Memory - 03
c语言·开发语言·学习方法
梦想的旅途21 小时前
企业微信API实战:Python自动化消息推送
java·前端·python·自动化·企业微信
YOU OU1 小时前
RabbitMQ运维
java·rabbitmq·java-rabbitmq
mldong1 小时前
流程定义设计:一份 LogicFlow JSON 全解
java·架构
爱敲代码的憨仔1 小时前
BM25全文检索
开发语言·python·全文检索
油丶酸萝卜别吃7 小时前
utils.js 说明文档
开发语言·javascript·ecmascript
rannn_11110 小时前
【力扣hot100】链表专题|160、206、234、141、142
java·算法·leetcode·链表·面试·开发
leisoo809710 小时前
100GBA股股票数据怎么存ClickHouseRedisMySQLJSON完整对比
大数据·linux·服务器·开发语言·python
不会代码的小猴10 小时前
21. 泛型编程上
开发语言·c++·笔记·算法