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

1. 从记忆到检索

在前面的章节中,我们已经多次接触到向量化。介绍 RAG 流程时,我们提到过向量化与索引;讲解记忆调度时,又把向量数据库类比为硬盘;到了 LangChain 实战,则直接使用 VectorStoreRetriever 检索记忆。这些内容都指向两个尚未展开的问题:为什么文本要转换成向量,向量检索又是如何工作的?

这一篇我们就来补上这部分知识。弄清向量化与语义检索之后,RAG 中检索环节的实现逻辑也就更容易理解了。

如果把 RAG 理解为一套先查询、再回答的流程,那么向量化就承担了搜索引擎的作用。没有它,AI 女友的长期记忆即使已经存入仓库,也很难在需要时准确找回来。

2. 为什么传统搜索不够用

在讲向量化之前,我们可以先看看传统关键词搜索在 AI 伴侣场景中会遇到什么问题。

场景:用户说“我想去上次那个很浪漫的地方”,记忆库里存着这样一条记录:“2024-03-14,和用户去了 Blue Note 爵士酒吧,烛光晚餐,气氛非常好。”

如果使用关键词 浪漫 搜索,由于记录中没有出现这两个字,系统就无法命中它。但对人来说,“烛光晚餐”和“气氛非常好”都很容易与“浪漫”联系起来。

关键词搜索的局限就在这里:它只能匹配字面,无法理解语义

在 AI 伴侣场景中,同一个意思往往会有很多种表达方式:

  • “我心情不好”、“今天好丧”与“感觉整个人都垮了”
  • “想吃上次那个”与“你还记得我爱吃什么吗”
  • “我们第一次见面”与“最初认识的时候”

这些表述在字面上差异很大,语义却高度相似。因此,我们需要的是能够理解意思、而不只是匹配字符的搜索方式,也就是语义检索。

3. 什么是 Embedding

Embedding(嵌入)是把文本转换成一组数字,也就是向量的过程。这组数字并非随意生成,而是由专门训练的神经网络模型计算得到。其中最重要的特征是:语义相近的文本,会被映射到向量空间中相近的位置

为了更直观地理解这个过程,我们先用一个简化的二维向量表示几段文本:

文本向量(简化)含义
“烛光晚餐,气氛很好”[0.82, 0.75]浪漫 + 积极
“想去浪漫的地方”[0.80, 0.73]浪漫 + 积极
“今天加班到12点”[0.15, 0.20]工作 + 疲惫
“好累啊想休息”[0.18, 0.25]工作 + 疲惫

从这组简化数据可以看出,前两条文本的向量很接近,后两条也是如此,但两组之间的差异明显更大。Embedding 做的事情,就是把意思相近转换成可以计算的数字相近

正在加载图示...

真实的 Embedding 模型当然不会只输出两个维度。常见的向量有 768 维(如 bge-base)和 1536 维(如 OpenAI text-embedding-3-small)。维度越高,对语义的表达通常越精细,但存储和计算成本也会随之增加。

从这个角度看,Embedding 可以理解为:文本 \to 高维空间中的一个点。语义越相似,这些点在空间中的距离就越近。

4. 衡量语义相似度

文本被转换成向量之后,判断两段文本是否相似,就可以转化为判断两个向量是否相近。为了完成这个判断,我们需要选择合适的距离度量方式。

余弦相似度(Cosine Similarity)

余弦相似度是最常用的度量方式。它关注两个向量的方向是否一致,而不关心向量本身的长度。

cosine_sim(A,B)=ABA×B\text{cosine\_sim}(A, B) = \frac{A \cdot B}{\|A\| \times \|B\|}

它的取值范围是 [1,1][-1, 1],数值越接近 1,表示两个向量越相似。由于计算时不依赖向量长度,所以即使一边是一句话、另一边是一段话,只要语义相同,也能得到较高的相似度。

欧氏距离(Euclidean Distance)

欧氏距离衡量的是两个向量在空间中的直线距离。

d(A,B)=i=1n(AiBi)2d(A, B) = \sqrt{\sum_{i=1}^{n}(A_i - B_i)^2}

欧氏距离越小,表示两个向量越相似。不过,它的结果会受向量长度影响;长文本的向量模通常更大,因此可能引入偏差。

在实际的 AI 伴侣项目中,通常会优先使用余弦相似度。用户的表达有长有短,我们关心的是意思是否相近,而不是字数是否相当。余弦相似度只关注方向、忽略长度,正好适合这类检索需求。

5. 记忆的存储与检索

接下来,我们把 Embedding 和相似度搜索连接起来,看看 AI 伴侣项目中的一段记忆,是如何被提取、存储,又如何在后续对话中被找回的。

正在加载图示...

第一步:记忆产生与切分

一轮对话结束后,后台异步任务会分析其中的内容,并提取出值得长期保存的记忆片段

index.ts
01
// 原始对话
02
const conversation = `
03
用户: 今天我生日,我们去了 Blue Note 听爵士
04
AI: 生日快乐!你喜欢今晚的演出吗?
05
用户: 超喜欢!以后每年生日都来这里
06
`
07
08
// 提取的记忆片段(由 LLM 或规则引擎完成)
09
const memories = [
10
"用户的生日活动:去 Blue Note 听爵士乐",
11
"用户希望每年生日都去 Blue Note",
12
"用户喜欢爵士乐演出"
13
]

整段对话不会被原封不动地直接存入记忆库,而是要先经过切分和提炼。短文本的语义更集中,可以让检索结果更精准,同时也能减少不必要的存储开销。

第二步:向量化

接着,系统会对每个记忆片段调用 Embedding 模型,得到与之对应的向量。

index.ts
1
// 调用 Embedding API
2
const response = await ai.run(
3
'@cf/baai/bge-base-en-v1.5',
4
{ text: ["用户的生日活动:去 Blue Note 听爵士乐"] }
5
)
6
7
// 返回结果:一个 768 维的浮点数数组
8
// [0.023, -0.156, 0.891, ..., 0.034]

第三步:存入向量数据库

向量生成后,需要连同元数据一起写入向量数据库。这些元数据包括原始文本、时间戳和用户 ID 等信息,后续可以用它们进行过滤或展示。

index.ts
01
// 写入 CloudFlare Vectorize
02
await vectorIndex.upsert([
03
{
04
id: "mem_20240314_001",
05
values: embeddingVector, // 768维向量
06
metadata: {
07
text: "用户的生日活动:去 Blue Note 听爵士乐",
08
userId: "user_123",
09
timestamp: "2024-03-14T20:30:00Z",
10
type: "event",
11
importance: 0.85 // 重要性评分
12
}
13
}
14
])

第四步:检索召回

当用户在之后的某次对话中说“我们去上次过生日的地方吧”时,系统需要找回与这句话相关的记忆。

index.ts
01
// 1. 将用户的查询向量化
02
const queryEmbedding = await ai.run(
03
'@cf/baai/bge-base-en-v1.5',
04
{ text: ["上次过生日的地方"] }
05
)
06
07
// 2. 在向量数据库中检索最相似的记忆
08
const results = await vectorIndex.query(
09
queryEmbedding.data[0],
10
{
11
topK: 5, // 返回最相似的5条
12
filter: { userId: "user_123" }, // 只搜这个用户的记忆
13
returnMetadata: true
14
}
15
)
16
17
// 3. 返回结果(按相似度排序)
18
// [
19
// { id: "mem_20240314_001", score: 0.94,
20
// metadata: { text: "用户的生日活动:去 Blue Note 听爵士乐" } },
21
// { id: "mem_20240314_002", score: 0.87,
22
// metadata: { text: "用户希望每年生日都去 Blue Note" } },
23
// ...
24
// ]

返回结果中的 score: 0.94 就是余弦相似度。用户说的是“上次过生日的地方”,记忆里存的却是“用户的生日活动:去 Blue Note 听爵士乐”。虽然两者的字面差异很大,语义却高度相似,所以会得到较高的分数。

第五步:注入上下文

完成检索后,这些记忆片段会被拼接到发给 LLM 的 Prompt 中。

index.ts
1
const systemPrompt = `
2
你是小薇,用户的 AI 女友。
3
4
【相关记忆】
5
- 用户的生日活动:去 Blue Note 听爵士乐(2024-03-14)
6
- 用户希望每年生日都去 Blue Note(2024-03-14)
7
8
请基于以上记忆,自然地回应用户。
9
`

有了这些上下文,AI 就可以回复:“你说的是 Blue Note 吧!上次生日在那里听爵士超棒的,我也好想再去~”

至此,语义检索的过程就完整了:存储时对记忆进行向量化,检索时计算相似度,找到相关记忆后,再将它们注入上下文。

6. CloudFlare Vectorize 的作用

这个项目使用 CloudFlare Vectorize 作为向量数据库。Vectorize 是 CloudFlare 提供的边缘向量数据库服务,可以与 Workers AI 和 D1 直接配合使用。

选择 Vectorize 的原因

维度CloudFlare VectorizePinecone / Milvus
部署位置边缘节点,离用户更近独立集群,需要跨网络请求
与 Workers AI 集成原生集成,同一生态需要额外的网络调用
冷启动延迟极低(边缘计算)较高(需要唤醒实例)
成本模型按用量计费,起步免费按实例时长计费,起步有成本
适用规模中小规模(百万级向量)大规模(十亿级向量)

对于 AI 伴侣的用户规模,Vectorize 的边缘部署和低延迟更符合项目需求。用户发出一条消息后,记忆检索的耗时需要控制在 50ms 以内。边缘节点与用户的物理距离更近,这一点对降低检索延迟很重要。

Vectorize 的基本用法

index.ts
01
// 创建索引(通常在项目初始化时执行一次)
02
// wrangler vectorize create memory-index
03
// --dimensions=768 --metric=cosine
04
05
// 在 Worker 中使用
06
export default {
07
async fetch(request, env) {
08
const { text, userId } = await request.json()
09
10
// 1. 生成 Embedding
11
const embedding = await env.AI.run(
12
'@cf/baai/bge-base-en-v1.5',
13
{ text: [text] }
14
)
15
16
// 2. 检索相似记忆
17
const matches = await env.VECTORIZE.query(
18
embedding.data[0],
19
{ topK: 5, filter: { userId } }
20
)
21
22
return Response.json(matches)
23
}
24
}

在这段代码中,生成 Embedding 和检索向量都在 CloudFlare 的边缘网络内完成,不需要跨云通信。Embedding 模型(Workers AI)与向量数据库(Vectorize)位于同一个数据中心,因此可以减少额外的网络延迟。

7. 优化检索质量

接入向量检索只是第一步。在实际工程中,记忆的切分方式、检索范围和结果排序,都会直接影响 AI 女友的记忆质量。

记忆片段的粒度控制

如果记忆粒度太粗,例如把整段对话存为一条,检索时的语义就不够集中,容易召回无关内容。如果粒度太细,每句话都单独存储,又会丢失上下文,导致检索到的片段缺乏连贯性。

项目中通常可以按一个完整事件一个明确事实进行切分:

index.ts
1
// ❌ 太粗:整段对话作为一条记忆
2
"2024-03-14的完整对话记录(500字)"
3
4
// ❌ 太细:每句话一条记忆
5
"用户说了生日快乐"
6
7
// ✅ 合适:一个事件/事实一条记忆
8
"用户生日当天去了 Blue Note 爵士酒吧庆祝"
9
"用户表示以后每年生日都想去 Blue Note"

元数据过滤 + 向量检索的组合

只依赖向量相似度并不总是足够。例如,用户提到“昨天晚上的事”时,系统应该先根据时间缩小检索范围,再在这个范围内进行语义匹配。

index.ts
1
const results = await vectorIndex.query(queryVector, {
2
topK: 5,
3
filter: {
4
userId: "user_123",
5
// 先用元数据缩小范围
6
timestamp: { $gte: "2024-03-13T00:00:00Z" }
7
}
8
})

这种结构化过滤 + 语义排序的混合检索策略,在实际项目中通常比纯向量搜索更有效。

重排序(Reranking)

向量检索返回的 Top-K 结果不一定已经按最合理的顺序排列。我们可以使用轻量的 Cross-Encoder 模型对候选结果做二次排序,进一步提高准确度。这一步只需要对 5-10 条候选结果进行推理,成本较低,但对排序质量的改善很明显。

8. 总结

回到文章开头的问题,AI 伴侣的长期记忆之所以能够被找回来,关键就在于 Embedding 与语义检索。整个过程可以归纳为以下几点:

  1. 传统关键词搜索无法处理语义相似、字面不同的表达,AI 伴侣场景因此需要使用语义检索。
  2. Embedding 会将文本映射为高维向量,语义相近的文本在向量空间中距离更近。
  3. 余弦相似度是衡量语义相似性的首选指标,它只关注向量方向,不受向量长度影响。
  4. 完整的处理过程是:记忆产生 \to 切分提炼 \to 向量化 \to 存入向量库 \to 检索召回 \to 注入上下文。
  5. CloudFlare Vectorize 的边缘部署特性,为实时记忆检索提供了低延迟保障。
  6. 工程上还需要控制记忆的切分粒度、通过元数据预先过滤,并使用 Reranking 进行精排。

下一篇我们会继续设计情绪状态机,看看 AI 女友的记仇害羞吃醋是如何实现的。