Agent从诞生之初不断进化:

  • 工具
  • 记忆
  • 规划
  • Rules
  • SubAgent
  • Teams

这样下去Agent的对话历史会被无限延长,直到撑爆LLM的上下文窗口。如果用Agent干些复杂的活,这个问题将会很快出现。
当然有很多种办法来延缓这个问题,其中一个是上下文压缩。

为什么messages会被撑爆

每一次循环对话,messages会新增至少两条消息:

  • 第一轮对话: messages + LLM回答
  • 第二轮对话:messages += LLM回答
  • 第三轮对话:messages += LLM回答

假设有一个任务:找到nginx日志,提取error,并统计近一天内每半个小时error总数量以及每个接口的的数量,按数量大小降序排序,并写入statistic.txt文件。将这样的任务完成 messages会累积到几十条,其中每个工具的输出包含几十上百行的命令输出。
任何LLM的上下文窗口都是有限的,不管是 几W tokens还是几十W tokens,只要Agent读取几个大文件、执行几次grep返回大量结果,再进行几轮工具调用(sort、 cat…)这个窗口就会被迅速填满。对于本地的小模型,一般这个context window可能只有几千tokens,几轮循环就会装满。

有人问:现在的LLM context window越来越大了,还需要压缩吗? 窗口变大只是推迟了问题而没有解决问题。大量的上下文背后就是高额的token费用、更慢的响应速度以及LLM在超长上下文文本中可能会迷失重点,导致分析过程漫长而结果可能出现偏差。

在解决这个问题之前,先梳理下思路

  • 用更大的 context window模型:只能缓解
  • 限制最大循环次数:不可取,复杂任务就是需要多轮会话才能完成
  • 截断工具返回结果:不可取,可能丢失关键信息
  • 压缩旧对话历史:把先前的详细对话压缩成一段摘要,只保留重点信息。Agent靠摘要回忆之前做了什么来完成复杂任务。

压缩原理、几种压缩策略

压缩策略 原理 优点 缺点
摘要法 利用大模型自身的能力,定期(例如每 10 轮对话)将之前的对话历史总结成一段简短的“摘要” 极度节省空间,保留逻辑连贯性 可能丢失具体的细节(如具体的数字或时间)
关键信息提取 不生成自然语言,而是将对话中的实体(Entity)、**状态(State)和意图(Intent)**提取为结构化数据(JSON 或 Key-Value) 机器读取极快,适合结构化任务(如订票、查数据) 丢失了对话的“语气”和“上下文情感”
语义向量检索 将历史消息向量化(Embedding),存入向量数据库。当新任务到来时,只检索与当前任务语义最相关的那几条历史消息,而不是全部塞入 最灵活,能保留最相关的细节,适合长跨度记忆 需要额外的向量数据库存储和检索成本

压缩的核心思想:忘掉细节,保留现场

┌──────────────────────────────────────────────────────────────────────────────────────┐
│  1. 原始历史对话 (Raw History) - 占用巨大,包含大量噪音                                 │
│  ┌──────────────────────────────────────────────────────────────────────────────┐    │
│  │ [User]: 你好,我想查一下明天的天气。 (Hello, check weather)                     │    │
│  │ [Agent]: 你好!请问您在哪座城市? (Hi! Which city?)                            │     │
│  │ [User]: 北京。 (Beijing)                                                      │    │
│  │ [Agent]: 好的,正在查询北京明天的天气... (Querying Beijing...)                  │    │
│  │ [User]: 顺便帮我查一下北京明天的限行吗? (Also check traffic restriction)       │    │
│  │ [Agent]: 正在查询限行... (Checking traffic...)                                │    │
│  │ ... (中间穿插了 100 条类似的寒暄、确认、重试) ... (大量冗余对话)                 │    │
│  │ [User]: 现在帮我规划一个去故宫的路线。 (Plan route to Forbidden City)          │     │
│  │ [Agent]: 好的,正在规划... (Planning...)                                      │     │
│  └──────────────────────────────────────────────────────────────────────────────┘     │
│  ❌ 问题:Token 浪费,关键信息淹没,模型注意力分散,导致回答变慢或遗忘前文。               │
└──────────────────────────────────────────────────────────────────────────────────────┘
              ⬇️                              ⬇️                              ⬇️
              ⬇️ (压缩算法介入)                ⬇️ (压缩算法介入)                ⬇️ (压缩算法介入)
┌───────────────────────────┐   ┌───────────────────────────┐   ┌───────────────────────────┐
│  策略 A: 摘要 (Summarization)│   │  策略 B: 关键信息提取 (Extraction)│   │  策略 C: 向量检索 (Vector Retention)│
│  核心:用一句话概括过去    │   │  核心:只保留实体和状态      │   │  核心:只保留高相似度片段    │
└───────────────────────────┘   └───────────────────────────┘   └───────────────────────────┘
              ⬇️                              ⬇️                              ⬇️
      ┌─────────────────┐           ┌─────────────────┐           ┌─────────────────┐
      │ 生成摘要文本    │           │ 结构化数据块      │           │ 语义向量索引    │
      │ "用户已确认位置  │           │ {city: "北京",  │           │ [User]: "天气"  │
      │ 为北京,需查询  │           │  goal: "天气",   │           │ [User]: "限行"  │
      │ 天气、限行及宫   │           │  restriction:   │           │ [User]: "故宫"  │
      │ 廷路线。"        │          │  true,          │           │ [User]: "路线"  │
      │ (从 100 行 变为 1 行) │     │  route: "待规划" │           │ (非文本,仅索引)  │
      └─────────────────┘           └─────────────────┘           └─────────────────┘
              ⬇️                              ⬇️                              ⬇️
      ┌─────────────────────────────────────────────────────────────────────────────────┐
      │                       压缩后的上下文 (Compressed Context)                        │
      │  ┌─────────────────────────────────────────────────────────────────────────────┐│
      │  │ [Summary]: 用户在北京,任务列表:[查天气,查限行,规划故宫路线]。                ││
      │  │ [State]: 天气=待查,限行=待查,故宫路线=待规划。                               │ │
      │  │ [Key_Facts]: 城市=北京,目的地=故宫。                                         │ │
      │  │ [Recent_Q]: 用户刚问"现在帮我规划路线"。                                      │ │
      │  └─────────────────────────────────────────────────────────────────────────────┘ │
      │  ✅ 效果:Token 消耗减少 70%+,关键信息保留,模型注意力更集中,响应更快。             │
      └─────────────────────────────────────────────────────────────────────────────────┘

摘要法代码实现

COMPACT_THRESHOLD = 20  # 超过 20 条就压缩
KEEP_RECENT = 6         # 保留最近 6 条不压缩
def compact_messages(messages):
    if len(messages) <= COMPACT_THRESHOLD:
        return messages  # 没超阈值,不压缩
    system_msg = messages[0]                   # system prompt 永远保留
    old_messages = messages[1:-KEEP_RECENT]     # 旧消息 → 要被压缩
    recent_messages = messages[-KEEP_RECENT:]   # 最近的消息 → 保留原样
    # 把旧消息拼成文本
    old_text = ""
    for msg in old_messages:
        role = msg.get("role", "unknown") if isinstance(msg, dict) else getattr(msg, "role", "unknown")
        content = msg.get("content", "") if isinstance(msg, dict) else getattr(msg, "content", "")
        if content:
            old_text += f"[{role}]: {content}\n"
    # 调用 LLM 生成摘要,用 LLM 来压缩 LLM 的对话历史。这不是浪费吗?不是。因为这次 LLM 调用的唯一任务就是"总结",不带工具,输出简短,token 开销远小于把完整历史塞进每次请求
    summary_response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": "Summarize the following conversation history. Keep all important facts, file paths, command results, and decisions. Be concise but don't lose critical details."},
            {"role": "user", "content": old_text}
        ]
    )
    summary = summary_response.choices[0].message.content
    # 重新组装
    return [
        system_msg,
        {"role": "user", "content": f"[Previous conversation summary]: {summary}"},
        {"role": "assistant", "content": "Understood. I have the context from our previous conversation. Let me continue."},
        *recent_messages
    ]

切分位置

  • system_message = messages[0] # 需保留,包含了 Agent核心指令,压缩进摘要会丢失基础设定
  • compact_messages = messages[1:-KEEP_RECENT] # 要压缩消息
  • recent_messages = messages[-KEEP_RECENT:] # 最近消息,需保留,这些信息一旦被压缩成摘要,LLM 就无法精确引用了

循环中调用压缩
Agent 核心循环里加入这行代码:每轮循环开始前检查一次。没超阈值就原样返回(零开销),超了就压缩,messages 数量像锯齿波一样:涨到阈值 → 压缩回去 → 继续涨 → 再压缩。永远不会超过阈值太多,Agent 可以无限工作下去

def run_agent(user_message, max_iterations=30):
    messages = [...]
    for i in range(max_iterations):
        messages = compact_messages(messages)  # ← 就这一行
        response = client.chat.completions.create(...)
        ...

压缩会丢失信息吗
LLM保留了关键事实信息,而这些信息足够做接下来的决策,但是如果某个细节信息真的需要呢,Agent可以再次调用工具获取细节,就比如小时候看《鲁滨逊漂流记》记得主人公救下了一个土著人“星期五”(摘要),是怎么救下的?具体我忘记了,让我再翻下书本回忆下。(再次获取细节)

更多推荐