Skip to content

Prompt 工程与上下文滑动窗口压缩策略 ​

背景:长会话下的上下文危机 ​

虽然现代大模型支持 128k 甚至 1M+ 的上下文窗口,但在实际业务与复杂 Agent 中,盲目堆砌历史对话存在三大严重问题:

  1. 费用线性甚至二次方激增:每一轮对话都需要为历史所有 Token 付费。
  2. 推理延迟明显上升:Prefill(提示词预填充)耗时随着上下文长度急剧增加。
  3. “大海捞针”迷失(Lost in the Middle):上下文过长时,模型对位于中部的关键约束与细节容易忽略,指令遵从度显著下降。

因此,上下文压缩与窗口管理是 AI 应用工程化的必修课。


主流压缩策略对比 ​

策略原理优点缺点适用场景
固定轮次截断 (Sliding Window)仅保留最近 N 轮对话极其简单、计算零延迟早期关键背景完全丢失短轮次日常闲聊
阶段性递归摘要 (Summarization)触发阈值时用轻量模型提炼旧对话浓缩关键脉络,Token 降低 70%+产生额外摘要调用,可能损耗具体数值长期顾问、多轮客户支持
混合分层记忆 (Hybrid Layering)系统指令 + 关键实体字典 + 最近 N 轮原始消息关键事实不丢,近期细节精确需要实体提取算法设计任务型 Agent、AI 编程助手

混合分层记忆架构落地 ​

针对高要求的任务型助手,推荐采用如下分层架构:

text
┌──────────────────────────────────────────────┐
│ 1. Immutable System Prompt (永久固定规范)       │
├──────────────────────────────────────────────┤
│ 2. Extracted Entities & Facts (已确认的事实字典) │
│    - 用户偏好: macOS, TypeScript, Tailwind   │
│    - 目标文件: src/utils/format.ts            │
├──────────────────────────────────────────────┤
│ 3. Summarized Prior History (旧会话递归摘要)   │
│    "用户在前 5 轮中确认了登录接口已完成重构..."  │
├──────────────────────────────────────────────┤
│ 4. Active Sliding Window (最近 3 轮完整对话)   │
│    User: ...                                 │
│    Assistant: ...                            │
└──────────────────────────────────────────────┘

动态压缩触发算法实现 ​

typescript
interface ChatMessage {
  role: 'system' | 'user' | 'assistant'
  content: string
}

export class ContextManager {
  private maxTokens: number
  private recentMessagesToKeep: number

  constructor(maxTokens = 4000, recentMessagesToKeep = 4) {
    this.maxTokens = maxTokens
    this.recentMessagesToKeep = recentMessagesToKeep
  }

  /**
   * 检查并压缩历史消息列表
   */
  async optimizeContext(
    systemPrompt: string,
    history: ChatMessage[],
    summarizeFn: (messages: ChatMessage[]) => Promise<string>
  ): Promise<ChatMessage[]> {
    const totalLength = history.reduce((sum, m) => sum + m.content.length, 0)
    
    // 粗估 Token (1 字符约为 0.4~0.7 token,依据语言而定)
    if (totalLength < this.maxTokens * 2) {
      return [{ role: 'system', content: systemPrompt }, ...history]
    }

    // 分离出最近保留的消息与需要压缩的历史
    const recent = history.slice(-this.recentMessagesToKeep)
    const toCompress = history.slice(0, -this.recentMessagesToKeep)

    if (toCompress.length === 0) {
      return [{ role: 'system', content: systemPrompt }, ...recent]
    }

    // 调用快速小模型进行摘要提炼
    const summary = await summarizeFn(toCompress)

    const consolidatedSystemPrompt = `${systemPrompt}\n\n[前置会话核心背景摘要]:\n${summary}`

    return [
      { role: 'system', content: consolidatedSystemPrompt },
      ...recent
    ]
  }
}

实践心得 ​

  1. 别拿大模型做简单的统计:字符数估算用轻量快速函数,不要每轮都调 embedding 算 token。
  2. 提取而非单纯压缩:对于路径、用户 ID、特定参数等关键标识,用结构化 JSON Key-Value 存放于系统上下文中,避免被模型摘要时当成“细枝末节”过滤掉。
  3. 保留原作者的明确指令:当用户说“务必保留 XXX”时,将其直接升格为全局规则。

基于 VitePress 构建 | 记录真实开发与 AI 协作过程