前言
在上一篇 动态记忆压缩 中,会遇到RAG 记忆系统中两个非常隐蔽但致命的缺陷:
- 记忆冗余(Memory Bloat):同一件事聊了多次,数据库里存了多条相似记录,浪费存储和检索资源。
- 检索截断(Retrieval Cutoff):因为相似记录太多,把真正关键的那条挤出了 Top-K,导致模型拿不到最新或最准确的信息。
我们需要在原有架构上增加 “记忆去重合并” 和 “超量检索 + 二次压缩” 机制。
解决记忆冗余与检索截断
在实现记忆检索时,我们常遇到这样的场景:用户多次询问“项目预算是多少”,每次对话后系统都生成了一条新的记忆存入向量库。
- 问题 1:向量库里充满了 10 条相似的“预算记忆”,造成存储浪费。
- 问题 2:检索时设置
Top-K=5,结果前 5 条都是旧预算,第 6 条才是最新的“预算已修改”,导致模型回答错误。
为了解决这两个问题,我们需要引入 记忆融合(Memory Fusion) 和 超量检索压缩(Over-Fetch & Consolidate) 策略。
-
写入侧优化:记忆融合(Upsert 策略)
不要每次对话结束都无条件 Insert 新记忆。在写入前,先检查是否已存在相似记忆。
机制流程
- 生成候选记忆:会话结束后,生成新的记忆摘要。
- 相似度检查:在向量库中检索与该摘要相似度最高的现有记忆。
- 决策分支:
- 相似度高(>0.85):判定为同一话题。触发 更新(Update) 操作,将新旧信息合并,而不是新增。
- 相似度低:判定为新话题。执行 新增(Insert) 操作。
代码逻辑示例
def save_memory(self, new_memory_summary):
# 1. 先检索最相似的一条
similar_memories = self.vector_store.search(new_memory_summary, top_k=1, threshold=0.85)
if similar_memories:
# 2. 如果存在高度相似记忆,执行合并更新
old_memory = similar_memories[0]
merged_content = self.llm_merge(old_memory.content, new_memory_summary)
# 更新旧记录,保留时间戳
self.vector_store.update(id=old_memory.id, content=merged_content, timestamp=now)
else:
# 3. 否则作为新记忆插入
self.vector_store.insert(content=new_memory_summary, timestamp=now)
- 优点:保证向量库中关于“预算”的记录只有一条最新的(或合并后的),从根本上解决冗余和挤占 Top-K 的问题。
- 注意:合并时需要 LLM 判断是“覆盖”还是“追加”。例如“预算从 50 万改为 60 万”是覆盖;“讨论了预算,又讨论了技术栈”是追加。
-
读取侧优化:超量检索 + 二次压缩
即使做了写入优化,仍可能因为语义细微差别导致关键信息排在第 6 位。为了保险,我们在读取时采用 “宽进严出” 策略。
机制流程
- 超量检索(Over-Fetch):不要只查
Top-5,改为查Top-20。把可能相关的都捞出来。 - 重排序(Re-Rank):使用 Cross-Encoder 模型(如 BGE-Reranker)对这 20 条进行精确打分排序。
- LLM 二次压缩(Consolidation):
- 将排序后的前 10 条记忆一次性丢给 LLM。
- Prompt 指令:“以下是检索到的 10 条相关历史记忆,可能存在重复或冲突。请综合分析,整理成一份不超过 500 字的‘上下文背景总结’。如果有冲突信息,以时间戳最新的为准。”
- 结果:无论关键信息在第几位,只要在前 20 里,就会被 LLM 提炼进最终的总结中。
为什么这样做不会爆显存?
- 虽然检索了 20 条,但它们没有直接进 Prompt。
- 进 Prompt 的是 LLM 压缩后的一份总结(固定 500 字)。
- 这既解决了“关键信息被截断”的问题,又控制了最终 Token 消耗。
-
数据结构优化:事实与叙事分离
为了彻底避免“预算变了但检索出旧预算”的问题,建议将记忆分为两类存储:
| 记忆类型 | 存储方式 | 更新策略 | 检索方式 | 例子 |
|---|---|---|---|---|
| **静态事实 (Facts)** | 结构化数据库 (SQL/JSON) | **覆盖更新** (Key-Value) | 直接读取 | 用户名、当前预算、截止日期 |
| **动态叙事 (Narratives)** | 向量数据库 (Vector DB) | 新增或合并 | 向量检索 | 讨论过程、用户情绪、需求变更原因 |
-
优势:对于“预算是多少”这种明确的事实,直接查结构化表,永远准确,不占用向量库空间,也不受 Top-K 影响。向量库只用来存“当时为什么定这个预算”的过程性信息。
-
实际生产
在实际生产中,简单的向量检索会遇到“记忆重复存储”和“关键信息被挤出 Top-K"的问题。我们需要引入更精细的管理策略。
1. 写入时的“记忆融合”
避免向量库膨胀。在存入新记忆前,先检索是否存在相似主题。
- 若相似度高:调用 LLM 将新旧记忆合并,更新原有记录(Upsert)。
- 若相似度低:作为新主题插入。 这确保了关于“项目预算”的记忆在库中只有一条最新的综合记录,而不是十条碎片。
2. 读取时的“宽进严出”
防止关键信息被截断。
- 宽进:检索时取
Top-20而非Top-5。 - 严出:将这 20 条记忆通过 LLM 进行 二次压缩(Consolidation),合并成一段连贯的摘要再填入 Prompt。
- 指令示例:“分析以下 20 条历史片段,去除重复,解决冲突(以最新时间为准),生成一份 300 字的背景总结。”
3. 事实与叙事分离
- 关键事实(如姓名、金额、日期)存入结构化数据库,查询时直接获取,保证 100% 准确。
- 过程信息(如讨论细节、偏好原因)存入向量库,用于补充上下文。
通过这套组合拳,我们不仅解决了上下文爆满问题,还构建了一个去重、准确、可进化的长期记忆系统。
总结
核心逻辑图更新:
graph TD
A[新记忆生成] --> B{检索库中是否有相似?}
B -- 有 --> C[LLM 合并新旧记忆 -> 更新原记录]
B -- 无 --> D[作为新记录插入]
E[用户查询] --> F[向量检索 Top-20]
F --> G[Cross-Encoder 重排序]
G --> H[LLM 二次压缩/去重/冲突解决]
H --> I[生成固定长度的记忆摘要]
I --> J[注入 Prompt]
这样处理,即便用户聊了 100 次预算,数据库里也只有一条最新的“预算记忆”,检索时也永远能拿到最新值,且不会撑爆上下文。
评论
* 登录后即可评论
登录