← 返回首页

Agent 上下文管理实战:三层渐进式压缩

2026/3/23

Agent LLM 上下文管理

Agent 上下文管理实战:三层渐进式压缩

Context Window 是有限资源,如何让它持续服务长时间对话?


为什么需要上下文管理

问题

问题后果
Token 溢出超过模型限制,API 报错
注意力退化Lost-in-Middle,中间信息召回率低 10-40%
成本暴增每个 Token 都要付费
响应变慢更多 Token = 更长推理时间

解决方案:三层渐进式压缩

┌─────────────────────────────────────────┐
│          上下文管理层次                   │
├─────────────────────────────────────────┤
│ Level 1: Prune    → 丢弃旧工具输出        │
│ Level 2: Compact  → 生成总结性消息        │
│ Level 3: Summarize → 会话级摘要           │
└─────────────────────────────────────────┘

Level 1:修剪 (Prune)

丢弃什么

工具输出:工具执行结果通常很长,但价值随时间递减。

// 修剪策略
function prune(messages: Message[]): Message[] {
  return messages.map(msg => {
    if (msg.role === 'tool') {
      // 保留前 500 字符,丢弃其余
      return {
        ...msg,
        content: msg.content.slice(0, 500) + '\n...[trimmed]'
      }
    }
    return msg
  })
}

保留什么

  • 用户原始请求(不能丢)
  • 助手的决策和推理(保留)
  • 最近的工具输出(保留)
  • 旧的、已处理的工具输出(可修剪)

Level 2:压缩 (Compact)

触发时机

// 检测是否需要压缩
async function isOverflow(tokens: TokenUsage, model: Model): boolean {
  const context = model.contextWindow    // 比如 200K
  const output = Math.min(model.maxOutput, 20000)  // 预留输出
  const usable = context - output

  const used = tokens.input + tokens.cached + tokens.output
  return used > usable
}

压缩策略

将多条消息压缩成一条总结性消息:

// 压缩前:10 条消息,5000 tokens
const before = [
  { role: 'user', content: '帮我分析这个 bug' },
  { role: 'assistant', content: '好的,我先...' },
  { role: 'tool', content: '很长很长的输出...' },
  // ... 更多消息
]

// 压缩后:1 条消息,500 tokens
const after = [
  {
    role: 'system',
    content: `[历史摘要] 用户请求分析 bug,助手检查了代码,
              发现问题在 async 函数的错误处理。当前状态:已定位问题。`
  }
]

Token 估算

// 简单估算:4 字符 ≈ 1 token
function estimateToken(text: string): number {
  return Math.ceil(text.length / 4)
}

Level 3:总结 (Summarize)

会话级摘要

当压缩也不够时,生成整个会话的摘要:

interface SessionSummary {
  mainTask: string        // 主要任务
  completed: string[]     // 已完成
  pending: string[]       // 待处理
  decisions: string[]     // 关键决策
  context: string         // 当前上下文
}

摘要生成

async function summarize(messages: Message[]): Promise<string> {
  const prompt = `
请总结以下对话的关键信息:
1. 主要任务是什么
2. 已完成什么
3. 当前状态
4. 待处理事项

对话历史:
${messages.map(m => `${m.role}: ${m.content}`).join('\n')}
`
  return await llm.complete(prompt)
}

实战:实现一个简单的上下文管理器

class ContextManager {
  private maxTokens: number
  private reserveTokens: number

  constructor(maxTokens: number = 200000, reserveTokens: number = 20000) {
    this.maxTokens = maxTokens
    this.reserveTokens = reserveTokens
  }

  // 估算 token 数量
  estimateTokens(text: string): number {
    return Math.ceil(text.length / 4)
  }

  // 检查是否需要压缩
  needsCompaction(messages: Message[]): boolean {
    const total = messages.reduce((sum, msg) =>
      sum + this.estimateTokens(msg.content), 0
    )
    return total > this.maxTokens - this.reserveTokens
  }

  // Level 1: 修剪工具输出
  prune(messages: Message[], maxLength: number = 500): Message[] {
    return messages.map(msg => {
      if (msg.role === 'tool' && msg.content.length > maxLength) {
        return {
          ...msg,
          content: msg.content.slice(0, maxLength) + '\n...[trimmed]'
        }
      }
      return msg
    })
  }

  // Level 2: 压缩历史
  async compact(messages: Message[], llm: LLM): Promise<Message[]> {
    if (!this.needsCompaction(messages)) {
      return messages
    }

    // 先修剪
    let processed = this.prune(messages)

    // 如果还是太长,生成摘要
    if (this.needsCompaction(processed)) {
      const summary = await this.generateSummary(processed, llm)
      processed = [
        { role: 'system', content: `[历史摘要]\n${summary}` },
        ...processed.slice(-3)  // 保留最近 3 条
      ]
    }

    return processed
  }

  // 生成摘要
  private async generateSummary(messages: Message[], llm: LLM): Promise<string> {
    const history = messages
      .map(m => `${m.role}: ${m.content.slice(0, 200)}`)
      .join('\n')

    const prompt = `总结以下对话的关键信息(200字以内):

${history}

摘要:`

    return await llm.complete(prompt)
  }
}

使用示例

const manager = new ContextManager(200000, 20000)
const messages: Message[] = []

// 添加消息
messages.push({ role: 'user', content: '...' })
messages.push({ role: 'assistant', content: '...' })
messages.push({ role: 'tool', content: '...' })

// 压缩前检查
if (manager.needsCompaction(messages)) {
  const compacted = await manager.compact(messages, llm)
  // 使用压缩后的消息继续对话
}

最佳实践

1. 预留输出 Token

// ❌ 错误:用满整个 context window
const usable = model.contextWindow

// ✅ 正确:预留输出空间
const usable = model.contextWindow - model.maxOutput

2. 优先级保留

用户请求 > 助手决策 > 最近工具输出 > 旧工具输出

3. 分层压缩

Prune → Compact → Summarize

先用轻量方法,不够再用重方法

4. 监控 Token 使用

// 每次 API 调用后记录
function trackUsage(usage: TokenUsage) {
  console.log(`Input: ${usage.input}, Output: ${usage.output}`)
  console.log(`Usage: ${usage.input + usage.output} / ${model.contextWindow}`)
}

常见问题

Q: 压缩会丢失信息吗?

会。但:

  • 旧信息的价值随时间递减
  • 摘要保留了关键信息
  • 用户可以重新提问

Q: 什么时候触发压缩?

通常在:

  • Token 使用超过 80%
  • 对话超过 50 轮
  • 模型返回 context overflow 错误

Q: 如何评估压缩效果?

指标:

  • Token 减少量
  • 对话连贯性(主观)
  • 任务完成率

参考资料

📝 文章反馈

你的反馈能帮助我写出更好的文章