目录
LLM 上下文压缩:让 AI Agent 拥有"无限记忆"的秘密
/    

LLM 上下文压缩:让 AI Agent 拥有"无限记忆"的秘密

LLM 上下文压缩:让 AI Agent 拥有"无限记忆"的秘密

在构建基于大语言模型(LLM)的 Agent 应用时,你一定遇到过这个问题:上下文窗口不够用

当对话轮次增多、工具调用频繁时,messages 列表会迅速膨胀,最终触发 context_length_exceeded 错误。本文将深入剖析一套经过生产验证的上下文压缩方案,让你的 Agent 能够处理任意长度的复杂任务。

问题:上下文窗口的天花板

以 GPT-4 Turbo 的 128K 上下文为例:

系统提示词:        ~2,000 Token
10轮简单对话:      ~5,000 Token
5次文件读取结果:   ~50,000 Token (每个文件约10K)
3次代码执行输出:   ~15,000 Token
─────────────────────────────────
累计消耗:          ~72,000 Token

看起来还有余量?但别忘了:

  • 需要为 AI 回复预留 32K 输出空间
  • 每轮对话都在累加
  • 一个大文件就可能占用 20K+

解决方案:两级压缩策略

我们采用渐进式压缩的思路:

轻量级 Prune(精简)→ 不够用 → 重量级 Compaction(压缩)

策略一:Prune(工具输出精简)

目标:清除历史工具调用的大文本输出,保留调用记录。

核心原理

# 原始 messages
{"role": "tool", "content": "// app.ts 的完整内容,5000行代码..."}

# 精简后
{"role": "tool", "content": "[Old tool result content cleared]"}

消息数量不变,但内容被占位符替代。

实现逻辑

def tool_compact(messages):
    """
    工具调用裁剪
    1. 保护最近 20K Token 的工具输出不被裁剪
    2. 至少要裁掉 10K Token 才执行
    """
    PRUNE_PROTECT = 20_000  # 保护区
    PRUNE_MINIMUM = 10_000  # 最小裁剪量
  
    # 获取所有工具消息
    tool_messages = [m for m in messages if m['role'] == 'tool']
  
    total = 0
    to_prune = []
  
    # 从新到旧遍历
    for message in reversed(tool_messages):
        token_count = estimate_tokens(message['content'])
        total += token_count
      
        # 超过保护区的旧消息需要裁剪
        if total > PRUNE_PROTECT:
            to_prune.append(message)
  
    # 只有裁剪量足够大才执行
    prune_total = sum(estimate_tokens(m['content']) for m in to_prune)
    if prune_total > PRUNE_MINIMUM:
        for message in to_prune:
            message['content'] = '[Old tool result content cleared]'

策略二:Compaction(摘要压缩)

目标:当 Prune 后仍然超限,让 AI 生成一份"进度摘要",用 1 条消息替代几十条历史。

压缩前后对比

压缩前(50 条消息):

{
  "messages": [
    {"role": "system", "content": "You are..."},
    {"role": "user", "content": "第一个任务"},
    {"role": "assistant", "content": "..."},
    // ... 48 条更多消息 ...
  ]
}

压缩后(4 条消息):

{
  "messages": [
    {"role": "system", "content": "You are..."},
    {"role": "user", "content": "What did we do so far?"},
    {"role": "assistant", "content": "到目前为止,我们完成了:1. ..."},
    {"role": "user", "content": "用户的新问题"}
  ]
}

实现步骤

第一步:生成摘要

def generate_summary(messages):
    """调用 LLM 生成对话摘要"""
  
    SUMMARY_SYSTEM_PROMPT = """
    You are a helpful AI assistant tasked with summarizing conversations.
  
    Focus on information helpful for continuing the conversation:
    - What was done
    - What is currently being worked on
    - Which files are being modified
    - What needs to be done next
    - Key user preferences that should persist
    """
  
    summary_request = [
        {"role": "system", "content": SUMMARY_SYSTEM_PROMPT},
        *messages[1:],  # 原始对话(不含 system prompt)
        {"role": "user", "content": "请总结上述对话,以便继续后续工作。"}
    ]
  
    response = openai.chat.completions.create(
        model="gpt-4",
        messages=summary_request
    )
  
    return response.choices[0].message.content

第二步:重建 messages

def rebuild_messages(original_messages, summary):
    """用摘要重建精简的 messages 列表"""
  
    return [
        original_messages[0],  # 保留原始 system prompt
        {"role": "user", "content": "What did we do so far?"},
        {"role": "assistant", "content": summary}
    ]

何时触发压缩?

关键公式

已用 Token > 模型上下文限制 - 预留输出空间

实现

def is_overflow(usage, model_limit=128000, reserved_output=32000):
    """检测是否需要压缩"""
    usable = model_limit - reserved_output
    total_used = usage['prompt_tokens'] + usage['completion_tokens']
    return total_used > usable

检测时机:在每轮 LLM 请求完成后,根据 API 返回的 usage 字段判断。

response = client.chat.completions.create(...)

if is_overflow(response.usage):
    summary = generate_summary(messages)
    messages = rebuild_messages(messages, summary)

Token 估算:中英混合优化

由于压缩判断发生在 API 返回后,Token 计数使用 API 的精确值。但 Prune 策略需要本地估算:

class Token:
    TOKENS_PER_CJK = 1.5      # 中文字符
    CHARS_PER_ASCII = 4       # 英文字符
  
    @staticmethod
    def is_cjk(char):
        code = ord(char)
        return 0x4E00 <= code <= 0x9FFF
  
    @classmethod
    def estimate(cls, text):
        if not text:
            return 0
      
        cjk_count = sum(1 for c in text if cls.is_cjk(c))
        ascii_count = len(text) - cjk_count
      
        return round(cjk_count * cls.TOKENS_PER_CJK + ascii_count / cls.CHARS_PER_ASCII)

完整流程图

用户发问 → LLM 返回 → finish-step
                         ↓
              ┌──────────────────────┐
              │   策略1: Prune       │  ← 每轮都执行
              │  清除旧工具输出       │
              └──────────────────────┘
                         ↓
              ┌──────────────────────┐
              │  isOverflow() 检测   │
              └──────────────────────┘
                         ↓
                  还是溢出?
                    ↓ 是
              ┌──────────────────────┐
              │  策略2: Compaction   │
              │  AI生成摘要+截断历史  │
              └──────────────────────┘
                         ↓
              ┌──────────────────────┐
              │     继续对话         │
              └──────────────────────┘

最佳实践

建议说明
预留足够缓冲建议预留 32K Token 给输出,避免被截断
Prune 保护近期数据最近 20-40K 的工具输出不要裁剪,保证上下文连贯
摘要提示词要详细明确告诉 AI 需要保留哪些关键信息
保留原始 System Prompt压缩只清理对话历史,不改变 AI 的"人设"
使用 API 返回的精确 Token不要依赖本地估算做溢出判断

结语

通过这套两级压缩策略,你的 Agent 可以:

  • ✅ 处理任意长度的复杂任务
  • ✅ 保持对项目状态的清晰认知
  • ✅ 节省 API 成本(减少重复发送的历史内容)
  • ✅ 避免 context_length_exceeded 错误

这不是"删除记忆",而是"压缩记忆"——就像人类的大脑会把详细的经历提炼成抽象的经验一样。


本文基于对 OpenCode 项目架构的深度分析。


标题:LLM 上下文压缩:让 AI Agent 拥有"无限记忆"的秘密
作者:gitsilence
地址:https://blog.lacknb.cn/articles/2026/01/10/1768052193768.html