Prompt 工程与上下文滑动窗口压缩策略
背景:长会话下的上下文危机
虽然现代大模型支持 128k 甚至 1M+ 的上下文窗口,但在实际业务与复杂 Agent 中,盲目堆砌历史对话存在三大严重问题:
- 费用线性甚至二次方激增:每一轮对话都需要为历史所有 Token 付费。
- 推理延迟明显上升:Prefill(提示词预填充)耗时随着上下文长度急剧增加。
- “大海捞针”迷失(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
]
}
}实践心得
- 别拿大模型做简单的统计:字符数估算用轻量快速函数,不要每轮都调 embedding 算 token。
- 提取而非单纯压缩:对于路径、用户 ID、特定参数等关键标识,用结构化 JSON Key-Value 存放于系统上下文中,避免被模型摘要时当成“细枝末节”过滤掉。
- 保留原作者的明确指令:当用户说“务必保留 XXX”时,将其直接升格为全局规则。