今天翻到一篇不错的技术分享,看完之后自己也琢磨了一下,把思路梳理记录下来。
目录
🌊前言
🌊结语
🌊面试回答
前言
随着大语言模型(LLM)的上下文窗口不断扩展——从最初的几K tokens,到现在的1M甚至10M tokens——很多人以为“长对话历史管理”这个问题已经过时了。但事实恰恰相反:窗口再大,也经不住无限对话的累积。更关键的是,长历史会带来三大痛点:计算成本飙升、响应延迟增加、模型在“大海”中难以精准定位关键信息。
本文将从原理到实践,系统讲解长对话管理的核心策略,并提供可直接落地的代码方案。
一、为什么需要管理对话历史?
在深入解决方案前,我们先明确问题的根源:
问题
说明
实际影响
Token 限制
即使模型支持 1M 上下文,单次请求的 token 数仍有上限
超长对话无法一次性发送
计算成本
输入 token 数是计费的核心指标(如 GPT-4 按 token 收费)
每次请求成本随历史线性增长
推理延迟
Attention 机制的计算复杂度为 O(n²)
历史越长,响应越慢
信息稀释
模型对输入中间部分的内容“关注度”较低(Lost in the Middle 现象)
关键信息容易被忽略
二、五种主流管理策略
1. 滑动窗口法
最轻松直观的方法——只保留最近 N 轮对话。
class SlidingWindowBuffer:
def __init__(self, max_turns: int = 10):
self.max_turns = max_turns
self.history = []
def add_message(self, role: str, content: str):
self.history.append({"role": role, "content": content})
# 按轮次截断(一轮=用户+助手)
if len(self.history) > self.max_turns * 2:
self.history = self.history[-self.max_turns * 2:]
def get_context(self):
return self.history
优点:实现轻松,资源可控
缺点:过早丢弃早期关键信息(如用户最初设定的任务目标)
2. Token 阈值法
以 token 数量为截断标准,比按轮次更精细。
import tiktoken
class TokenThresholdBuffer:
def __init__(self, max_tokens: int = 4000, model: str = "gpt-4"):
self.max_tokens = max_tokens
self.tokenizer = tiktoken.encoding_for_model(model)
self.history = []
def add_and_prune(self, role: str, content: str):
self.history.append({"role": role, "content": content})
while self._count_tokens() > self.max_tokens:
# 丢弃最早的一条消息(非轮次边界)
self.history.pop(0)
def _count_tokens(self):
text = "".join([m["content"] for m in self.history])
return len(self.tokenizer.encode(text))
3. 摘要压缩法
将早期对话“压缩”成摘要,替代原始内容。
class SummarizationBuffer:
def __init__(self, summarizer_llm, max_recent_turns: int = 5):
self.summarizer = summarizer_llm
self.max_recent = max_recent_turns
self.summary = "" # 累积摘要
self.recent_history = []
def add_message(self, role: str, content: str):
self.recent_history.append({"role": role, "content": content})
# 当最近历史超过阈值,将最旧的一轮压缩进摘要
if len(self.recent_history) > self.max_recent * 2:
to_summarize = self.recent_history.pop(0)
self._update_summary(to_summarize)
def _update_summary(self, message):
prompt = f"现有摘要:{self.summary}\n新对话:{message['content']}\n请生成更新后的简洁摘要:"
self.summary = self.summarizer(prompt)
def get_context(self):
# 返回:[摘要] + 最近N轮完整对话
summary_msg = {"role": "system", "content": f"对话历史摘要:{self.summary}"}
return [summary_msg] + self.recent_history
优点:信息保留度高
缺点:需要额外的 LLM 调用,引入延迟和成本
4. 重要事件筛选法
不只看时序,而是基于“重要性”评分保留关键消息。
class ImportanceBasedBuffer:
def __init__(self, max_tokens: int, importance_scorer):
self.max_tokens = max_tokens
self.scorer = importance_scorer # 函数或模型
self.messages = []
def add_message(self, role: str, content: str):
importance = self.scorer(content)
self.messages.append({
"role": role,
"content": content,
"importance": importance,
"timestamp": time.time()
})
self._prune()
def _prune(self):
# 按重要性排序,保留高重要性的消息
self.messages.sort(key=lambda x: x["importance"], reverse=True)
# 截断逻辑...(需保留时序顺序,实现较复杂)
挑战:如何定义“重要性”?常见方案包括:关键词匹配、用户明确标记、模型输出的置信度分数。
5. 混合策略(工业级方案)
实际生产环境通常组合多种策略:
┌─────────────────────────────────────────────────────┐
│ 混合策略架构 │
├─────────────────────────────────────────────────────┤
│ 最近 2 轮 → 完整保留 │
│ 第 3-10 轮 → 按 token 阈值滑动窗口 │
│ 第 10 轮以前 → 每 5 轮合并生成一次摘要 │
│ 所有用户标记为“重要”的消息 → 永久保留在 system 提示中 │
└─────────────────────────────────────────────────────┘
三、进阶技巧
技巧一:结构化历史注入
将对话历史按角色、时间、主题结构化,帮助模型更好地理解。
def format_structured_history(history: list) -> str:
"""输出格式化的历史字符串"""
parts = []
for idx, msg in enumerate(history):
parts.append(f"[{idx}] {msg['role']}: {msg['content']}")
if "key_points" in msg:
parts.append(f" → 关键点:{msg['key_points']}")
return "\n".join(parts)
技巧二:增量式摘要
避免每次重新摘要全部历史,使用滚动摘要:
class RollingSummarizer:
def __init__(self, llm):
self.llm = llm
self.global_summary = ""
def update(self, new_dialogues: list[str]):
prompt = f"""
当前全局摘要: {self.global_summary}
新增对话片段: {new_dialogues}
请更新摘要,保留所有关键信息,控制在200字以内。
"""
self.global_summary = self.llm.generate(prompt)
技巧三:上下文重构
不仅压缩历史,还可以重排信息位置,对抗“Lost in the Middle”:
def reorder_for_attention(messages: list) -> list:
"""
将高重要性信息移到开头或结尾,
因为模型对这两部分的注意力权重更高
"""
high_importance = [m for m in messages if m.get("importance", 0) > 0.8]
normal = [m for m in messages if m.get("importance", 0) self.max_recent_turns * 2:
self._compress_oldest_turn()
if self._total_tokens() > self.max_tokens:
self._aggressive_prune()
def _compress_oldest_turn(self):
"""将最早的一轮对话(user+assistant)压缩进摘要"""
if len(self.recent_messages) < 2:
return
oldest_user = self.recent_messages.pop(0)
oldest_assistant = self.recent_messages.pop(0)
dialogue = f"用户:{oldest_user['content']}\n助手:{oldest_assistant['content']}"
if self.summarizer and dialogue:
prompt = f"现有摘要:{self.summary}\n新对话:{dialogue}\n生成更新后的摘要(50字内):"
self.summary = self.summarizer(prompt)
def _total_tokens(self) -> int:
total = self._count_tokens(self.summary)
for msg in self.important_messages + self.recent_messages:
total += self._count_tokens(msg["content"])
return total
def _count_tokens(self, text: str) -> int:
return len(self.tokenizer.encode(text))
def _aggressive_prune(self):
"""紧急截断:删除非重要消息中最早的一半"""
keep = len(self.recent_messages) // 2
self.recent_messages = self.recent_messages[-keep:]
def get_context(self) -> List[Dict]:
"""构建最终发送给LLM的上下文"""
context = []
if self.summary:
context.append({"role": "system", "content": f"【对话历史摘要】{self.summary}"})
context.extend(self.important_messages)
context.extend(self.recent_messages)
return context
# 使用示例
manager = LongConversationManager(max_total_tokens=4000, max_recent_turns=3)
manager.add_message("user", "帮我总结一下这份报告...")
manager.add_message("assistant", "好的,报告的核心内容是...")
manager.add_message("user", "请记住这个关键数字:95%", important=True)
final_context = manager.get_context()
# 发送给 LLM
# response = llm.chat(final_context)
五、不同场景的最佳实践
应用场景
推荐策略
理由
客服机器人
滑动窗口 + 用户ID标记
话题切换快,不需要长期记忆
编程助手
Token阈值 + 结构化注入
代码片段长,但早期逻辑常被引用
角色扮演/虚拟伴侣
摘要压缩 + 重要事件标记
需要保留剧情和个人偏好
长文档分析
无历史管理,复用RAG
对话历史短,主要是单轮问答
企业知识库问答
混合策略
既有短期上下文,也有长期用户画像
六、避坑指南
1. 不要把 System Prompt 算进滑动窗口 System 消息应当始终保留,它定义了模型的角色和行为。
2. 注意摘要的信息衰减 多次嵌套摘要会导致信息失真。建议每 10 轮重新生成一次完整摘要(而非滚动更新)。
3. 对中文场景调整 token 计数 tiktoken 对中文按 Unicode 编码,一个中文字符通常 1-3 tokens,不同于英文的按子词切分。
4. 检测“灾难性遗忘” 定期用测试问题验证模型是否还记得早期关键信息。例如:“我们一开始讨论的项目名称是什么?”
七、未来方向
- 可学习的遗忘机制:用小模型预测哪些信息未来会被引用,动态决定保留策略。
- 分层记忆网络:短期记忆(缓存)+ 长期记忆(向量数据库)+ 永久记忆(system prompt),按检索相关度动态组合。
- 模型原生支持:如 MemGPT,让 LLM 自己管理自己的上下文窗口。
结语
管理长对话历史,没有“万能解药”。理解每一种策略的 trade-off,结合你的应用场景选择合适方案,必要时组合使用。记住一个朴素的真理:80% 的对话价值集中在最近 20% 的历史中——把压缩精力留给那 80% 的信息,同时确保那 20% 的关键信息永不丢失。
面试回答
长对话场景下,如果直接把所有历史都塞给模型,token消耗大、延迟高,而且模型中间容易‘失忆’。我会分几个层面来处理:
第一,先做‘有选择的遗忘’。 不存全部原始对话,而是保留关键信息。比如用户说‘我喜欢科幻片’,后面再说‘不看恐怖片’,这些偏好要结构化存下来;像‘嗯、好的、谢谢’这种客套话直接丢掉。
第二,用‘滑动窗口 + 摘要’组合。 最近3-5轮完整保留,保证上下文连贯;更早的历史每隔几轮调用一次大模型做‘滚动摘要’,把长剧情压缩成一段话。这样模型每次只看到‘最近对话 + 上一轮摘要’,成本低,又不会丢主线。
第三,分场景优化。
- 如果是客服场景,用户问‘订单状态’,我会用向量检索:把历史对话切成小块存进向量库,每次只召回最相关的2-3条,配合当前问题一起发给模型。
- 如果是角色扮演或心理咨询这种强记忆场景,我会维护一个用户记忆画像,把‘用户说过的重要事实、偏好、指令’单独存成结构化字段,每次请求都带上。
这篇笔记就先到这里,后面用到新的思路或者发现有问题再补充。
评论 (0)
暂无评论