Skip to content

8.8 上下文工程与压缩:双轨防御体系(0ms 滑动截断 + 工具裁剪 vs /compact 深度语义压缩) ​

“厚厚的会议记录本写满了琐碎的‘咳了一声、喝了口水’。抽屉满了,你可以直接把底层旧草稿扔碎纸机(截断:0秒搞定但旧事全忘);更聪明的秘书会撕掉流水账,提炼出一页清晰的《决议备忘录》(压缩:核心事实全保住)!”


🗜️ 为什么长对话必须做上下文工程? ​

在复杂的长程任务中(例如全栈重构、多模块联调、分布式故障排查),Agent 会频繁调用工具、读取长文件、执行终端命令。随之而来的就是 Token 爆炸危机:

  1. 费用昂贵:每轮对话都要把前面几万 Token 重新传一遍,账单直线上升;
  2. 大海捞针效应(Lost in the Middle):当上下文超过数万 Token 时,大模型的注意力会被海量无关日志分散,开始“失忆”或答非所问;
  3. 触顶报错:最终超出模型的最大 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_count

2. 📦 手写 /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 上下文工程与压缩 标签页:

  1. 一键载入真实超长对话预设:
    • 🛒 电商全栈开发与支付排错(多轮需求、Vue3代码、500死锁报错排查,~1800 Tokens);
    • 🐛 微服务重构与分布式排错(gRPC/Saga 事务、Kafka 消息堆积排查,~2200 Tokens);
    • 📊 120行超长终端编译日志(工具截断体验专用)。
  2. 体验策略一【滑动窗口截断】:拖动 Token 预算滑块(如 500 Token),点击运行,感受 0ms 极速截断,观察哪些早期消息被丢弃;
  3. 体验策略二【/compact 历史压缩】:点击执行,观察大模型如何将长对话提炼为结构化《前情提要》,并抽查关键修改、技术栈与待办事项是否被保留;
  4. 点击【全策略多维度对比矩阵】:一键并行查看 原始 vs 截断 vs 压缩的 Token、时延与信息保留度全景对比。

压缩时必须留下哪些内容 ​

好的摘要不是把对话缩短成几句话,而是保留后续行动所需的状态:用户目标、已经确认的约束、完成的修改、验证结果、失败尝试、待办事项,以及关键证据所在的文件或日志位置。观点可以概括,数字、路径和未解决的问题不应凭空改写。

摘要还应注明生成时间和覆盖的消息范围。长任务恢复时,先用原始记录抽查摘要,再继续执行。若摘要与项目当前状态冲突,应以可验证的文件和工具结果为准,而不是继续沿用旧结论。

📝 本节小结 ​

  • 拒绝单一策略:截断用于 0ms 极速降温与超长工具日志裁剪,压缩用于多轮复杂任务的事实提炼;
  • 锁定 System Prompt:无论是截断还是压缩,核心人设与全局规则永远锁定在第 0 位;
  • 下一步:除了当次会话的上下文,如何让 Agent 在关闭终端后依然记住你的长期偏好与专业技能?请进入——8.9 记忆系统与技能挂载!