
在构建基于大语言模型(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
看起来还有余量?但别忘了:
我们采用渐进式压缩的思路:
轻量级 Prune(精简)→ 不够用 → 重量级 Compaction(压缩)
目标:清除历史工具调用的大文本输出,保留调用记录。
# 原始 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]'
目标:当 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)
由于压缩判断发生在 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 可以:
context_length_exceeded 错误这不是"删除记忆",而是"压缩记忆"——就像人类的大脑会把详细的经历提炼成抽象的经验一样。
本文基于对 OpenCode 项目架构的深度分析。