向量化与语义检索
要点
- 在前面的章节中,我们反复提到一个概念:向量化
- 在讲向量化之前,我们先看看传统的关键词搜索为什么搞不定 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 Vectorize | Pinecone / 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 伴侣的"长期记忆"是怎么存进去、又怎么找出来的。
关键要点:
- 传统关键词搜索无法处理语义相似但字面不同的表达,AI 伴侣场景必须用语义检索
- Embedding 将文本映射为高维向量,语义相近的文本在向量空间中距离更近
- 余弦相似度是衡量语义相似性的首选指标,它只关注方向不受长度影响
- 完整流程:记忆产生 →\to→ 切分提炼 →\to→ 向量化 →\to→ 存入向量库 →\to→ 检索召回 →\to→ 注入上下文
- CloudFlare Vectorize 的边缘部署特性为实时检索提供了低延迟保障
- 工程优化的三板斧:控制切分粒度、元数据预过滤、Reranking 精排
下一篇我们将进入情绪状态机的设计,讲清楚 AI 女友是如何"记仇"、"害羞"和"吃醋"的。