解决记忆冗余与检索截断

RAG 搜索优化 - 解决记忆冗余与检索截断

前言

在上一篇 动态记忆压缩 中,会遇到RAG 记忆系统中两个非常隐蔽但致命的缺陷:

  • 记忆冗余(Memory Bloat):同一件事聊了多次,数据库里存了多条相似记录,浪费存储和检索资源。
  • 检索截断(Retrieval Cutoff):因为相似记录太多,把真正关键的那条挤出了 Top-K,导致模型拿不到最新或最准确的信息。

我们需要在原有架构上增加 “记忆去重合并”“超量检索 + 二次压缩” 机制。

解决记忆冗余与检索截断

在实现记忆检索时,我们常遇到这样的场景:用户多次询问“项目预算是多少”,每次对话后系统都生成了一条新的记忆存入向量库。

  • 问题 1:向量库里充满了 10 条相似的“预算记忆”,造成存储浪费。
  • 问题 2:检索时设置 Top-K=5,结果前 5 条都是旧预算,第 6 条才是最新的“预算已修改”,导致模型回答错误。

为了解决这两个问题,我们需要引入 记忆融合(Memory Fusion)超量检索压缩(Over-Fetch & Consolidate) 策略。

  1. 写入侧优化:记忆融合(Upsert 策略)

不要每次对话结束都无条件 Insert 新记忆。在写入前,先检查是否已存在相似记忆。

机制流程

  1. 生成候选记忆:会话结束后,生成新的记忆摘要。
  2. 相似度检查:在向量库中检索与该摘要相似度最高的现有记忆。
  3. 决策分支
    • 相似度高(>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 万”是覆盖;“讨论了预算,又讨论了技术栈”是追加。
  1. 读取侧优化:超量检索 + 二次压缩

即使做了写入优化,仍可能因为语义细微差别导致关键信息排在第 6 位。为了保险,我们在读取时采用 “宽进严出” 策略。

机制流程

  1. 超量检索(Over-Fetch):不要只查 Top-5,改为查 Top-20。把可能相关的都捞出来。
  2. 重排序(Re-Rank):使用 Cross-Encoder 模型(如 BGE-Reranker)对这 20 条进行精确打分排序。
  3. LLM 二次压缩(Consolidation)
    • 将排序后的前 10 条记忆一次性丢给 LLM。
    • Prompt 指令:“以下是检索到的 10 条相关历史记忆,可能存在重复或冲突。请综合分析,整理成一份不超过 500 字的‘上下文背景总结’。如果有冲突信息,以时间戳最新的为准。”
    • 结果:无论关键信息在第几位,只要在前 20 里,就会被 LLM 提炼进最终的总结中。

为什么这样做不会爆显存?

  • 虽然检索了 20 条,但它们没有直接进 Prompt。
  • 进 Prompt 的是 LLM 压缩后的一份总结(固定 500 字)。
  • 这既解决了“关键信息被截断”的问题,又控制了最终 Token 消耗。
  1. 数据结构优化:事实与叙事分离

为了彻底避免“预算变了但检索出旧预算”的问题,建议将记忆分为两类存储:

记忆类型存储方式更新策略检索方式例子
**静态事实 (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 次预算,数据库里也只有一条最新的“预算记忆”,检索时也永远能拿到最新值,且不会撑爆上下文。

评论


暂无评论

* 登录后即可评论