向量化与语义检索

要点

  • 在前面的章节中,我们反复提到一个概念:向量化
  • 在讲向量化之前,我们先看看传统的关键词搜索为什么搞不定 AI 伴侣场景
  • Embedding(嵌入)是将文本转换为一组数字(向量)的过程
  • 既然 Embedding 把文本变成了向量,那"两段文本是否相似"就变成了"两个向量是否相近"
  • 现在我们把 Embedding 和相似度搜索串起来,看看在 AI 伴侣项目中,一段记忆是怎么从"产生"到"被找回"的

内容

1. 从「记忆」到「检索」的关键一步

在前面的章节中,我们反复提到一个概念:向量化。第一篇文章介绍 RAG 流程时提到了「向量化与索引」,第二篇讲内存调度时将向量数据库类比为「硬盘」,第三篇 LangChain 实战中用 VectorStoreRetriever 检索记忆。但我们一直没有深入解释:为什么文本要变成向量?向量检索到底是怎么工作的?

这篇文章将彻底讲清楚这个问题。理解了向量化与语义检索,你才能真正看懂 RAG 的核心引擎。

打一个比方:如果 RAG 是一套「先查再答」的流程,那向量化就是这套流程里的搜索引擎。没有它,AI 女友的长期记忆就是一堆躺在仓库里的纸条,能存但找不到。

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

在讲向量化之前,我们先看看传统的关键词搜索为什么搞不定 AI 伴侣场景。

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

如果用关键词搜索"浪漫",这条记录里根本没有"浪漫"这两个字,搜不到。但你一看就知道,"烛光晚餐"、"气氛非常好"跟"浪漫"是同一个意思。

这就是关键词搜索的根本缺陷:它只能匹配字面,不能理解语义

而 AI 伴侣场景里,用户的表达方式千变万化:

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

这些表述字面上差异巨大,但语义高度相似。我们需要一种能理解"意思"而不仅仅匹配"字符"的搜索方式。这就是语义检索。

3. 什么是 Embedding

Embedding(嵌入)是将文本转换为一组数字(向量)的过程。这组数字不是随意生成的,而是由专门训练的神经网络模型计算出来的,语义相近的文本会被映射到空间中相近的位置

举个直觉性的例子。假设我们用一个极简的二维向量来表示几段文本:

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

你会发现,前两条的向量非常接近,后两条也非常接近,但前两条和后两条之间的差异很大。这就是 Embedding 的魔力:它把"意思相近"转化成了"数字相近"

Drawing canvas 实际的 Embedding 模型输出的维度远不止两个。常见的维度有 768 维(如 bge-base)和 1536 维(如 OpenAI text-embedding-3-small)。维度越高,语义表达越精细,但存储和计算成本也越大。

Embedding 的本质:文本 →\to→ 高维空间中的一个点。语义相似的文本,在这个空间中距离更近。

4. 如何衡量"语义相似"

既然 Embedding 把文本变成了向量,那"两段文本是否相似"就变成了"两个向量是否相近"。这就引出了距离度量的问题。

余弦相似度(Cosine Similarity)

这是最常用的度量方式。它衡量的是两个向量的方向是否一致,而不关心长度。

cosine_sim(A,B)=A⋅B∥A∥×∥B∥\text{cosine\_sim}(A, B) = \frac{A \cdot B}{\|A\| \times \|B\|}cosine_sim(A,B)=∥A∥×∥B∥A⋅B​
  • 值域:[−1,1][-1, 1][−1,1],越接近 1 表示越相似
  • 优势:不受文本长度影响。一句话和一段话,只要语义相同,余弦相似度就高

欧氏距离(Euclidean Distance)

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

d(A,B)=∑i=1n(Ai−Bi)2d(A, B) = \sqrt{\sum_{i=1}^{n}(A_i - B_i)^2}d(A,B)=∑i=1n​(Ai​−Bi​)2​
  • 值越小,表示越相似
  • 劣势:受向量长度影响,长文本的向量通常模更大,会引入偏差

在实践中,AI 伴侣场景几乎都用余弦相似度。 原因很简单:用户的表达有长有短,我们关心的是"意思是否相近",而不是"字数是否相当"。余弦相似度恰好只关注方向,忽略长度。

5. 完整的存储与检索流程

现在我们把 Embedding 和相似度搜索串起来,看看在 AI 伴侣项目中,一段记忆是怎么从"产生"到"被找回"的。

Drawing canvas 第一步:记忆产生与切分

当一轮对话结束后,后台异步任务会分析这段对话,提取出值得长期保存的"记忆片段"。

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

注意,我们不是把整段对话原封不动地存进去,而是先切分和提炼。这样做有两个好处:检索更精准(短文本的语义更聚焦),存储更节省。

第二步:向量化

对每个记忆片段调用 Embedding 模型,得到对应的向量。

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

第三步:存入向量数据库

将向量连同元数据一起写入向量数据库。元数据包含原始文本、时间戳、用户 ID 等信息,方便后续过滤和展示。

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

第四步:检索召回

当用户在未来的某一天说"我们去上次过生日的地方吧",系统需要找回相关记忆。

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

注意 score: 0.94,这就是余弦相似度。尽管用户说的是"上次过生日的地方",而记忆里存的是"用户的生日活动:去 Blue Note 听爵士乐",两者字面差异很大,但语义高度相似,所以得分很高。

第五步:注入上下文

检索到的记忆片段会被拼接进发给 LLM 的 Prompt 中。

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

于是 AI 可以回复:"你说的是 Blue Note 吧!上次生日在那里听爵士超棒的,我也好想再去~"

这就是语义检索的完整闭环:存储时向量化,检索时算相似度,找到后注入上下文。

6. CloudFlare Vectorize 在项目中的角色

我们的技术选型中,向量数据库使用的是 CloudFlare Vectorize。它是 CloudFlare 提供的边缘向量数据库服务,天然与 Workers AI 和 D1 协作。

为什么选择 Vectorize 而不是 Pinecone / Milvus

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

对于 AI 伴侣这种用户量级的场景,Vectorize 的边缘部署和低延迟是核心优势。用户发一句消息,检索记忆的耗时需要控制在 50ms 以内,边缘节点的物理距离优势在这里非常关键。

Vectorize 的基本使用

// index.ts
// 创建索引(通常在项目初始化时执行一次)
 
// wrangler vectorize create memory-index
 
//   --dimensions=768 --metric=cosine
 
// 在 Worker 中使用
 
export default {
 
  async fetch(request, env) {
 
    const { text, userId } = await request.json()
 
    // 1. 生成 Embedding
 
    const embedding = await env.AI.run(
 
      '@cf/baai/bge-base-en-v1.5',
 
      { text: [text] }
 
    )
 
    // 2. 检索相似记忆
 
    const matches = await env.VECTORIZE.query(
 
      embedding.data[0],
 
      { topK: 5, filter: { userId } }
 
    )
 
    return Response.json(matches)
 
  }
 
}

整个流程在 CloudFlare 的边缘网络内完成,不需要跨云通信。Embedding 模型(Workers AI)和向量数据库(Vectorize)在同一个数据中心里,这是延迟优势的来源。

7. 检索质量优化策略

向量检索不是"接上就完事",实际工程中有几个关键的优化点直接影响 AI 女友的记忆质量。

记忆片段的粒度控制

粒度太粗(整段对话存一条),检索时语义不聚焦,容易召回无关内容。粒度太细(每句话存一条),则丢失上下文,检索到的片段缺乏连贯性。

实践中,推荐按"一个完整事件"或"一个明确事实"为单位切分:

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

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

纯靠向量相似度有时候不够。比如用户说"昨天晚上的事",你需要先用时间过滤缩小范围,再在范围内做语义匹配。

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

这种"结构化过滤 + 语义排序"的混合检索策略,在实际项目中效果远好于纯向量搜索。

重排序(Reranking)

向量检索返回的 Top-K 结果并不总是完美排序的。可以用一个轻量的 Cross-Encoder 模型对候选结果做二次排序,显著提升精度。这一步的成本很低(只需要对 5-10 条候选做推理),但效果提升明显。

8. 总结

这篇文章解决了一个核心问题:AI 伴侣的"长期记忆"是怎么存进去、又怎么找出来的

关键要点:

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

下一篇我们将进入情绪状态机的设计,讲清楚 AI 女友是如何"记仇"、"害羞"和"吃醋"的。