你有没有遇到过这样的情况:和 AI 聊了十几轮,突然发现它"忘记"了之前说过的内容?或者 API 调用因为 Token 超限直接报错?
这是因为大模型有上下文长度限制,对话越长,历史消息越多,就越容易超限。结合我的理解,这篇文章分享一下如何实现上下文压缩,让你的 AI 应用既能"记住"关键信息,又能节省成本。
为什么需要上下文压缩?
大模型的上下文窗口是有限的。GPT-3.5 只有 4K tokens,GPT-4 最多 32K,即 Claude 3 有 200K,看起来很长,但实际对话中,10 轮对话就可能消耗几千 Token。
压缩的好处:
- 省钱:Token 越少,API 费用越低
- 避免报错:不会因为超限导致调用失败
- 提升质量:保留关键信息,让对话更连贯
压缩策略怎么选?
面对上下文压缩,我们有三种主流策略:
摘要压缩:让 AI 把历史对话总结成一段摘要。适合长对话场景,比如客服机器人、知识问答。优点是信息损失小,缺点是需要额外 API 调用。
滑动窗口:只保留最近 N 条消息。适合实时性要求高的场景,比如聊天机器人。优点是实现简单、零成本,缺点是可能丢失重要信息。
关键信息提取:从对话中提取实体、意图、偏好等结构化信息。适合任务型对话,比如订票助手、购物咨询。优点是信息结构化,缺点是需要设计合理的信息模型。
摘要压缩:让 AI 自己总结
摘要压缩的思路很直观:把历史对话丢给 AI,让它生成一段简洁的摘要。
实现要点:
第一步,收集历史对话。把用户和 AI 的消息按时间顺序整理好。
第二步,构建提示词。告诉 AI:"请将以下对话压缩成一段简洁的摘要,保留关键信息。"
第三步,调用 AI 生成摘要。拿到摘要后,用它替换原来的历史消息。
核心代码:
func summarizeHistory(messages []Message) (string, error) {
prompt := "请将以下对话压缩成简洁摘要,保留关键信息:\n"
for _, msg := range messages {
prompt += fmt.Sprintf("%s: %s\n", msg.Role, msg.Content)
}
// 调用 AI API 生成摘要...
return callAI(prompt)
}
什么时候触发压缩?
可以设置一个阈值,比如对话超过 15 轮时触发。也可以根据 Token 数量判断,超过总上下文的 70% 就压缩。
注意事项:
摘要会丢失细节信息。如果用户问"我刚才说的那个餐厅叫什么",AI 可能答不上来。这时候可以设计一个追问机制:"抱歉,能再告诉我一次吗?"
滑动窗口:简单高效的方案
滑动窗口是最简单的策略:只保留最近 N 条消息,旧的直接丢弃。
为什么好用?
实现极其简单,几行代码就能搞定。不需要额外的 API 调用,零成本。对于大多数聊天场景,最近几轮对话已经足够。
核心代码:
type Conversation struct {
Messages []Message
MaxSize int
}
func (c *Conversation) AddMessage(msg Message) {
c.Messages = append(c.Messages, msg)
if len(c.Messages) > c.MaxSize {
c.Messages = c.Messages[len(c.Messages)-c.MaxSize:]
}
}
窗口大小怎么定?
一般建议 5-10 条。太小会丢失上下文,太大又起不到压缩效果。可以根据实际场景调整。
一个改进技巧:
系统消息(System Prompt)要始终保留。它定义了 AI 的角色和行为准则,丢了会影响回复质量。
缺点也很明显:
早期的重要信息会被丢弃。比如用户一开始说了"我叫张三",过了 10 轮后 AI 就不知道了。
关键信息提取:保留核心数据
关键信息提取是从对话中抽取出实体、意图、状态等结构化信息。
适合什么场景?
任务型对话最合适。比如订票助手需要知道出发地、目的地、日期;购物咨询需要知道预算、品牌偏好。
提取什么信息?
- 用户信息:姓名、联系方式、身份
- 意图识别:用户想做什么
- 实体抽取:地点、时间、物品、金额
- 偏好记录:喜欢什么、不喜欢什么
核心代码:
type UserInfo struct {
Name string `json:"name"`
Intent string `json:"intent"`
Entities []string `json:"entities"`
}
func extractKeyInfo(dialog string) (*UserInfo, error) {
prompt := "从对话中提取关键信息,返回 JSON 格式:" + dialog
// 调用 AI 提取并解析 JSON...
return &UserInfo{}, nil
}
举个例子:
用户说"我想订一张明天从北京到上海的机票,预算 1000 以内"。
提取结果:
意图:订机票
出发地:北京
目的地:上海
日期:明天
预算:1000 元以内
这些信息即使对话很长也不会丢失。
组合策略实践
单一策略各有优劣,实际项目中建议组合使用。
推荐的组合方案:
对话初期(1-10 轮):不做任何压缩,保留完整上下文。
对话中期(10-20 轮):启用滑动窗口,同时开始提取关键信息。
对话后期(20 轮以上):触发摘要压缩,把早期对话压缩成摘要,关键信息单独保留。
func (m *Manager) Compress(messages []Message) []Message {
switch {
case len(messages) > 20:
return compressBySummary(messages) // 摘要压缩
case len(messages) > 10:
return messages[len(messages)-10:] // 滑动窗口
default:
return messages // 不压缩
}
}
一个完整的上下文结构:
系统消息(始终保留)
↓
关键信息摘要(用户姓名、意图、偏好等)
↓
历史对话摘要(早期对话的总结)
↓
最近 N 条对话(滑动窗口)
↓
当前用户输入
这样既保留了关键信息,又控制了上下文长度。
踩坑指南
Token 估算要提前做
中文大约 1.5-2 个字一个 Token,英文约 4 个字符一个 Token。压缩前先估算当前上下文大小,别等到报错了才处理。
func estimateTokens(text string) int {
return len(text) / 2 // 中文粗略估算
}
系统消息不能丢
无论用哪种策略,系统消息(System Prompt)都必须保留。它定义了 AI 的角色和行为准则。
压缩时机要合理
可以在每次对话后检查,也可以达到阈值时触发。不建议每轮都压缩,会增加延迟和成本。
给用户留条后路
压缩可能导致信息丢失。可以设计一个机制,当 AI 不确定时主动追问:"抱歉,能再告诉我一次您的联系方式吗?"
写在最后
今天我们介绍了三种 AI 对话上下文压缩策略:
摘要压缩:让 AI 总结历史,保留语义完整性。适合长对话场景。
滑动窗口:只保留最近 N 条,简单高效。适合实时性要求高的场景。
关键信息提取:抽取实体和意图,结构化存储。适合任务型对话。
实际项目中,建议根据对话长度动态选择策略,既能控制成本,又能保证对话质量。