第 8 章 四级压缩法干掉上下文膨胀 · Agent架构实操八 开始测验

如何用「四级压缩法」干掉 Agent 上下文膨胀?

加大窗口只能推迟问题。真正要做的是管理信息生命周期:大结果放哪、旧内容怎样降级、哪些消息绝不能拆开、压缩后如何追溯原始记录。核心原则:先便宜,后昂贵
⏱ 约 15 分钟 🗜 四级管线 🔗 消息原子组 +ARTIFACTS/COMPACTION

🎯 学习目标

学完能答
  • 四层压缩分别是什么?为什么按成本从低到高执行?
  • canonical history / request history / transcript / artifact 四者区别?
  • 什么是消息原子组?为什么不能只看相邻两条消息?
  • 大工具结果落盘的阈值是多少?为什么按 UTF-8 bytes 不按字符串 length?
  • 50,000 bytes 摘要阈值是什么?不是什么?
  • finish_reason==="length" 等于 prompt-too-long 吗?P08 Loop 自动恢复了吗?

🧠 核心概念

点击展开。
1 · 核心原则:先便宜,后昂贵
  1. 大工具结果落盘:只把路径+有界预览留在历史。
  2. snip:按完整消息组裁掉中段。
  3. micro:较早工具组结果替换为占位符。
  4. 模型摘要:前三层后仍超阈值才调用模型。

前三层不增加模型请求,第四层才发起一次不带工具的摘要请求。压缩在两次模型调用之间预处理 request history,不改 System Prompt 静态前缀。这个时机是刻意的:未压缩部分与上一轮请求共享前缀,能命中供应商 KV Cache;中段一旦被裁,后缀缓存全部失效,所以能用便宜层解决就不动结构。

2 · 三种历史 + 一种产物
对象含义被请求压缩改写?
canonical historyAgentRunner.#history 会话事实;RunResult.history 返回不可变快照
request history每次模型调用前从 canonical 复制出的请求快照是(只影响本次及后续可复用快照)
transcript进入模型摘要前保存的 canonical history JSONL 快照否;文件独占发布
tool-result artifact大工具结果的完整 UTF-8 内容否;canonical 中只存路径和预览
关键边界 大工具结果在写入 canonical history 之前处理,所以 canonical 里是 <persisted-tool-result> 引用而非大正文。snip/micro/摘要都发生在请求快照上,不删除 canonical 中已存在消息。
3 · 消息原子组:不能只看相邻两条

一条 assistant 消息可含多个 tool_calls,合法历史不是「一条调用配一条结果」,而是不可拆分原子组:

assistant(tool_calls=[call_a, call_b])
tool(tool_call_id=call_a)
tool(tool_call_id=call_b)

validatedGroups() 先 validateToolPairing() 再划成两类组:普通消息单独一组;带工具调用的 assistant + 全部 tool results 共同构成工具交换组。snip 的保留边界、micro 的替换单位都用组,不会留孤儿 tool 或缺结果的 assistant。

4 · 第一层:大结果按 UTF-8 bytes 落盘

阈值用真实 UTF-8 字节数 Buffer.from(content,"utf8").byteLength:

  • 单项结果 > 30,000 bytes 直接落盘(等于阈值仍保留);
  • 未落盘合计仍 > 200,000 bytes,按字节数从大到小继续落盘;
  • 完整内容写 .agent_tutorial/artifacts/tool-result-<id>.txt;
  • 消息保留相对路径、original_bytes、头部最多 2,000 bytes + 尾部最多 2,000 bytes 预览。
注意 2,000 是 bytes 不是字符。UTF-8 中文占多字节,边界切在多字节字符中间时预览丢弃不完整字符,实际显示字符数可能更少。
5 · 第二层 snip + 第三层 micro

snip:超过 50 个消息组触发,保留前 3 组 + 一条 [Compacted: N message groups omitted] 标记 + 后 46 组。统计的是消息组不是 ChatMessage 条数。

micro:完整保留最近 3 个工具交换组,更早工具组所有结果替换为 [Earlier tool result compacted. Re-run if needed.]。不删除 assistant 调用,不改 tool_call_id,整个原子组合法。snip 先执行,micro 后执行,都基于不可变数组快照生成 request history,不原地改 canonical。

6 · 第四层:50,000 bytes 才摘要

前三层后 historyUtf8Bytes() 把请求快照转规范 OpenAI JSONL 算 UTF-8 bytes,只有 > 50,000 才进模型摘要。顺序:先存未压缩 canonical 快照为 transcript → summarizer 读已过便宜层的 request history → 摘要请求不携带工具 → 只接受 finishReason==="stop"、无 toolCalls、非空且字段精确的 JSON object → 成功后请求历史变成一条 system 消息(含 transcript 路径 + 五字段)。

50,000 不是 JS 字符串 length / 工具正文长度之和 / tokenizer token 数 / 供应商承诺窗口。它是确定性字节预算(含序列化的 role、tool_calls、tool_call_id、JSON 标点和换行)。

五字段:current_goalkey_findingsfiles_read_or_changedremaining_workuser_constraints

7 · prompt-too-long:原语有,Loop 信号没有
信号含义P08 行为
finishReason==="length"请求已接受但输出达上限,响应不完整抛 IncompleteModelReplyError,不做响应式压缩
输入 prompt-too-long输入上下文被供应商拒绝尚无统一 typed signal,无自动恢复接线
compactOnPromptTooLong(retryCount:0)调用者完成错误分类后可用的恢复原语单独可测;只允许一次尝试
不能 把 finish_reason==="length" 当 prompt-too-long;不能靠异常字符串含 prompt_too_long 冒充通用信号。错误分类应在模型适配器或恢复层完成。

▶️ 交互演示:四级压缩管线

拖动滑块增加历史大小,看四层如何依次生效。注意前三层免费,第四层才调模型;且只改 request history 不改 canonical。
历史大小: 20K
request history(送给模型):20K / 50K 阈值
canonical history 不变(20K),transcript 已存()

🔑 一句话总结

压缩不是省 token,而是整理知识:把「需要现场归纳的原始记录」换成「可直接检索的结构化结论」

按需加载负责少拿(第7章),Context Compact 负责及时放下(本章)。两者都只影响 request history,canonical history 始终是会话事实。

QA 测验