系列第 14 篇 · 生产篇
上篇讲 Workspace 让 Agent "身份持久"。但有个隐含前提:上下文不能无限膨胀。
模型上下文窗口是有限的。一个跑几小时、累积几十轮工具调用的 Agent,迟早撞上 context_length_exceeded。大多数框架的做法是粗暴截断文本------结果 Agent "失忆",长任务崩掉。
2.0 提供了一套四层上下文工程策略,目标不是"塞下更多",而是"保住关键信息、丢掉噪声"。
一、四层策略总览
sql
flowchart TD
A[长会话] --> B{上下文太深?消息数/ token 堆积}
B -->|是| C[① 结构化压缩CompactionMiddleware]
C --> D{单条工具结果超大?}
D -->|是| E[② 大结果卸载ToolResultEvictionMiddleware]
E --> F{模型返回context_length_exceeded?}
F -->|是| G[③ 溢出兜底recoverFromOverflow 强制压缩重试]
G --> H[继续执行]
A --> I[④ 参数截断TruncateArgsConfig压缩前的轻量预处理]
I --> C
四者相互独立、可自由组合,且都默认关闭。下面逐个讲解。
二、1 结构化压缩(CompactionMiddleware)
触发条件:消息数或估计 token 数超阈值。一次 LLM 调用把对话前缀蒸馏成结构化摘要,保留最近 N 条原文,把 摘要 + 最近尾部 写回 AgentState。
java
HarnessAgent agent = HarnessAgent.builder()
.name("long-task-agent")
.model("dashscope:qwen-max")
.workspace(Path.of(".agentscope/workspace"))
.compaction(CompactionConfig.builder()
.triggerMessages(30) // 消息数到 30 触发
.keepMessages(10) // 保留最近 10 条原文
// .triggerTokens(8000) // 也可按 token 触发
// .keepTokens(2000)
// .model("dashscope:qwen-plus") // 压缩用独立小模型省成本,不填则用主模型
.build())
.build();
摘要默认按SESSION INTENT / SUMMARY / ARTIFACTS / NEXT STEPS四节组织------特别适合工程/编排类 Agent,保住"目标、状态、结论、下一步",丢掉闲聊与中间推理噪声。
与记忆协同:
flushBeforeCompact(默认 true)会在压缩前,把对话前缀里的事实抽取进MEMORY.md(由MemoryFlushMiddleware执行);offloadBeforeCompact(默认 true)会把原始消息写进未压缩的*.log.jsonl。这样即使前缀被摘要掉,信息仍在------Agent 之后还能memory_search找回。
三、2 大工具结果卸载(ToolResultEvictionMiddleware)
和摘要压缩独立。某条工具结果文本超过阈值(默认80K 字符 ≈ 20K tokens),框架把全文写进 Workspace 目录,上下文里只留首尾各约 2K 字符 + 一个read_file路径指针,Agent 想看全文就自己 read_file。
java
HarnessAgent agent = HarnessAgent.builder()
.name("log-analyzer")
.model("dashscope:qwen-max")
.workspace(Path.of(".agentscope/workspace"))
.toolResultEviction(ToolResultEvictionConfig.defaults()) // 一行开启
.build();
默认排除 read_file/write_file/edit_file/grep_files/glob_files/list_files/memory_*/session_search------它们要么自带分页、要么返回值很小。shell执行默认不排除,因为命令输出可能极大。
四、3 溢出兜底(Overflow Safety Net)
如果模型真的返回 context_length_exceeded / maximum context / token limit 等错误,HarnessAgent.recoverFromOverflow() 会强制走一次 triggerMessages=1 的极端压缩,自动重试一次。
javascript
// 前提:构造时就配了 .compaction(...)
// 溢出时框架自动 recoverFromOverflow() + 重试,无需你写代码
HarnessAgent.builder()
.compaction(CompactionConfig.builder().triggerMessages(30).build())
.build();
// 只要开了 compaction,溢出恢复自动生效;
// 没开 compaction,错误会原样抛出
这条兜底无需额外配置:只要 compaction 开了,溢出恢复就跟着开。但没开 compaction,错误就直接抛给你------所以长任务务必开
.compaction(...)。
五、4 参数截断(TruncateArgsConfig)
在 LLM 摘要之前,先做一次零成本的字符串截断,裁掉过大的工具调用参数(如 write_file/edit_file 的大段 body):
javascript
CompactionConfig.builder()
.triggerMessages(80)
.truncateArgs(CompactionConfig.TruncateArgsConfig.builder()
.maxArgLength(2000) // 参数超 2000 字符截断
.truncationText("... [truncated] ...")
.build())
.build();
很多负载中,光这一步就能大幅推迟摘要触发,且成本近乎为零。
六、压缩"碰不到"的东西
ConversationCompactor 只操作 AgentState.contextMutable() 的对话消息列表。以下活在 AgentState 的其他字段,摘要不触碰:
-
Plan Mode 状态:是否激活、当前计划文件路径(计划文件本身在
workspace/plans/,由 Plan Mode 自己管理,呼应6)。 -
子 Agent 后台任务:结果存在
workspace/agents//tasks/.json,通过system-reminder回推给父 Agent,不进对话流。 -
todo 任务清单(
tasksContext):独立字段,随 AgentState 持久化但不走压缩。
如此就能保证压缩不会"误删"你的计划、后台任务或待办------设计上实现了隔离。
七、新手最常踩的 6 个坑
-
长任务没开 compaction:上下文迟早溢出,且因为没开,溢出兜底不生效,错误直接抛。
-
以为压缩会丢计划/任务:不会,它们在不同字段,摘要碰不到。
-
keepMessages 设太小:保留的近期原文太少,压缩后 Agent 忘了"刚说了什么",对话连贯性下降。
-
大结果卸载后 Agent 不读全文:它只拿到首尾预览 +
read_file指针,需要 prompt 里引导它"必要时 read_file 看完整结果"。 -
用主模型做压缩烧钱:长会话频繁压缩,建议
.model(...)指定一个小模型专门干摘要。 -
关掉 flushBeforeCompact 又指望记忆:关了就不会把事实写进
MEMORY.md,压缩后信息可能真丢了。
小结
-
2.0 上下文工程四层:结构化压缩 / 大结果卸载 / 溢出兜底 / 参数截断,全部默认关闭、可组合。
-
压缩保留 SESSION INTENT / SUMMARY / ARTIFACTS / NEXT STEPS,与记忆(
MEMORY.md)协同不丢信息。 -
大结果卸载:超 80K 字符写文件、上下文留首尾预览。
-
溢出兜底的前提是开了 compaction------长任务必开。
-
计划、后台任务、todo 不进压缩路径,安全。
上下文稳了,但 Agent 越自主,越需要"边界":它能删文件吗?能改订单吗?模型挂了怎么办?跑起来怎么观测?下一篇内容 权限 / 容错 / 可观测性将讲解权限三态决策、FallbackModel 主备切换、OpenTelemetry + 28 种类型化事件流,把 Agent "管得住、稳得住、看得清"。
如果这篇文章对你有帮助,欢迎关注公众号「栈知见」,我会持续输出有深度的技术实战笔记