AI 电子伴侣
创建时间: 2026-03-24最后更新: 2026-08-04

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直接读取
正在加载图示...

这里选择存储介质时,判断依据是访问模式,而不是单纯看数据量。

CloudFlare KV 负责热数据。最近 N 轮对话、当前情绪状态、亲密度数值等内容体量不大,却几乎每次请求都要读取,很适合放在 KV 中。KV 是全球分布的键值存储,读取延迟在个位数毫秒,但它只能按照 key 精确读取,不支持复杂查询。

CloudFlare D1 负责结构化数据。用户画像中的姓名、生日和职业,对话摘要中的时间段索引,键值形式的偏好标签,以及用于关键词检索的记忆条目,都需要条件查询能力。D1 是边缘 SQLite 数据库,支持完整的 SQL,WHERELIKEORDER BYGROUP BY 都可以使用,但它无法计算语义相似度。

CloudFlare Vectorize 负责语义数据。事件记忆、情感片段和长文本记忆更关心“意思是否相近”,适合放入向量数据库。这一层的具体实现已经在第四篇文章中讲过。它的优势是理解语义,短板则是精确匹配和条件过滤。

这里还有一个很重要的架构选择:事件记忆要同时写入 D1 和 Vectorize。同一条数据保存两份,Vectorize 用来做语义检索,D1 用来做关键词检索。混合架构因此会增加存储冗余,但换来的是更完整的检索能力。

3. 混合检索流程

收到用户消息后,系统不会只选择一条检索路径,而是先判断这条消息需要哪些记忆,再并行执行相应的检索任务。等各通道返回结果后,系统会把它们合并成一份可供模型使用的记忆列表。

正在加载图示...

第一步:查询理解

查询理解是整个流程的入口。LLM 会分析用户消息并生成一份查询计划,这份计划决定语义检索、结构化查询、时间范围查询和关键词检索中,哪些通道需要被激活。

下面几条消息可以帮助我们理解这个判断过程:

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

实际使用时,大部分查询会同时激活多个通道。这并不意味着延迟会按通道数量累加,因为各通道可以并行执行,总延迟取决于其中最慢的一个,通常在 50-150ms 内。

查询理解本身需要调用一次 LLM,大约增加 100-300ms 延迟。如果省略这一步,直接让每条消息都查询所有通道,就会产生大量没有必要的数据库查询和向量检索。这里需要权衡延迟和资源消耗:当用户规模增大后,查询理解节省下来的后端资源,通常会超过它自身带来的延迟成本。

第二步:多通道并行检索

查询计划确定后,所有被激活的通道同时开始执行,彼此之间不需要等待。

第三步:结果融合

当各通道都返回结果后,系统会统一去重、加权和排序,把来源不同的记忆合并到同一份列表中。具体的融合规则会在第五节展开。

第四步:截断输出

融合后的结果按照分数排序,只取 Top-K 作为最终输出,通常保留 5-8 条。截断并不是为了少查数据,而是为了保护模型的上下文:输入太多关联较弱的记忆,会稀释真正重要的信息,反而降低回复质量。

4. 四条检索通道

四条检索通道分别解决不同类型的问题。它们没有谁能够替代谁,组合起来才构成完整的记忆检索能力。

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

语义检索可以看作兜底通道。用户消息通常都会带有一些语义信息,因此它几乎每次都会被激活;另外三个通道则负责精准补充,在特定场景中给出语义检索无法提供的确定结果。

结构化查询的置信度天然更高。用户问“我的生日是几号”时,从画像表查到的 birthday: "03-14" 是一个明确事实,不存在相似度高低的问题。因此在结果融合阶段,结构化查询的结果应该获得更高权重。

时间范围查询依赖对话摘要,而不是直接返回原始消息。假设用户一周内产生了 200 条对话,把这些原始内容全部放进模型上下文显然不可行。系统需要在后台异步生成对话摘要,例如每 N 轮对话压缩成一段摘要。之后再遇到“上周”或“去年夏天”一类问题时,查询的是带时间范围的摘要,而不是完整的原始对话。

关键词检索与语义检索经常会返回互补的信息。用户提到《三体》时,关键词检索可以准确命中包含“三体”二字的记忆;语义检索则可能找到“用户喜欢刘慈欣的科幻作品”这类没有出现书名、但含义相关的内容。把两类结果放在一起,才能补全这段上下文。

5. 结果融合

四条通道返回的结果不能直接拼接。系统还需要处理重复内容、不同来源的分数尺度,以及记忆的新旧程度,最后再决定哪些内容进入模型上下文。

正在加载图示...

去重

同一条记忆可能同时被语义检索和关键词检索召回。例如,一条包含“三体”的记忆既会因为字面匹配出现在关键词结果中,也可能因为与“科幻小说”语义相近而被向量检索命中。如果直接把两条结果都交给 LLM,相同信息就会重复占用上下文窗口。

去重时,可以计算任意两条结果的文本相似度。相似度超过 85% 时,将它们视为同一条记忆,只保留分数更高的结果。这里不需要再次执行向量计算,因为我们要找的是“几乎相同的文本”,而不是“意思相近的文本”。字符级 Jaccard 系数或编辑距离已经足够。

来源加权

不同通道的原始分数不能直接比较。语义检索返回的是 0-1 之间的余弦相似度,结构化查询表达的则是命中或未命中。为了把这些结果放到同一尺度中排序,需要为每种来源分配权重:

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

最终分数的计算方式是 原始分数 × 来源权重。结构化查询获得最高权重,是因为它返回的是确定事实,出错概率通常低于其他通道。

时效性加成

越接近当前时间的记忆,通常越可能与这轮对话相关。系统可以为 24 小时内产生的记忆额外增加 10%,为 7 天内的记忆增加 5%,更早的记忆不做加成。

完成去重、来源加权和时效性加成后,所有记忆就进入了同一个分数体系。系统按最终分数降序排列,再取 Top-K 输出。

6. 记忆写入

检索解决的是如何读取记忆,而系统最终能查到什么,取决于每轮对话结束后写入了哪些内容。混合记忆架构的写入过程分为同步和异步两个部分。

正在加载图示...

同步阶段只更新 KV 中的短期上下文。系统把最近 N 轮对话写入 KV,并设置 24 小时过期时间。下一轮对话会立即读取这份上下文,因此这一步需要在当前处理链路内完成。

长期记忆的提取和落库放在后台异步执行。系统把任务交给消息队列,再由 LLM 从对话中提取结构化信息,写入 D1 和 Vectorize。这样既能完成记忆沉淀,也不会阻塞用户发送下一条消息。把同步与异步两部分合在一起看,写入过程一共包含以下四项操作。

事实提取写入 D1 画像表。 LLM 识别对话中的结构化事实,例如把“我下周生日”转换为 birthday 字段更新,再以键值对形式写入用户画像表。这些内容会被结构化查询通道使用。

事件提取同时写入 Vectorize 和 D1。 LLM 识别值得长期保留的事件与情感,例如“用户今天被裁员了,心情很低落”。这段内容向量化后写入 Vectorize,供语义检索使用;原文还会写入 D1 的 memory_entries 表,供关键词检索使用。这是整个架构中唯一的双写点。同一条数据保存两份,是获得混合检索能力必须承担的存储代价。

摘要写入 D1 摘要表。 每 N 轮对话,例如每 20 轮,LLM 会把这一段对话压缩成摘要,写入 D1 摘要表,并记录对应的时间范围。之后的时间范围查询会从这些摘要中寻找结果。

上下文更新写入 KV。 系统把最新对话追加到短期上下文缓存中,供下一轮对话直接读取。

事实提取与事件提取是两个独立的 LLM 调用,它们的目标和写入方式完全不同:

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

覆盖写入与追加写入的区别会直接影响记忆的含义。用户说“我换工作了,现在在字节跳动”时,事实提取会覆盖 job 字段中的旧值,确保画像表反映当前状态;事件提取则会追加一条“用户从上一家公司离职,入职字节跳动”的新记忆,用来保留完整的历史轨迹。

7. 总结

单一向量检索擅长理解语义,却无法独立处理精确事实、时间范围和专有名词。混合检索架构补上了这些能力:KV 保存需要高频读取的热数据,D1 保存可按条件查询的结构化数据,Vectorize 保存需要语义检索的文本。六类记忆因此可以按照各自的访问模式,进入合适的存储层。

读取记忆时,LLM 先生成查询计划,再并行激活语义、结构化、时间范围和关键词通道。返回结果依次经过 85% 阈值去重、来源加权、时效性加成与 Top-K 截断。其中,语义检索负责兜底,结构化查询拥有最高置信度,时间范围查询依赖后台摘要,关键词检索则与语义检索相互补充。

写入记忆时,短期上下文同步进入 KV,事实、事件与摘要由后台异步提取。事件记忆会同时写入 D1 和 Vectorize,这是整个架构唯一的双写点。它增加了存储冗余,却让同一条记忆既能按字面检索,也能按语义召回,从而补齐混合记忆系统的检索能力。