目录:
6. 沙箱(Sandbox)让 Agent 在安全笼子里自由奔跑
8. Skill技能让 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
(溢出兜底自动生效)
八、设计哲学总结
回顾整套压缩机制,可以提炼出几个核心设计原则:
- 正交组合:四种策略解决不同维度的问题,互不干扰、自由搭配;
- 默认关闭:不预设用户需要压缩,避免不必要的 LLM 调用开销;
- 有损但可恢复:压缩是有损的,但通过 Memory 联动和日志备份,确保信息可追溯;
- 零成本优先:参数截断这种"免费"操作放在 LLM 摘要之前,能省则省;
- 透明隔离:压缩只碰对话消息,Plan、Task、权限等状态完全不受影响。
九、结语
上下文压缩不是一个"锦上添花"的功能,而是长期 Agent 走向生产的必备基础设施。AgentScope Harness 的设计给出了一个教科书级的答案:
- 用分层策略应对不同维度的膨胀;
- 用Memory 联动保证信息不丢失;
- 用正交设计保持系统的可组合性;
- 用零成本预处理降低整体开销。