主题
8.8 上下文工程与压缩:双轨防御体系(0ms 滑动截断 + 工具裁剪 vs /compact 深度语义压缩)
“厚厚的会议记录本写满了琐碎的‘咳了一声、喝了口水’。抽屉满了,你可以直接把底层旧草稿扔碎纸机(截断:0秒搞定但旧事全忘);更聪明的秘书会撕掉流水账,提炼出一页清晰的《决议备忘录》(压缩:核心事实全保住)!”
🗜️ 为什么长对话必须做上下文工程?
在复杂的长程任务中(例如全栈重构、多模块联调、分布式故障排查),Agent 会频繁调用工具、读取长文件、执行终端命令。随之而来的就是 Token 爆炸危机:
- 费用昂贵:每轮对话都要把前面几万 Token 重新传一遍,账单直线上升;
- 大海捞针效应(Lost in the Middle):当上下文超过数万 Token 时,大模型的注意力会被海量无关日志分散,开始“失忆”或答非所问;
- 触顶报错:最终超出模型的最大 Context Window(如 8k/32k/128k)限制直接崩溃。
⚖️ 截断 (Truncation) vs 压缩 (Compact) 深度对比
很多初学者容易混淆**“截断”与“压缩”**。在工业级 Coding Agent(参考 learn-claude-code s08 Context Compact 与 deepagents)中,两者是相辅相成的“两道防线”:
| 评估维度 | ✂️ 截断策略 (Truncation Track) | 📦 压缩策略 (Compact Track) |
|---|---|---|
| 生活化比喻 | 🗄️ 清理抽屉:抽屉装不下了,把最底层的旧草稿直接扔进碎纸机 | 📝 秘书写备忘录:把 20 轮开会的争吵流水账提炼成一页《决议纪要》 |
| 执行延迟 | ⚡ 0 ms(纯 Python 列表切片与字符串截取,极速响应) | ⏳ 1 ~ 2 秒(需调用一次轻量大模型做事实提炼) |
| API 成本 | 💰 0 消耗(完全不消耗任何大模型 Token) | 💵 消耗极少量 Token(单次提炼仅需几百 Token) |
| 信息保留度 | ⚠️ 硬丢失(被丢弃的早期用户偏好、接口约定彻底消失) | 🌟 极高(LLM 语义提取技术栈、已修 Bug、修改文件与遗留 Todo) |
| 典型算法 | ① 工具超长日志首尾截断 ② 保持 System 锁定的 Token 预算滑动窗口 (FIFO) | ① 手写 /compact 结构化事实摘要提炼② 75% 警戒水位线自适应静默压缩 |
| 最佳适用场景 | 实时性要求极高的单步查询、超长终端日志、临时一次性会话 | 复杂长程多文件开发、大型 Bug 排查、跨轮次持续演进任务 |
🏔️ 四重上下文工程金字塔
▲
/ \ L4: 深度语义摘要 (/compact) —— 提炼结构化事实,节约 70%~90%
/---\ L3: 滑动窗口截断 (Sliding Window) —— 锁定 System,按预算丢弃旧轮次
/-----\ L2: 微压缩 (Micro Compact) —— 清理瞬态控制字符与冗余 Tool Call 状态
/-------\ L1: 工具预算首尾折叠 (Tool Result Budget) —— 超 40 行只留首尾各 20 行
-----------1. L1:工具输出首尾折叠(Tool Result Budget)
Agent 在执行 pytest、git log 或 npm build 时,终端经常吐出上千行日志。直接回填会瞬间撑爆上下文。
- 生活化比喻:看 50 页体检单,医生只看开头的基本体征和结尾的异常指标,中间几十页全部正常的化验流水直接折叠;
- 实现原理:超过设定行数(如 40 行)时,保留 前 20 行(启动参数与报错起点) + 后 20 行(最终退出码与 Traceback 根因),中间插入
... ✂️ [已自动省略 N 行] ...。
2. L2 & L3:滑动窗口 Token 预算截断(Sliding Window FIFO)
当必须在 0ms 内控制上下文大小时,采用滑动窗口丢弃最旧的消息。
- 需要避免的错误:不能简单地从头删除消息。第 0 条往往包含
System Prompt和工具规范,删掉后可能改变 Agent 的行为; - 正确做法:永久锁定 System Prompt + 永久保留最新 1~2 轮对话,从第 1 条旧消息向前逐条剔除,直到满足 Token 预算。
3. L4:手写 /compact 深度历史摘要
当上下文达到警戒线(例如 75% 容量水位)时,通过轻量大模型把过去数十轮历史提炼为结构化的事实:
- 核心提取:用户的最终目标、确定的技术栈架构、已修改的文件、已修复的 Bug、遗留 Todo;
- 重组格式:
[System] + [📋 前情提要与事实备忘] + [最近 1~2 轮交互]。
💻 源码实现精讲:code/s08_context_compact.py
1. ✂️ 0ms 滑动窗口 Token 预算截断实现
python
def truncate_sliding_window(
self,
messages: List[Dict[str, Any]],
max_tokens: int = 1000,
preserve_recent_turns: int = 2
) -> Tuple[List[Dict[str, Any]], str, int]:
"""基于 Token 预算的滑动窗口截断(0ms 延迟,永久锁定 System Prompt)"""
curr_tokens = estimate_tokens(messages)
if curr_tokens <= max_tokens or len(messages) <= 3:
return messages, "无需截断", 0
has_system = (messages[0].get("role") == "system")
system_msg = messages[0] if has_system else None
body_messages = messages[1:] if has_system else messages[:]
min_preserve = max(preserve_recent_turns * 2, 2)
dropped_count = 0
# 从最旧的消息开始依次剔除,直到满足预算或达到保留底线
while len(body_messages) > min_preserve:
candidate = ([system_msg] if system_msg else []) + body_messages
if estimate_tokens(candidate) <= max_tokens:
break
body_messages.pop(0)
dropped_count += 1
final_messages = ([system_msg] if system_msg else []) + body_messages
return final_messages, f"截断完成:丢弃早期 {dropped_count} 条消息", dropped_count2. 📦 手写 /compact 深度历史摘要引擎
python
def compact_history(self, messages: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], str]:
"""将早期冗长多轮历史提炼为结构化《前情提要背景摘要》"""
before_tokens = estimate_tokens(messages)
if before_tokens < 350 or len(messages) <= 3:
return messages, "上下文较小,无需压缩"
system_msg = messages[0] if messages[0].get("role") == "system" else {"role": "system", "content": "智能助手"}
history_to_compress = messages[1:-2] # 中间早期多轮
recent_messages = messages[-2:] # 保留最近 1-2 轮
prompt = f"""请对以下过往多轮交互历史进行高度精炼与结构化事实提炼:
1. 🎯 核心目标与技术栈:用户要解决什么、使用的框架与库;
2. 🛠️ 关键事实与已解决项:排查修复了什么 Bug、修改了哪些文件;
3. ⚠️ 过滤所有废话:丢弃中间反复试错流水账与长日志;
4. 输出 200 字以内的 Markdown 结构化摘要。
历史记录:
{str(history_to_compress)}
"""
res = self.client.chat([{"role": "user", "content": prompt}], temperature=0.2)
summary_text = res.choices[0].message.content.strip()
# 组装紧凑上下文
compacted = [
system_msg,
{"role": "system", "content": f"📋 【/compact 历史背景与前情提要摘要】:\n{summary_text}"}
] + recent_messages
after_tokens = estimate_tokens(compacted)
return compacted, f"压缩完成:Token 从 {before_tokens} ➔ {after_tokens}"💾 进阶深化:deepagents 上下文管理三板斧之二三
deepagents 将上下文管理精辟概括为三板斧:① 压缩历史、② 卸载大结果、③ Prompt 缓存:
板斧二:工具大结果“卸载”到文件(Offloading)
与其把 2000 行日志直接塞回上下文,不如只回填一句“结果已写入 /tmp/out.txt(共 2000 行)”,模型需要时再用 view_file 按行查——文件系统成了 Agent 的“外置硬盘”:
python
def offload_large_result(result: str, path: str = "/tmp/agent_out.txt") -> str:
"""结果过大时落盘,仅回填摘要指针,避免撑爆上下文"""
if len(result) < 2000:
return result
with open(path, "w", encoding="utf-8") as f:
f.write(result)
return f"[结果已卸载到 {path},共 {len(result)} 字符;需要时用 view_file 按需分段查看]"板斧三:Prompt 缓存(Prompt Caching)
GLM-V3 / Claude / DeepSeek 对前缀完全一致的请求会命中自动缓存,命中部分的 Token 费用可降至 1/10。代价是“前缀一致性”:越早的消息越不能乱动。因此工程上System 提示词与前情提要永远置顶稳定,动态更新仅发生在尾部——与我们的双轨设计天然契合。
记忆口诀:历史太旧就压缩,结果太大就卸载,前缀不变就缓存,极速应急就截断。
🕹️ 在 Gradio 中动手体验
🧪 本页是混合模式:“滑动窗口截断”和“工具超长输出首尾截断”全程本地运行,不消耗模型 Token;“
/compact深度压缩”和“全策略对比”会调用真实模型提炼摘要,需要配置ZHIPU_API_KEY。
在 code/app.py 中切换到 8.8 上下文工程与压缩 标签页:
- 一键载入真实超长对话预设:
- 🛒
电商全栈开发与支付排错(多轮需求、Vue3代码、500死锁报错排查,~1800 Tokens); - 🐛
微服务重构与分布式排错(gRPC/Saga 事务、Kafka 消息堆积排查,~2200 Tokens); - 📊
120行超长终端编译日志(工具截断体验专用)。
- 🛒
- 体验策略一【滑动窗口截断】:拖动 Token 预算滑块(如 500 Token),点击运行,感受 0ms 极速截断,观察哪些早期消息被丢弃;
- 体验策略二【/compact 历史压缩】:点击执行,观察大模型如何将长对话提炼为结构化《前情提要》,并抽查关键修改、技术栈与待办事项是否被保留;
- 点击【全策略多维度对比矩阵】:一键并行查看 原始 vs 截断 vs 压缩的 Token、时延与信息保留度全景对比。
压缩时必须留下哪些内容
好的摘要不是把对话缩短成几句话,而是保留后续行动所需的状态:用户目标、已经确认的约束、完成的修改、验证结果、失败尝试、待办事项,以及关键证据所在的文件或日志位置。观点可以概括,数字、路径和未解决的问题不应凭空改写。
摘要还应注明生成时间和覆盖的消息范围。长任务恢复时,先用原始记录抽查摘要,再继续执行。若摘要与项目当前状态冲突,应以可验证的文件和工具结果为准,而不是继续沿用旧结论。
📝 本节小结
- 拒绝单一策略:截断用于 0ms 极速降温与超长工具日志裁剪,压缩用于多轮复杂任务的事实提炼;
- 锁定 System Prompt:无论是截断还是压缩,核心人设与全局规则永远锁定在第 0 位;
- 下一步:除了当次会话的上下文,如何让 Agent 在关闭终端后依然记住你的长期偏好与专业技能?请进入——8.9 记忆系统与技能挂载!