从文本到向量:理解Embedding
作者:程序员马丁
热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。
上一节我们聊了元数据管理——怎么给每个 chunk 贴上标签,让它从“一段裸文本”变成“一段带上下文的文本”。到这一步,每个 chunk 都带着来源、权限、位置等信息了,看起来已经很完整。
但有个根本问题还没解决:这些文本还是人类语言,计算机看不懂。
你让计算机去比较“七天无理由退货”和“买了一周的东西还能退吗”这两句话,它只会逐字比对,发现没几个字是一样的,然后告诉你 不相关。可任何一个正常人都知道,这两句话说的是同一件事。
怎么让计算机也能理解这种语义上的相似性?答案是把文本转成一组数字——向量(Vector)。这个转换过程,就叫向量化(Embedding)。
关键词检索的困境:为什么文本匹配不够用
在讲向量化之前,咱们先看看不用向量化的传统检索方式有什么问题。这样你才能理解,为什么 RAG 系统非要多这么一步。
1. 场景:电商客服知识库的检索难题
还是用前两篇的电商客服知识库场景。假设知识库里有这么一条规则:
自签收之日起 7 天内,商品未经使用且不影响二次销售的,消费者可申请七天无理由退货。
现在用户问了一句:“买了一周的东西还能退吗?”
如果用传统的关键词检索(比如 Elasticsearch 的全文搜索),系统会把用户的问题拆成关键词:“买”“一周”“东西”“退”,然后去知识库里找包含这些词的文本块。
结果呢?知识库里那条规则用的是“七天”“签收”“无理由退货”这些词,和用户问题里的“一周”“东西”“退”几乎没有重叠。关键词检索大概率找不到这条规则,或者把它 排在很后面。
但这两句话明明说的是同一件事。
2. 关键词匹配的三个致命问题
上面这个例子暴露的其实是关键词检索的通病,归纳起来有三个:
2.1 同义词问题
“一周”和“七天”是同一个意思,“退”和“退货”是同一个动作,但关键词检索不知道。它只做字面匹配,不理解语义。
再举几个例子:
| 用户的说法 | 知识库的写法 | 关键词能匹配吗 |
|---|---|---|
| 手机坏了怎么修 | 设备故障维修流程 | 不能,“手机”≠“设备”,“坏了”≠“故障” |
| 怎么把钱要回来 | 退款申请流程 | 不能,“把钱要回来”≠“退款” |
| 快递到哪了 | 物流状态查询 | 不能,“快递”≠“物流”,“到哪了”≠“状态查询” |
这些都是日常对话中很自然的表达,但关键词检索全部抓瞎。
2.2 一词多义问题
同一个词在不同语境下意思完全不同。
用户问“苹果的售后政策是什么”——这里的“苹果”是指 Apple 品牌,还是指水果?关键词检索不知道,它会 把所有包含“苹果”的文本块都返回,水果类目的退货政策和 Apple 产品的保修政策混在一起。
再比如“充值”这个词,在游戏场景是充游戏币,在话费场景是充话费,在会员场景是充会员余额。关键词检索没法区分。
2.3 上下文理解问题
有些问题需要理解整句话的意思,而不是拆成单个关键词。
用户问:“我不想要了,但已经拆了包装。”
关键词检索拆出来的是“不想要”“拆了”“包装”,可能匹配到“包装材料说明”或者“拆箱指南”这种完全不相关的内容。但实际上用户想问的是“拆封商品能不能退货”。
3. 我们需要的是“语义检索”而不是“文本匹配”
上面三个问题的根源是一样的:关键词检索只看字面,不看含义。
咱们真正需要的是一种能理解语义的检索方式——不管用户怎么表达,只要意思相近,就能匹配上。“一周”和“七天”意思一样,就应该匹配上;“苹果手机”和“水果苹果”意思不同,就不应该混在一起。
这种基于语义的检索,就是 RAG 系统的核心能力。而要实现语义检索,第一步就是把文本转成一种计算机能比较语义的格式——向量。
向量:让计算机理解语义的方式
向量这个词听起来有点数学味,但别被吓到,核心思想其实很直觉。
1. 什么是向量——用坐标来表示含义
打个比方。假设我们用一个二维坐标系来表示词语的含义,横轴代表“和购物相关的程度”,纵轴代表“和售后相关的程度”:
售后相关 ↑
|
1.0 | ● 退货(0.3, 0.9)
| ● 退款(0.2, 0.85)
0.8 |
|
0.6 |
| ● 换货(0.5, 0.7)
0.4 |
| ● 物流配送(0.6, 0.2)
0.2 |
| ● 加入购物车(0.8, 0.1)
0.0 +-----|-----|-----|-----|---→ 购物相关
0 0.2 0.4 0.6 0.8 1.0
在这个坐标系里:
- “退货”的坐标是 (0.3, 0.9),“退款”的坐标是 (0.2, 0.85)——两个点离得很近,因为它们语义相近
- “退货”和“加入购物车”的坐标差得很远——语义确实不相关
- “换货”在中间偏上的位置——它既和购物有关,也和售后有关
每个词语在坐标系里的位置,就是它的向量。向量本质上就是一组数字(坐标值),用来表示这个词语的含义。
两个词语的含义越接近,它们的向量(坐标)就越接近。这就是向量表示语义的基本原理。
2. 从二维到高维:真实的文本向量长什么样
上面的例子只用了两个维度(购物相关、售后相关) ,这当然太粗糙了。真实的语言含义非常丰富,两个维度根本不够用。
实际的 Embedding 模型会用几百甚至上千个维度来表示一段文本。每个维度捕捉文本含义的一个方面——虽然我们没法直观地说出每个维度具体代表什么,但模型通过大量训练数据学会了怎么分配这些维度。
举个例子,把“七天无理由退货”这句话送进一个 Embedding 模型,输出大概长这样:
[0.0234, -0.0156, 0.0891, -0.0423, 0.0567, -0.0312, 0.0178, -0.0645,
0.0923, -0.0089, 0.0456, -0.0234, 0.0712, -0.0567, 0.0345, -0.0198,
... (省略几百个数字)
0.0123, -0.0456, 0.0789, -0.0234]
就是一长串浮点数。如果模型的维度是 1024,那这个向量就有 1024 个数字。
你不需要理解每个数字的含义。你只需要知道:这组数字整体上编码了这段文本的语义信息。两段语义相近的文本,它们的向量(这组数字)会非常接近。
3. 语义相近 = 向量相近:这就是 Embedding 的核心思想
用一句话概括 Embedding 的核心思想:把文本映射到一个高维空间中,让语义相近的文本在空间中距离相近。
回到开头的例子:
- “七天无理由退货”的向量和“买了一周的东西还能退吗”的向量,在高维空间中距离很近
- “七天无理由退货”的向量和“物流配送时效说明”的向量,距离很远
有了向量表示,计算机就不用再做字面匹配了。它只需要比较两个向量之间的距离,距离近就是语义相关,距离远就是语义无关。
这就是 RAG 系统能做语义检索的根基。
Embedding 模型:文本到向量的转换器
知道了向量是什么,接下来的问题是:谁来做这个转换?答案是 Embedding 模型。
1. Embedding 模型在干什么
Embedding 模型的工作很纯粹:输入一段文本,输出一组浮点数向量。

你可以把它类比成一个翻译器:把人类语言“翻译”成计算机能比较的数字语言。不同的是,普通翻译器是中文翻英文,Embedding 模型是自然语言翻向量 。
几个关键特性:
- 输入长度有限制:每个模型都有最大输入 token 数(比如 512 或 8192 等等),超过会被截断。这也是为什么前面要做分块——把长文档切成短文本块,确保每个块都在模型的输入限制内
- 输出维度固定:同一个模型输出的向量维度是固定的。比如某个模型输出 1024 维,那不管你输入一个词还是一段话,输出都是 1024 个浮点数
- 同一模型内可比较:只有用同一个模型生成的向量才能互相比较。用模型 A 生成的向量和模型 B 生成的向量,没法直接算相似度
第三点非常重要。这意味着你在数据准备阶段用什么模型把 chunk 转成向量,检索阶段就必须用同一个模型把用户的 query 转成向量。换模型 = 所有向量要重新生成。
2. 主流 Embedding 模型对比
市面上的 Embedding 模型不少,选起来容易眼花。这里按照实际项目中最关心的几个维度做个对比。
2.1 模型选型的关键指标
在看具体模型之前,先搞清楚选型时要看哪些指标:
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 向量维度 | 输出向量的浮点数个数 | 维度越高,表达能力越强,但存储和计算成本也越高 |
| 最大输入 token 数 | 单次能处理的 最大文本长度 | 决定了你的 chunk 最大能有多长 |
| 中文效果 | 对中文文本的语义理解能力 | 中文场景必须关注,有些模型主要针对英文训练 |
| API 成本 | 每次调用的费用 | 大规模向量化时,成本差异会很明显 |
| 是否支持本地部署 | 能不能在自己的服务器上跑 | 涉及数据安全和隐私的场景,可能不允许数据出外网 |
2.2 主流模型横向对比
| 模型 | 提供方 | 向量维度 | 最大输入 token | 中文效果 | 部署方式 | 备注 |
|---|---|---|---|---|---|---|
| text-embedding-3-small | OpenAI | 1536 | 8191 | 中等 | 仅云端 API | 性价比高,适合英文为主的场景 |
| text-embedding-3-large | OpenAI | 3072 | 8191 | 中等 | 仅云端 API | 维度更高,效果更好,成本也更高 |
| text-embedding-v3 | 阿里通义 | 1024/768 | 8192 | 优秀 | 云端 API | 中文效果好,支持多种维度输出 |
| BGE-large-zh | BAAI(智源) | 1024 | 512 | 优秀 | 本地部署/API | 开源模型,中文效果突出 |
| BGE-M3 | BAAI(智源) | 1024 | 8192 | 优秀 | 本地部署/API | 支持多语言、多粒度,综合能力强 |
| Qwen3-Embedding-8B | 阿里通义 | 4096 | 32768 | 优秀 | 本地部署/API | 最新一代,维度高,上下文窗口大 |
| GTE-large-zh | 阿里通义 | 1024 | 8192 | 优秀 | 本地部署/API | 中文基准测试表现好 |
2.3 中文场景推荐
如果你的项目主要处理中文文本(比如中文知识库、中文客服系统),模型选型的优先级大致是:
- 需要云端 API 且预算充足:阿里通义 text-embedding-v3,中文效果好,API 稳定
- 需要云端 API 且追求性价比:通过 SiliconFlow 等平台调用 Qwen3-Embedding 或 BGE 系列,价格更低
- 需要本地部署:BGE-M3 或 Qwen3-Embedding,开源可商用,中文效果优秀
- 中英文混合场景:BGE-M3,专门为多语言设计
OpenAI 的 Embedding 模型在英文场景表现很好,但中文效果和国产模型比没有明显优势,加上 API 访问可能不稳定(你懂的),中文项目建议优先考虑国产模型。
3. 向量维度怎么选
维度是选模型时绑定的——你选了某个模型,维度就确定了(部分模型支持多种维度输出,但大多数是固定的)。
那维度高好还是低好?
打个比方:维度就像描述一个人用了多少个特征。用 2 个特征(身高、体重)描述一个人,信息很有限,很多人会“撞衫”。用 100 个特征(身高、体重、肤色、发型、口音、走路姿势……)描述,区分度就高多了。
但特征越多,记录和比较的成本也越高。
实际项目中的权衡:
| 维度范围 | 适用场景 | 存储成本(100 万条) |
|---|---|---|
| 256~512 | 简单场景,文本短、类目少 | 约 1~2 GB |
| 768~1024 | 大多数生产场景的甜蜜点 | 约 3~4 GB |
| 1536~4096 | 对精度要求极高的场景 | 约 6~16 GB |
对于大多数中文 RAG 项目,768 到 1024 维是一个比较稳妥的选择。既能保证足够的语义区分度,存储和检索成本也在可控范围内。除非你的场景对精度有极致要求(比如法律条文检索、医疗知识库等),否则不需要上 3072 或 4096 维。
相似度计算:怎么判断两个向量“像不像”
文本变成向量之后,下一步就是比较两个向量之间的相似程度。用户输入一个 query,系统把它转成向量,然后和知识库里所有 chunk 的向量逐一比较,找出最相似的几个——这就是语义检索的核心流程。
那怎么比较两个向量“像不像”?这就涉及到相似度计算。
1. 余弦相似度——最常用的度量方式
余弦相似度(Cosine Similarity)是 Embedding 检索中最常用的相似度度量方式。
不讲公式,用一个直觉的类比:把每个向量想象成从原点出发的一个箭头。余弦相似度衡量的是两个箭头的方向有多接近。
- 两个箭头方向完全一致(夹角 0°):余弦相似度 = 1.0,表示语义完全相同
- 两个箭头方向垂直(夹角 90°):余弦相似度 = 0,表示语义完全无关
- 两个箭头方向相反(夹角 180°):余弦相似度 = -1.0,表示语义完全相反
方向一致(相似度 ≈ 1.0) 方向垂直(相似度 ≈ 0) 方向相反(相似度 ≈ -1.0)
↗ A ↑ B ↗ A
↗ B A → ↙ B
为什么用方向而不是距离?因为 Embedding 向量的长度(模)可能不一样,但我们关心的是语义方向。两段文本可能一长一短,向量的模不同,但只要语义方向一致,余弦相似度就高。
1.1 余弦相似度的计算逻辑
虽然不需要记公式,但了解计算逻辑有助于理解后面的代码。余弦相似度的计算分三步:
- 算两个向量的点积(对应位置的数字相乘,然后全部加起来)
- 算每个向量的模(每个数字的平方加起来,再开根号)
- 点积除以两个模的乘积
就这么简单。
2. Java 代码示例:手动计算余弦相似度
public class CosineSimilarity {
/**
* 计算两个向量的余弦相似度
*
* @param vectorA 向量 A
* @param vectorB 向量 B
* @return 余弦相似度,范围 [-1.0, 1.0]
*/
public static double calculate(double[] vectorA, double[] vectorB) {
if (vectorA.length != vectorB.length) {
throw new IllegalArgumentException(
"两个向量的维度必须相同,vectorA: " + vectorA.length + ", vectorB: " + vectorB.length
);
}
double dotProduct = 0.0; // 点积
double normA = 0.0; // 向量 A 的模
double normB = 0.0; // 向量 B 的模
for (int i = 0; i < vectorA.length; i++) {
dotProduct += vectorA[i] * vectorB[i];
normA += vectorA[i] * vectorA[i];
normB += vectorB[i] * vectorB[i];
}
normA = Math.sqrt(normA);
normB = Math.sqrt(normB);
// 避免除以零
if (normA == 0 || normB == 0) {
return 0.0;
}
return dotProduct / (normA * normB);
}
public static void main(String[] args) {
// 模拟三个文本的向量(实际维度会高得多,这里用 5 维演示)
double[] returnPolicy = {0.8, 0.1, 0.9, 0.2, 0.7}; // "七天无理由退货"
double[] returnQuery = {0.75, 0.15, 0.85, 0.25, 0.65}; // "