Skip to content
AI 工程
Embedding 模型原理:文本向量化与语义检索机制
Embedding 模型原理:文本向量化与语义检索机制 的概念、用法、示例和注意点
2026/03/1712 分钟AI 工程
文本 Embedding 模型的原理:语义向量生成、训练目标与检索机制
概述
文本 Embedding 的任务是将一个词语、一个句子或一段文本映射到一个固定维度的稠密数值向量。语义检索系统依赖这种向量:它希望查询和文档的向量距离能够反映语义距离,而不是字面重合程度。
例如,“如何办理社保卡”和“社保卡申请流程”没有共同的词,但在语义上相关。一次合适的 Embedding 转换,应当让这两个句子的向量距离明显小于查询与“如何烘焙面包”之间的距离。以下内容从文本的离散表示开始,逐步说明 Embedding 模型的训练机制、句向量的生成方式,以及向量索引、评估和混合检索在语义检索系统中的位置。
基本概念:文本向量化
机器学习模型不能直接处理字符串,文本进入模型之前必须先转换为数值。最直接的表示是 one-hot 编码:为词表中的每个词分配一个维度,该词对应的维度为 1,其余为 0。
js
const vocab = ['猫', '狗', '鱼'];
const oneHot = (word) => vocab.map((item) => (item === word ? 1 : 0));
function cosine(a, b) {
const dot = a.reduce((sum, value, i) => sum + value * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, value) => sum + value * value, 0));
const normB = Math.sqrt(b.reduce((sum, value) => sum + value * value, 0));
return dot / (normA * normB || 1);
}
console.log(oneHot('猫')); // [ 1, 0, 0 ]
console.log(cosine(oneHot('猫'), oneHot('狗'))); // 0one-hot 向量之间最大的问题是相互正交:任意两个不同词的余弦相似度都是 0。也就是说,这种表示方式无法表达“猫”和“狗”比“猫”和“银行”更接近。
把文档中所有词的 one-hot 向量相加,就得到词袋模型。词袋向量记录每个词在文档中出现了多少次,但它仍然是一个离散的稀疏向量,并且丢失了词序:“人咬狗”和“狗咬人”会得到完全相同的向量。
TF-IDF 与词法匹配的局限
TF-IDF 在词袋模型上加入权重调整:词频衡量词在文档中的重要性,逆文档频率削弱在所有文档中都出现的常见词,提高在少数文档中出现的词的区分度。
TF-IDF 依然是一个离散的稀疏向量,它解决的是“哪些词重要”的问题,没有解决“哪些词意思相近”的问题。查询“轿车维修”和文档“汽车保养”之间的 TF-IDF 余弦相似度可能很低,因为两个字符串没有重叠词。这种依赖字面词重合的匹配方式,称为词法匹配。
稠密向量的语义几何空间
稠密向量把每个词或句子映射到一个低维连续空间,例如 BERT-base 的隐藏层维度是 768。向量中的每个维度不直接对应词表中的某个词,语义关系体现在向量之间的距离上。
在合适的稠密向量空间里,“猫”和“狗”的向量方向较接近,“银行”的向量方向则明显更远。这个空间因此被称为语义几何空间:文本是否相关,转化为向量是否靠近。
工作原理:Word2Vec 与静态词向量
Word2Vec 是表示学习思想的代表性方法。它的基础是分布假说:一个词的意义可以由它周围经常出现的词来体现。Word2Vec 用两种自监督任务来学习词向量([2]):
- Skip-gram:给定中心词,预测它周围的上下文词。
- CBOW:给定上下文词的组合,预测中心词。
以句子“猫 喜欢 鱼”为例,设置窗口大小为 2,以“喜欢”作为中心词,可以构造 (“喜欢”, “猫”) 和 (“喜欢”, “鱼”) 两个训练样本。Skip-gram 的目标是仅凭“喜欢”的向量,预测出“猫”和“鱼”出现的概率。
js
// 训练信号示意(伪代码)
for (const [center, context] of corpus) {
// 希望 dot(vector(center), vector(context)) 尽量大
// 同时希望 dot(vector(center), vector(noise)) 尽量小
}训练完成后,输入侧的映射矩阵就是词向量表。相似词在足够大的语料中会拥有相似的上下文分布,因此它们的向量会被推到相邻区域。
这种表示还会出现一个有意思的现象:关系可以通过向量差表示。
js
const result = add(subtract(king, man), woman);
// 在训练好的向量空间中,与 result 最接近的词通常是一个表示“女王”的词“国王 - 男人 + 女人 ≈ 女王”是这个现象的经典表述。它不是 Word2Vec 的显式训练目标,而是训练过程在向量空间中自然形成的几何结构。
Word2Vec 的主要限制是“静态”:无论“bank”出现在“河岸”还是“银行”语境中,它都返回同一个向量。一词多义问题,需要上下文相关的表示来解决。
工作原理:BERT 与动态 Token Embedding
BERT 通过 Transformer Encoder 让每个词的表征依赖整个输入句子。Transformer Encoder 的每一层都包含多头自注意力机制和前馈网络,并带有残差连接与层归一化。自注意力计算时,每个 Token 会从其他所有 Token 位置上收集信息,因此同一个 Token 在不同的句子中会得到不同的输出向量。
BERT 的输入表示由三部分相加构成([3]):
js
// BERT 输入的构造示意
const hidden = tokenEmbedding(tokenIds)
.add(positionEmbedding(positionIds))
.add(segmentEmbedding(segmentIds));- token embedding:把 Token 映射为向量。
- position embedding:记录 Token 在句中的位置。
- segment embedding:区分两个句子。
对于一个输入句子“苹果很好吃”,BERT 输出的不是单个向量,而是一个形状为 [序列长度, 隐藏层维度] 的矩阵([3]),也就是每个 Token 各有一个向量。
同一个 Token 在不同上下文中的差异可以用两个句子说明:
- 苹果很好吃
- 苹果发布了新手机
前一句里的“苹果”会从“好吃”上收集语义信息,后一句里的“苹果”会从“发布”“手机”上收集语义信息。由于自注意力的存在,两个“苹果”在最后一层的向量已经有了明显差异。这就是动态 Token Embedding 的含义。
工作原理:从 Token 到句子向量
要得到一个句向量,需要把 [序列长度, 隐藏层维度] 的矩阵压缩成 [隐藏层维度] 的向量。常见做法有三种。
CLS 向量:BERT 在输入开头放置一个特殊的 [CLS] Token,预训练时这个位置的输出被用于下一句预测任务。因此可以把 [CLS] 位置的向量当作整个序列的聚合表示。但在句向量场景中,直接使用 [CLS] 并不总是效果最好。
Mean Pooling:对所有 Token 的向量在每个维度上取平均。
js
function meanPool(tokenVectors) {
if (tokenVectors.length === 0) return [];
const dim = tokenVectors[0].length;
const sum = new Array(dim).fill(0);
for (const v of tokenVectors) {
for (let i = 0; i < dim; i++) {
sum[i] += v[i];
}
}
return sum.map((x) => x / tokenVectors.length);
}Max Pooling:对每个维度取所有 Token 向量中的最大值,保留该维度上最强烈的激活。
js
function maxPool(tokenVectors) {
if (tokenVectors.length === 0) return [];
const dim = tokenVectors[0].length;
const result = new Array(dim).fill(-Infinity);
for (const v of tokenVectors) {
for (let i = 0; i < dim; i++) {
if (v[i] > result[i]) result[i] = v[i];
}
}
return result;
}Mean Pooling 更容易保留句子的整体语义分布,但长文本中局部关键信息容易被稀释。Max Pooling 更突出局部强特征,但会丢失词频等信息,且对极端值更敏感。
实验也表明,Pooling 策略的选择需要与具体任务一起评估。在 Sentence-BERT 的改进实验中发现,不同任务适合不同的 Pooling 和投影组合([1])。因此,不能简单认为某个 Pooling 方案在所有检索场景中都最优。
工作原理:双塔模型与对比学习
BERT 生成的 Token 向量并不天然适合检索排序。预训练任务是掩码语言模型和下一句预测,而不是“判断两个句子是否语义相似”。Sentence Embedding 需要专门的训练方法。
双塔模型是一种典型的训练架构([4])。Query 塔和 Document 塔各是一个文本编码器,把输入文本映射为句向量。两个塔可以共享参数,也可以不共享。之后用一个相似度函数计算 Query 向量和 Document 向量的得分。
js
const q = encode('如何办理社保卡');
const d = encode('社保卡申请流程');
const score = cosineSimilarity(q, d);训练目标通常是对比学习。一组训练数据包含一个查询、一个正例文档和一个或多个负例文档。模型需要让正例的相似度高于负例。
Triplet Loss 是其中一个直接的目标函数:
js
function cosineSimilarity(a, b) {
const dot = a.reduce((sum, value, i) => sum + value * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, value) => sum + value * value, 0));
const normB = Math.sqrt(b.reduce((sum, value) => sum + value * value, 0));
return dot / (normA * normB || 1);
}
function tripletLoss(anchor, positive, negative, margin = 0.3) {
const posScore = cosineSimilarity(anchor, positive);
const negScore = cosineSimilarity(anchor, negative);
return Math.max(0, negScore - posScore + margin);
}这里的 margin 是正负例之间的间隔。示例中取 0.3,实际训练中需要根据任务调整。间隔越大,模型对正负例区分度的要求越高。
实际训练中更常用基于 Batch 的对比损失。一个 Batch 内包含 (query_i, positive_i),其余的 positive_j(或 doc_j)都作为负例:
js
// 伪代码
// logits[i][j] = similarity(query_i, doc_j)
// labels[i] = i,即对角线上的 (query_i, doc_i) 是正例
// 其余 batch 内样本视为负例
loss = crossEntropy(logits / temperature, labels);负例的选择对训练效果影响很大。完全无关的负例很容易区分,模型学不到细粒度差异。困难负例是那些和查询部分相关、但不是正确答案的文档,例如同一类别下的另一个商品、同一新闻事件的不同报道。使用困难负例训练,模型需要学习更精细的语义边界。
训练数据的来源包括搜索点击日志、社区问答中的“问题-最佳回答”、同义改写句子等。双塔模型的工程价值在于:文档数量很大,但可以在离线阶段批量编码成向量并建立索引;查询阶段只需要对 Query 编码一次,再执行近似最近邻搜索。
基本用法:相似度计算与向量索引
余弦相似度与内积的选择
向量检索中最常用的相似度是余弦相似度。它只衡量向量的方向差异,不关心向量的长度。对文本 Embedding 来说,这通常是合理的:一个句子出现多次,或者一个文档更长,不应该影响它与查询的语义相似程度。
如果把向量归一化为单位向量,即 L2 范数为 1,那么余弦相似度等于内积:
text
cosine(a, b) = dot(a, b) // 当 a 和 b 都是单位向量时因此,许多 Embedding 模型在输出句向量时会直接做归一化。这样检索系统可以统一使用内积计算,减少一次除法开销。
如果模型没有对向量做归一化,内积会对向量范数较大的一侧产生偏好。范数较大的文档会获得更高的分数,而这不一定是语义上的优势。
ANN 与 HNSW:近似最近邻搜索原理
精确最近邻搜索需要把查询向量与索引中的每一个文档向量计算距离。当文档数量达到百万或亿级时,线性扫描的延迟无法接受。
近似最近邻搜索(ANN)用索引结构对向量空间进行划分或建立图连接,用少量距离计算找到“足够接近”的近邻。它牺牲少量召回率,换取查询延迟的大幅下降。
HNSW 是其中一种广泛使用的索引算法([5])。它的结构可以理解为多层图:
- 上层图包含少量节点,边连接相距较远的节点,负责快速定位到目标区域。
- 底层图包含全部节点,边更密集,负责精细寻找近邻。
- 查询从顶层开始,沿着当前层的边做贪心搜索,移动到局部最近节点,然后进入下一层重复这个过程。
HNSW 索引与 Embedding 模型无关,它本身只能存储和搜索浮点向量。索引的构建参数会影响查询速度和召回率之间的权衡,具体参数选择属于索引调优范畴。
应用:评估与混合检索
Recall@K 与 MRR 指标
离线评估语义检索,需要一组带标注的查询集合:每个查询都对应一个“相关文档集合”。
Recall@K 衡量“前 K 个结果中找回了多少相关文档”。对单个查询 q,设相关文档集合为 R_q,检索返回的前 K 个结果集合为 A_q,则:
text
Recall@K = |R_q ∩ A_q| / |R_q|如果某个查询有 5 个相关文档,前 K 个结果中命中了 3 个,那么该查询的 Recall@K 是 3/5。最后对所有查询取平均。
这里需要注意:Recall@K 只关心前 K 个结果中是否包含相关文档,不关心相关文档在其中的排序位置。如果要衡量“第一个相关结果是否靠前”,需要看 MRR。
MRR 是“倒数排名”的平均值。对每个查询,找到第一个相关文档在结果列表中的排名 r,贡献 1/r。排名从 1 开始。如果前 K 个结果中没有相关文档,该查询贡献 0。
js
function mrr(rankings) {
const reciprocalRanks = rankings.map((docs) => {
const hitIndex = docs.findIndex((doc) => doc.isRelevant);
return hitIndex === -1 ? 0 : 1 / (hitIndex + 1);
});
return (
reciprocalRanks.reduce((sum, value) => sum + value, 0) /
reciprocalRanks.length
);
}
const rankings = [
[{ id: 'd1', isRelevant: false }, { id: 'd2', isRelevant: true }],
[{ id: 'd3', isRelevant: true }, { id: 'd4', isRelevant: false }],
];
console.log(mrr(rankings)); // (1/2 + 1/1) / 2 = 0.75第一个查询的第一个相关文档出现在位置 2,贡献 1/2;第二个查询的第一个相关文档出现在位置 1,贡献 1。两个查询的平均 MRR 为 0.75。
Recall@K 更适合有多条正确答案的召回场景,MRR 更适合“第一个正确答案在哪里”的场景。
稠密检索与 BM25 的互补
稠密向量检索能识别同义改写,但对精确词、编号、专有名词的匹配能力不稳定。BM25 是经典的词法检索算法,它依赖词项重合,但能精确处理“iPhone 15”“USB-C”这类词面匹配。
实际检索系统常把两者组合,称为混合检索。混合方式之一是 Reciprocal Rank Fusion(RRF),它把多个排序结果合并成一个综合排序:
js
function rrf(rankings, smoothing = 1) {
const scores = new Map();
for (const ranking of rankings) {
ranking.forEach((docId, index) => {
const rank = index + 1; // 排名从 1 开始
const key = typeof docId === 'string' ? docId : docId.id;
const current = scores.get(key) || 0;
scores.set(key, current + 1 / (smoothing + rank));
});
}
return scores;
}
const bm25Rank = ['doc-001', 'doc-003', 'doc-002'];
const vectorRank = ['doc-002', 'doc-001', 'doc-004'];
console.log(rrf([bm25Rank, vectorRank]));RRF 不要求不同检索器的得分具有可比性,它只看排名。这种融合方式在工程上实现简单,也常被用在 RAG 系统的召回阶段。语义召回的结果可以作为上下文交给生成模型;RAG 的完整链路还包括文档分块、元数据过滤、精排等环节,这里不展开。
示例:迷你语义检索流程
下面用一个 Node.js 示例演示完整流程:用 Embedding 模型生成句向量,计算查询向量与文档向量的相似度,并返回排序结果。
js
// npm install @huggingface/transformers
import { pipeline } from '@huggingface/transformers';
// 特征提取管道,模型输出 token 向量
const extractor = await pipeline(
'feature-extraction',
'Xenova/all-MiniLM-L6-v2'
);
async function embed(text) {
const output = await extractor(text, {
pooling: 'mean', // 先对 token 向量取平均
normalize: true, // 再归一化为单位向量
});
return Array.from(output.data);
}
const documents = [
'语义检索通过向量相似度查找相关文本',
'BM25 基于词频和文档频率做精确匹配',
'HNSW 是一种近似最近邻索引算法',
];
const docVectors = await Promise.all(documents.map(embed));
function cosineSimilarity(a, b) {
const dot = a.reduce((sum, value, i) => sum + value * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, value) => sum + value * value, 0));
const normB = Math.sqrt(b.reduce((sum, value) => sum + value * value, 0));
return dot / (normA * normB || 1);
}
const query = '怎么找语义上相似的句子';
const queryVector = await embed(query);
const ranked = docVectors
.map((vec, index) => ({
document: documents[index],
score: cosineSimilarity(queryVector, vec),
}))
.sort((a, b) => b.score - a.score);
console.log(ranked);这段代码中有两个关键行为:
pooling: 'mean'把模型输出的所有 Token 向量合并成一个句向量。normalize: true把句向量归一化为单位向量,因此后面的余弦相似度可以直接用内积理解。
由于这里用的是线性扫描,结果等价于精确 KNN。真实系统中文档向量通常在离线阶段批量生成并建立 ANN 索引,查询时只编码 Query,再用索引检索。
在这个示例中,与查询“怎么找语义上相似的句子”最接近的文档大概率是“语义检索通过向量相似度查找相关文本”,因为两者都涉及“语义”和“相似”。
注意点
语义检索并不是万能的文本匹配方案。以下场景需要额外处理。
词法信息缺失。 向量模型无法保证记住所有专有名词、型号和编号。查询“iPhone 15 支持 Type-C 吗”和文档“iPhone 15 采用 USB-C 接口”在语义上相关,但模型未必能建立这种联系。BM25 在这种精确词匹配场景下往往更可靠。
条件过滤与数值约束。 查询“价格低于 3000 且电池大于 5000mAh”包含明确的数值比较,这不是向量空间能稳定表达的关系。这类条件应该放在检索前或检索后由元数据过滤完成。
否定与比较结构。 查询“不包含蓝牙的音响”会被模型同时拉向“蓝牙”和“音响”两个语义中心,导致包含蓝牙的文档也获得较高相似度。需要额外的语法分析或规则来修正。
长文档语义稀释。 使用 Mean Pooling 时,一个长文档中的关键句会被大量无关内容稀释。文档分块的长度需要和 Embedding 模型的处理能力匹配。
模型选型与领域适配。 MTEB(Massive Text Embedding Benchmark)覆盖检索、重排序、语义相似度、分类、聚类等任务([6])。一个模型在 MTEB 上的总分反映了它在通用任务上的平均能力,但不直接等于在某个垂直领域检索任务上的效果。选型时应该在目标语料上构造少量查询和相关文档,直接比较候选模型的 Recall@K 和 MRR。
如果领域语料包含大量专有词汇,通用模型可能效果有限。使用领域内数据构造正负样本,对双塔模型继续训练,是提升领域检索效果的常用手段。
参考链接
相关推荐
2026/03/21 · 12 分钟RAG 文档处理流程:解析、切片、Embedding 与索引构建
RAG 文档处理流程:解析、切片、Embedding 与索引构建 的概念、用法、示例和注意点
2026/05/30 · 9 分钟assistant-ui 架构分析:现代 AI Chat UI 的组件设计assistant-ui 架构分析:现代 AI Chat UI 的组件设计 的概念、用法、示例和注意点
2026/05/16 · 10 分钟现代 AI 产品前端架构:从 Chat UI 到 Agent Interface现代 AI 产品前端架构:从 Chat UI 到 Agent Interface 的概念、用法、示例和注意点
2026/05/14 · 10 分钟React AI 应用架构:组件设计、状态管理与流式交互React AI 应用架构:组件设计、状态管理与流式交互 的概念、用法、示例和注意点
