Agent 开发进阶(六):上下文不是越多越好,如何实现智能压缩

3 阅读6分钟

Agent 开发进阶(六):上下文不是越多越好,如何实现智能压缩

本文是「从零构建 Coding Agent」系列的第六篇,适合想让 Agent 处理长对话、长任务的开发者。

先问一个问题

你有没有遇到过这种情况:

  • 让 Agent 读一个大文件,上下文直接爆了
  • 跑一条长命令,输出塞满了整个对话
  • 多轮任务推进后,Agent 突然"失忆"了

这背后的原因很简单:上下文窗口是有限的,而任务产生的文本是无限的

语言模型的「上下文膨胀」问题

当你的 Agent 越来越强大时,它会产生越来越多的中间结果:

  • 读一个大文件,会塞进很多文本
  • 跑一条长命令,会得到大段输出
  • 多轮任务推进后,旧结果会越来越多

如果没有压缩机制,很快就会出现这些问题:

  1. 注意力被淹没:模型注意力被旧结果淹没,分不清主次
  2. 成本爆炸:API 请求越来越重,越来越贵
  3. 任务中断:最终直接撞上上下文上限,任务被迫中断

这就是上下文压缩要解决的核心问题:怎样在不丢掉主线连续性的前提下,把活跃上下文重新腾出空间

上下文压缩的核心设计:三层策略

用一个图来表示上下文压缩的工作原理:

tool output
   |
   +-- 太大 -----------------> 保存到磁盘 + 留预览
   |
   v
messages
   |
   +-- 太旧 -----------------> 替换成占位提示
   |
   v
if whole context still too large:
   |
   v
compact history -> summary

关键点只有三个:

  1. 大结果先落盘:太大不直接塞进上下文,写磁盘只留预览
  2. 旧结果先缩短:替换成简短占位,不一直原样保留
  3. 整体过长再摘要:生成一份保持连续性的摘要

几个必须搞懂的概念

上下文窗口

模型这一轮真正能一起看到的输入容量,不是无限的

活跃上下文

当前这几轮继续工作时,最值得模型马上看到的那一部分,不是历史上出现过的所有内容。

压缩

不是 ZIP 压缩文件,而是:

用更短的表示方式,保留继续工作真正需要的信息。

最小实现

第一步:大工具结果先写磁盘

PERSIST_THRESHOLD = 5000

def persist_large_output(tool_use_id: str, output: str) -> str:
    if len(output) <= PERSIST_THRESHOLD:
        return output

    stored_path = save_to_disk(tool_use_id, output)
    preview = output[:2000]
    return (
        "<persisted-output>\n"
        f"Full output saved to: {stored_path}\n"
        f"Preview:\n{preview}\n"
        "</persisted-output>"
    )

def save_to_disk(tool_use_id: str, output: str) -> str:
    output_dir = Path(".task_outputs/tool-results")
    output_dir.mkdir(parents=True, exist_ok=True)
    stored_path = output_dir / f"{tool_use_id}.txt"
    stored_path.write_text(output)
    return str(stored_path)

这一步的关键思想是:

让模型知道“发生了什么”,但不强迫它一直背着整份原始大输出。

第二步:旧工具结果做微压缩

def collect_tool_results(messages: list) -> list:
    results = []
    for msg in messages:
        if isinstance(msg.get("content"), list):
            for block in msg["content"]:
                if isinstance(block, dict) and block.get("type") == "tool_result":
                    results.append(block)
    return results

def micro_compact(messages: list) -> list:
    tool_results = collect_tool_results(messages)
    for result in tool_results[:-3]:
        result["content"] = "[Earlier tool result omitted for brevity]"
    return messages

这一步不是为了优雅,而是为了防止上下文被旧结果持续霸占。

第三步:整体历史过长时,做一次完整压缩

def compact_history(messages: list) -> list:
    summary = summarize_conversation(messages)
    return [{
        "role": "user",
        "content": (
            "This conversation was compacted for continuity.\n\n"
            + summary
        ),
    }]

def summarize_conversation(messages: list) -> str:
    summary_parts = [
        "Context was compacted due to length.",
        "Key information to preserve:",
        "- Current task objective",
        "- Completed key actions",
        "- Modified/viewed files",
        "- Key decisions and constraints",
        "- Next steps to take"
    ]
    return "\n".join(summary_parts)

这里最重要的不是摘要格式多么复杂,而是要保住这几类信息:

  • 当前目标是什么
  • 已经做了什么
  • 改过哪些文件
  • 还有什么没完成
  • 哪些决定不能丢

第四步:在主循环里接入压缩

def agent_loop(state):
    while True:
        state["messages"] = micro_compact(state["messages"])

        if estimate_context_size(state["messages"]) > CONTEXT_LIMIT:
            state["messages"] = compact_history(state["messages"])
            state["has_compacted"] = True

        response = call_model(...)
        ...

新手最容易犯的 5 个错

1. 以为压缩等于删除

# ❌ 错误
messages = messages[-10:]  # 直接砍掉前面的,断了连续性

# ✅ 正确
messages = compact_history(messages)  # 生成摘要,保留关键信息

不是,压缩是把"不必常驻活跃上下文"的内容换一种表示。

2. 只在撞到上限后才临时乱补

更好的做法是从一开始就有三层思路:

  • 大结果先落盘
  • 旧结果先缩短
  • 整体过长再摘要

3. 摘要只写成一句空话

# ❌ 错误
summary = "用户和AI聊了很久"

# ✅ 正确
summary = """
Context was compacted due to length.

Key information preserved:
- Current task: Review code for PR #123
- Completed: Read 5 files, identified 3 security issues
- Modified files: src/auth.py, src/utils.py
- Next: Generate review report
"""

如果摘要没有保住文件、决定、下一步,它对继续工作没有帮助。

4. 把压缩和 memory 混成一类

  • 压缩解决的是:当前会话太长了怎么办
  • memory解决的是:哪些信息跨会话仍然值得保留

5. 一上来就给初学者讲过多产品化层级

先讲清最小正确模型,比堆很多层名词更重要。

压缩后,真正要保住什么

压缩不是"把历史缩短"这么简单。

真正重要的是:让模型还能继续接着干活

所以一份合格的压缩结果,至少要保住下面这些东西:

必须保留的信息说明
当前任务目标用户想让 Agent 做什么
已完成的关键动作做到哪一步了
已修改的文件改了什么,防止重复
关键决定与约束哪些约束不能违反
下一步应该做什么继续推进的方向

为什么这很重要

因为 Agent 之所以能「持续干活」,不是因为它记忆力好。

而是因为系统持续维护着活跃上下文的预算,把真正重要的信息留在窗口里。

就像人一样:工作记忆是有限的,聪明人会把不重要的信息暂时记在纸上,只把关键的留在脑子里。

主循环的新责任

从这一章开始,主循环不再只是:

  • 收消息
  • 调模型
  • 跑工具

它还多了一个很关键的责任:管理活跃上下文的预算

也就是说,agent loop 现在同时维护两件事:

任务推进 <--> 上下文预算

这也和后面的机制紧密联动:

  • memory 决定什么信息值得长期保存
  • prompt pipeline 决定哪些块应该重新注入
  • error recovery 处理压缩不足时的恢复分支

下一章预告

有了上下文压缩,你的 Agent 可以在更长的会话中稳定工作。下一章我们将探讨如何让 Agent 具备自我纠错能力,在执行失败时能够自动恢复和重试。


一句话总结:上下文压缩的核心,不是尽量少字,而是让模型在更短的活跃上下文里,仍然保住继续工作的连续性。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。