混合记忆架构设计

要点

  • 在第四篇文章中,我们讲了向量化与语义检索的完整流程
  • 在设计混合检索之前,我们先把「记忆」分个类
  • 用户的一条消息进来后,系统不是只走一条检索路径,而是同时发起多条检索,最后将结果融合
  • 四条通道各有所长,覆盖不同类型的查询需求
  • 四条通道的结果汇总后,需要解决三个问题:去重、排序、截断

内容

1. 纯向量检索的局限

在第四篇文章中,我们讲了向量化与语义检索的完整流程。那套方案在大多数场景下工作得不错,但如果你真的用它上线一个 AI 伴侣产品,很快会遇到几类搞不定的问题。

场景一:精确事实查询

用户说「我的生日是几月几号?」。向量检索可能会召回「用户生日当天去了 Blue Note 爵士酒吧庆祝」——这条语义上和「生日」相关,但它回答不了「几月几号」这个精确问题。真正的答案应该从结构化的用户画像表中直接查出来:birthday: "03-14"

场景二:时间范围查询

用户说「上周我们聊了什么?」。向量检索只关心语义相似度,它没有「上周」的概念。你可以用元数据过滤(我们在第四篇提过),但如果「上周」涉及的对话有 200 条,向量检索返回的 Top-5 远远不够覆盖。

场景三:全文关键词查询

用户说「我之前提到过一本叫《三体》的书」。「三体」是一个专有名词,Embedding 模型在处理专有名词时经常「翻车」——它可能把「三体」映射到「物理学」或「科幻」的方向,而不是精确匹配这两个字。传统的关键词搜索反而能精准命中。

这三类场景有一个共同特征:它们需要的不是「意思相近」,而是「精确匹配」或「结构化查询」。单靠向量检索无法覆盖。

这就引出了本篇的核心主题:混合检索架构——将向量检索、关键词检索和结构化查询组合在一起,让记忆层能应对所有类型的查询。

2. 记忆的分类与存储策略

在设计混合检索之前,我们先把「记忆」分个类。不同类型的记忆,适合存储在不同的介质中,也需要用不同的方式检索。

记忆类型示例存储介质检索方式
事实(Fact)生日 03-14、喜欢巧克力蛋糕D1 关系型数据库结构化查询
事件(Event)去 Blue Note 听爵士、第一次吵架Vectorize + D1语义 + 关键词
偏好(Preference)讨厌被忽视、喜欢被夸聪明D1 + Vectorize结构化 + 语义
对话摘要(Summary)"上周主要聊了工作压力和追剧"D1 关系型数据库时间范围查询
情绪快照(Mood)昨天 23:00 心情指数 35/100CloudFlare KV键值查询
短期上下文(Context)最近 10 轮对话CloudFlare KV直接读取

Drawing canvas 三种存储介质的选型依据是访问模式而非数据量:

CloudFlare KV(热数据层):适合"每次请求都要读"的数据。最近 N 轮对话、当前情绪状态、亲密度数值这类高频读取的小体量数据放在这里。KV 是全球分布的键值存储,读取延迟在个位数毫秒,但不支持复杂查询——只能按 key 精确读取。

CloudFlare D1(结构化数据层):适合需要"条件查询"的数据。用户画像(姓名、生日、职业)、对话摘要(按时间段索引)、偏好标签(键值对形式)、记忆条目(供关键词检索)放在这里。D1 是边缘 SQLite 数据库,支持完整的 SQL——WHERELIKEORDER BYGROUP BY 都能用,但不支持语义相似度计算。

CloudFlare Vectorize(语义数据层):适合需要"意思相近"检索的数据。事件记忆、情感片段、长文本记忆放在这里。这一层我们在第四篇已经详细讲过。它的优势是语义理解,劣势是无法精确匹配和条件过滤。

一个关键的架构决策是:事件记忆同时写入 D1 和 Vectorize。这意味着同一条数据存两份——Vectorize 用于语义检索,D1 用于关键词检索。这是混合架构的核心代价:用存储冗余换取检索能力的完整覆盖

3. 混合检索管线

用户的一条消息进来后,系统不是只走一条检索路径,而是同时发起多条检索,最后将结果融合。整个管线分为四步:

Drawing canvas 第一步:查询理解(Query Understanding)

这是管线的入口,也是最关键的一步。它由 LLM 分析用户消息,输出一个"查询计划"——决定哪些检索通道需要被激活。

几个典型的分析结果:

用户消息激活的通道原因
"我的生日是几号"结构化查询精确事实,直接查 D1 画像表
"上周我们聊了什么"时间范围查询涉及时间表述,查 D1 摘要表
"我之前说过喜欢《三体》"关键词 + 语义专有名词用关键词匹配,上下文用语义检索
"你还记得我们第一次吵架吗"语义检索纯语义问题,无精确事实或时间范围
"我上个月跟你说过我换工作了"结构化 + 时间 + 语义"换工作"是事实变更,"上个月"是时间范围,还需要语义补充

大部分查询会同时激活多个通道。这不浪费——并行执行的总延迟等于最慢的那个通道,通常在 50-150ms 内。

查询理解本身需要一次 LLM 调用(100-300ms),这是管线中的延迟成本。但如果跳过这一步直接激活所有通道,每次请求都会产生不必要的数据库查询和向量检索,对后端资源是更大的浪费。这是一个延迟 vs 资源的权衡——在用户量大时,查询理解带来的资源节省远超它自身的延迟成本。

第二步:多通道并行检索

查询计划确定后,被激活的通道同时执行,互不等待。

第三步:结果融合

所有通道的结果汇总后,需要去重、加权、排序,合并为一个统一的记忆列表。下一节详细讲解。

第四步:截断输出

融合后的结果按分数排序,取 Top-K(通常 5-8 条)作为最终输出。截断是必要的——LLM 的上下文窗口有限,塞入太多记忆反而会稀释重要信息,导致回复质量下降。

4. 四条检索通道的特征对比

四条通道各有所长,覆盖不同类型的查询需求:

通道数据源匹配方式延迟擅长场景弱点
语义检索Vectorize向量余弦相似度50-100ms模糊回忆、情感关联、开放性问题专有名词、精确数值
结构化查询D1 画像表SQL 精确匹配10-30ms姓名、生日、职业等确定性事实无法处理模糊描述
时间范围查询D1 摘要表SQL 范围过滤10-30ms"上周"、"去年夏天"等时间表述依赖摘要质量
关键词检索D1 记忆表SQL LIKE / FTS15-40ms书名、地名、人名等专有名词无法理解同义词

几个架构层面的观察:

语义检索是"兜底通道"。它几乎在每次查询中都被激活,因为用户的大部分消息或多或少包含语义信息。其他三个通道是"精准补充"——在特定场景下提供语义检索无法给出的精确结果。

结构化查询的置信度天然最高。当用户问"我的生日是几号",从画像表查出的 birthday: "03-14" 是 100% 准确的事实,不存在"相似度"的概念。因此在结果融合时,结构化查询的结果权重应该高于其他通道。

时间范围查询依赖"摘要"而非原始对话。如果直接检索"上周的所有对话",可能返回 200 条原始消息,远超 LLM 的上下文容量。所以系统需要在后台异步生成对话摘要(每 N 轮对话压缩为一段摘要),时间范围查询检索的是这些摘要,而不是原始对话。

关键词检索和语义检索经常互补。用户说"我之前提到过《三体》",关键词检索能精确命中包含"三体"二字的记忆条目,语义检索能召回"用户喜欢刘慈欣的科幻作品"这类没有"三体"字面但语义相关的记忆。两者组合才能给出完整的上下文。

5. 结果融合策略

四条通道的结果汇总后,需要解决三个问题:去重、排序、截断

Drawing canvas 去重

同一条记忆可能同时被语义检索和关键词检索召回。比如一条包含"三体"的记忆,语义上与"科幻小说"相似(被语义检索命中),关键词上也匹配"三体"(被关键词检索命中)。如果不去重,同一条信息会在 LLM 的上下文中出现两次,浪费宝贵的上下文窗口。

去重策略是:计算任意两条结果的文本相似度,如果超过 85%,视为同一条记忆,保留分数更高的那条。这里的"文本相似度"不需要再走向量计算——用简单的字符级 Jaccard 系数或编辑距离就够了,因为目的是检测"几乎相同的文本"而非"语义相近"。

来源加权

不同通道返回的分数含义不同:语义检索返回的是余弦相似度(0-1),结构化查询返回的是"命中/未命中"。为了将它们放在同一个尺度上比较,需要给每个来源分配一个权重系数:

来源权重理由
结构化查询1.2x精确事实,可信度最高
语义检索1.0x基准权重
时间范围查询0.9x摘要有信息损失
关键词检索0.85x能匹配字面,但不理解语义

最终分数 = 原始分数 × 来源权重。精确事实查询的权重最高,因为它返回的结果几乎不会出错。

时效性加成

越近的记忆越可能与当前对话相关。一条昨天产生的记忆,其价值通常高于一条三个月前的记忆。时效性加成规则很简单:24 小时内的记忆额外加 10%,7 天内加 5%,更早的不加成。

这三步之后,所有记忆片段被统一到同一个分数维度上,按最终分数降序排列,取 Top-K 输出。

6. 记忆写入:双写架构

检索是"读"的一面,还有"写"的一面。每轮对话结束后,系统需要将新记忆写入三层存储。写入路径的设计直接决定了检索时能找到什么。

Drawing canvas 写入分为两个阶段:

同步写入(不阻塞用户回复):只更新 KV 中的短期上下文。将最近 N 轮对话写入 KV,设置 24 小时过期。这步必须同步执行,因为下一轮对话立即需要读取最新的上下文。

异步写入(在后台完成):由 LLM 从对话中提取结构化信息,写入 D1 和 Vectorize。这步通过消息队列异步执行,不阻塞用户的下一条消息。异步写入包含四个操作:

事实提取 → D1 画像表。LLM 识别对话中的结构化事实("我下周生日"→ birthday 字段更新),以键值对形式写入用户画像表。这些数据供结构化查询通道使用。

事件提取 → Vectorize + D1(双写)。LLM 识别值得长期记住的事件和情感("用户今天被裁员了,心情很低落"),将其向量化后写入 Vectorize(供语义检索),同时以原文形式写入 D1 的 memory_entries 表(供关键词检索)。这是整个架构中唯一的双写点——同一条数据存两份,是混合检索能力的必要代价。

摘要生成 → D1 摘要表。每 N 轮对话(如 20 轮),LLM 将这段对话压缩为一段摘要,写入 D1 摘要表并标记时间范围。这些摘要供时间范围查询通道使用。

上下文更新 → KV。将最新的对话追加到短期上下文缓存。

事实提取 vs 事件提取是两个独立的 LLM 调用,目标完全不同:

维度事实提取事件提取
输出格式键值对:{ field: "birthday", value: "03-14" }自然语言:"用户今天被裁员,心情低落"
存储方式覆盖写入(同一个 field 只保留最新值)追加写入(每条事件独立存储)
写入目标D1 画像表D1 记忆表 + Vectorize
调用频率有新事实时才写入每轮对话都可能产生

覆盖写入 vs 追加写入的区别很重要。用户说"我换工作了,现在在字节跳动",事实提取会覆盖 job 字段的旧值;而事件提取会追加一条新记忆"用户从上一家公司离职,入职字节跳动"。前者保证画像表始终是最新状态,后者保留完整的历史轨迹。

7. 总结

这篇文章将 AI 伴侣的记忆系统从"单一向量检索"升级为了完整的混合检索架构。

关键要点:

  1. 纯向量检索无法覆盖三类场景:精确事实查询、时间范围查询、专有名词匹配。混合检索架构通过组合多种检索方式解决这个问题
  2. 记忆分为 6 种类型,存储介质的选择依据是访问模式——KV 存高频热数据、D1 存需要条件查询的结构化数据、Vectorize 存需要语义检索的文本
  3. 混合检索管线四步走:查询理解(LLM 分析意图)→ 四通道并行检索 → 结果融合(去重 + 加权 + 时效性)→ Top-K 截断
  4. 四条通道各有所长:语义检索是兜底通道,结构化查询置信度最高,时间查询依赖摘要,关键词与语义互补
  5. 事件记忆的双写(Vectorize + D1)是混合架构的核心代价,用存储冗余换取检索能力的完整覆盖
  6. 写入分为同步(KV 上下文)和异步(LLM 提取 → D1/Vectorize)两个阶段,异步写入不阻塞用户体验