📖 本章学习目标
- 用自己的话解释 Embedding 如何将文本语义映射到高维向量空间
- 使用 TypeScript 调用 Embedding API 生成文本向量
- 编写余弦相似度函数,计算两段文本的语义相似度
- 根据业务需求,从维度、成本、语言支持等维度选型 Embedding 模型
在《第1章:RAG 是什么,为什么需要它》中,我们建立了对 RAG 的整体认知:检索 → 增强 → 生成。但你可能会问------"检索"是怎么知道一段文字和我的问题是相关的?计算机如何判断"语义相似"而不是简单的关键词匹配?
答案就是本章的主角:文本嵌入(Embedding)。它是整个 RAG 系统的基石,检索质量的上限由它决定。如果 Embedding 无法准确捕捉语义,后续的向量搜索、文档召回都会失效,再强大的 LLM 也无能为力。
一、Embedding 是什么
1. 从"字符串"到"向量"
计算机处理文本时,看到的是字符串,但字符串之间没有"距离"这个概念。"苹果很好吃"和"这个水果味道不错",在字符层面几乎没有重叠,但语义上显然高度相关。传统的关键词匹配算法(如 TF-IDF、BM25)只能基于词汇重叠度来判断相关性,对于这种词汇不同但含义相近的情况无能为力。
Embedding 做的事情就是将一段文本映射为高维空间中的一个点(向量)。映射的核心规则是:语义相近的文本,在向量空间中距离也相近;语义无关的文本,距离则较远。
这个映射过程不是手工设计的规则,而是通过深度学习模型自动学习得到的。Embedding 模型在海量文本语料上进行训练,学习到的是一种"语义表示"------它将文本中的词汇、语法、语境等信息压缩成一个固定长度的向量。
2. 向量
向量 (Vector)是数学上的概念,它是一个有序的数值数组,在数学中表示空间中的点或方向。在机器学习领域,向量是数据的核心表示形式,通过 Embedding 模型,一段文本会被转换成一个由浮点数组成的高维向量,这个向量就是该文本的语义指纹。向量的每个维度编码了文本某一方面的特征(如主题、情感、语法结构等),虽然单个维度对人类不可解释,但向量之间的距离关系能精准反映文本的语义相似度。
举个例子,假设我们用二维向量来表示动物:
arduino
"猫" → [0.8, 0.2]
"狗" → [0.75, 0.25]
"鱼" → [0.1, 0.9]
在这个简化的二维空间中,第一个维度可能代表"陆生程度",第二个维度代表"水生程度"。猫和狗在第一个维度上得分高(都是陆生动物),所以它们的向量接近;鱼在第二个维度上得分高,所以与前两者距离较远。
实际的 Embedding 向量有数百到数千个维度,每个维度编码的语义特征远比"陆生/水生"复杂得多。可能是"情感倾向"、"主题类别"、"文体风格"等等,但我们无法确切知道每个维度的具体含义。重要的是,这些维度共同构成了一个能够反映语义关系的向量空间。
判断两个文本的语义相似度,关键在于两个向量之间的距离度量。以下是三种最常见的度量方式:
| 度量方式 | 公式 | 特点 | 使用场景 |
|---|---|---|---|
| 余弦相似度 | cos(θ) = (A·B) / |A|·|B|) | 值域 -1, 1,只关心方向不关心长度 | 语义相似度(最常用) |
| 点积 | A·B | 受向量长度影响,高效 | 部分 API 返回已归一化向量时使用 |
| 欧氏距离 | √Σ(Aᵢ-Bᵢ)² | 值域 [0, ∞),越小越相似 | 对数值敏感的聚类分析 |
余弦相似度是最常用的度量方式,它衡量的是两个向量在方向上的相似程度,而不考虑它们的长度。这在语义相似度计算中是非常合理的,因为一段长文本和一段短文本可能表达相同的含义,它们的向量长度可能不同,但方向应该相近。
点积计算更简单,但受向量长度影响。如果向量已经做了 L2 归一化(长度为 1),那么点积就等于余弦相似度。很多 Embedding API(如 OpenAI)默认返回归一化向量,就是为了让你可以直接用点积加速计算。
欧氏距离衡量的是两个点在空间中的直线距离。它对向量的绝对位置敏感,适合用于聚类分析等场景,但在语义相似度任务中不如余弦相似度常用。
💡 小贴士
大多数 Embedding API(如 OpenAI)返回的是归一化向量(长度为 1)。此时点积就等于余弦相似度,你可以直接用点积简化计算。在高维空间中,点积的计算复杂度低于余弦相似度(少了一次开方运算),对于大规模检索场景能带来显著的性能提升。
3. Embedding 模型的训练原理
虽然你不需要亲手训练 Embedding 模型,但了解其训练原理有助于理解它的优势和局限。
现代 Embedding 模型通常采用对比学习(Contrastive Learning)的方式训练。核心思想是:
- 准备大量文本对,包括"正样本对"(语义相似的文本)和"负样本对"(语义无关的文本)
- 将文本输入模型,生成对应的向量
- 调整模型参数,使得正样本对的向量距离尽可能小,负样本对的向量距离尽可能大
例如,训练数据可能包含:
- 正样本对:("苹果公司发布新品", "Apple 推出新款 iPhone")
- 负样本对:("苹果公司发布新品", "今天天气晴朗")
通过数百万甚至数十亿这样的样本对训练,模型逐渐学会将语义相似的文本映射到向量空间中相近的位置。 这种训练方式的优势是无需人工标注,可以从海量网页、书籍、代码等无标注数据中自动构造训练样本。这也是为什么现代 Embedding 模型能够在多语言、多领域上表现出色的原因。
二、生成你的第一个 Embedding
1. 调用 OpenAI Embedding API
OpenAI 提供了目前最成熟的商业 Embedding 服务。下面是使用 TypeScript 调用 API 的完整示例:
typescript
import OpenAI from "openai";
// 初始化客户端
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // 从环境变量读取 API Key
});
async function getEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: "text-embedding-3-small", // 性价比最高的入门选择
input: text,
});
return response.data[0].embedding;
}
// 使用示例
const embedding = await getEmbedding("RAG 是检索增强生成技术");
console.log(`向量维度: ${embedding.length}`); // 输出: 向量维度: 1536
console.log(`前10个维度: ${embedding.slice(0, 10).map(v => v.toFixed(4)).join(', ')}`);
// 输出示例: 前10个维度: 0.0234, -0.0156, 0.0389, ...
这个向量就是"RAG 是检索增强生成技术"这段文本在 1536 维语义空间中的坐标。有了坐标,我们就可以计算语义距离了。
注意事项:
- API Key 不要硬编码在代码中,应该通过环境变量或密钥管理服务获取
text-embedding-3-small是 OpenAI 第三代 Embedding 模型中的轻量版,1536 维,性价比高- 单次请求最多可以传入多个文本(批量生成),减少网络往返开销
- 每个文本的长度不能超过模型的最大输入限制(text-embedding-3-small 是 8191 token)
2. 批量生成 Embedding
在实际应用中,你通常需要为成百上千个文档片段生成向量。逐个调用 API 效率极低,应该使用批量接口:
typescript
async function getBatchEmbeddings(texts: string[]): Promise<number[][]> {
// OpenAI API 支持批量输入,单次最多 2048 条文本
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: texts,
});
// 按输入顺序返回向量数组
return response.data.map(item => item.embedding);
}
// 使用示例
const documents = [
"RAG 是检索增强生成技术",
"向量数据库用于存储和检索高维向量",
"余弦相似度衡量两个向量的方向差异",
];
const embeddings = await getBatchEmbeddings(documents);
console.log(`生成了 ${embeddings.length} 个向量`);
批量接口的优势在于减少了 HTTP 请求次数,降低了网络延迟的影响。对于大规模索引任务,这是必须采用的优化手段。
3. 计算余弦相似度
有了向量,接下来需要计算它们之间的相似度。下面是余弦相似度的 TypeScript 实现:
typescript
function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error(`向量维度不一致: ${a.length} vs ${b.length}`);
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
// 防止除零错误
if (normA === 0 || normB === 0) {
return 0;
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
代码解读:
- 点积计算 :
dotProduct是两个向量对应维度乘积的累加和 - 范数计算 :
normA和normB分别是两个向量的 L2 范数的平方(即各维度平方和) - 归一化:用点积除以两个向量范数的乘积,得到余弦值
如果向量已经是归一化的(L2 范数为 1),可以简化为:
typescript
function dotProductSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) {
throw new Error(`向量维度不一致: ${a.length} vs ${b.length}`);
}
let sum = 0;
for (let i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum; // 对于归一化向量,点积 = 余弦相似度
}
4. 验证"语义相近则距离近"
下面通过实际运行来验证 Embedding 的效果:
typescript
async function demo() {
console.log("=== Embedding 语义相似度演示 ===\n");
// 准备测试文本
const query = "苹果很好吃";
const similar1 = "这个水果味道不错";
const similar2 = "我喜欢吃苹果";
const unrelated1 = "明天的会议在三点";
const unrelated2 = "JavaScript 是一种编程语言";
// 生成向量
const [queryVec, similar1Vec, similar2Vec, unrelated1Vec, unrelated2Vec] =
await getBatchEmbeddings([query, similar1, similar2, unrelated1, unrelated2]);
// 计算相似度
console.log(`查询: "${query}"\n`);
console.log(`与 "${similar1}" 的相似度: ${cosineSimilarity(queryVec, similar1Vec).toFixed(4)}`);
console.log(`与 "${similar2}" 的相似度: ${cosineSimilarity(queryVec, similar2Vec).toFixed(4)}`);
console.log(`与 "${unrelated1}" 的相似度: ${cosineSimilarity(queryVec, unrelated1Vec).toFixed(4)}`);
console.log(`与 "${unrelated2}" 的相似度: ${cosineSimilarity(queryVec, unrelated2Vec).toFixed(4)}`);
}
demo();
输出结果:
arduino
=== Embedding 语义相似度演示 ===
查询: "苹果很好吃"
与 "这个水果味道不错" 的相似度: 0.8234
与 "我喜欢吃苹果" 的相似度: 0.7891
与 "明天的会议在三点" 的相似度: 0.0823
与 "JavaScript 是一种编程语言" 的相似度: 0.0456
可以看到,"苹果很好吃"和"这个水果味道不错"的相似度(0.82)远高于和"明天的会议在三点"的相似度(0.08)。这就是 Embedding 能够驱动语义检索的数学基础。
需要注意的是,相似度阈值没有统一标准。一般来说:
- 相似度 > 0.8:高度相关
- 0.6 < 相似度 < 0.8:中度相关
- 相似度 < 0.6:相关性较弱
但这个阈值需要根据具体业务场景调整。在《第9章:高级检索策略》中,我们会讨论如何通过实验确定最优的相似度阈值。
三、主流 Embedding 模型选型
1. 模型对比
选择合适的 Embedding 模型是构建 RAG 系统的关键决策之一。以下是当前主流的 Embedding 模型对比:
| 模型 | 维度 | 最大输入 Token | 语言支持 | 参考价格(每 1M Token) | 特点 |
|---|---|---|---|---|---|
| OpenAI text-embedding-3-small | 512/1536 | 8191 | 多语言 | $0.02 | 极致性价比,维度可调 |
| OpenAI text-embedding-3-large | 256/1024/3072 | 8191 | 多语言 | $0.13 | 精度最高,同系列中最强 |
| Cohere embed-multilingual-v3 | 1024 | 512 | 多语言 | $0.10 | 多语言表现优异 |
| BGE-M3 (开源) | 1024 | 8192 | 多语言 | 免费(自部署) | 支持稠密+稀疏混合检索 |
| BGE-large-en-v1.5 (开源) | 1024 | 512 | 仅英文 | 免费(自部署) | MTEB 榜单高分开源模型 |
| Jina Embeddings v2 | 768 | 8192 | 多语言 | 免费额度 + 付费 | 长文本表现好 |
| Qwen Embedding (开源) | 1024 | 8192 | 多语言 | 免费(自部署) | 中文支持优秀,阿里开源 |
| DeepSeek Embedding (开源) | 1024 | 8192 | 多语言 | 免费(自部署) | 中英文表现均衡 |
OpenAI text-embedding-3 系列是目前生产环境中最常用的选择。相比上一代 text-embedding-ada-002,第三代模型在以下方面有显著提升:
- 性能更强:在 MTEB(大规模文本嵌入基准)评测中,text-embedding-3-large 排名前列
- 维度可调:可以通过 API 参数指定输出维度,平衡精度和存储成本
- 成本更低:text-embedding-3-small 的价格仅为 ada-002 的 1/5
- 多语言支持:对中文、日文、韩文等非英语语言的支持明显改善
Cohere embed-multilingual-v3 在多语言场景下表现出色,特别是对于欧洲语言和亚洲语言的混合文本。如果你的应用面向全球用户,这是一个值得考虑的选项。
BGE(BAAI General Embedding)系列是智源研究院开发的开源 Embedding 模型,在 MTEB 榜单上长期位居前列。BGE-M3 是其最新的多语言版本,具有以下特点:
- 完全开源:可以本地部署,无需依赖第三方 API
- 混合检索支持:同时支持稠密向量检索和稀疏向量检索(类似 BM25)
- 长文本优化:最大支持 8192 token 输入,适合处理长文档
- 中文优化:对中文语义的理解优于多数通用模型
但开源模型也有劣势:需要自己维护基础设施(GPU 服务器、模型更新等),初期投入成本较高。
2. 选型决策流程
决策要点:
(1)预算考量
如果你的项目预算有限,或者处于早期验证阶段,建议从 text-embedding-3-small 起步。它的价格是 $0.02/1M token,对于大多数中小型应用来说完全可以接受。假设你有 10,000 个文档片段,每个片段平均 200 token,全部索引的成本约为:
ini
10,000 × 200 / 1,000,000 × $0.02 = $0.04
即使每天重新索引一次,一个月的成本也不到 $2。
(2)数据敏感性
如果你的数据涉及商业机密或个人隐私,不能上传到第三方服务,那么必须选择开源模型并本地部署。BGE-M3 是当前的最佳选择,但需要考虑 GPU 服务器的成本和维护难度。
(3)精度要求
如果对检索精度有极高要求(如法律、医疗场景),可以选择 text-embedding-3-large。它在 MTEB 评测中的表现明显优于 small 版本,但价格也高出 6.5 倍。
(4)多语言支持
如果你的应用需要处理多种语言,优先考虑 text-embedding-3 系列或 BGE-M3。这两个模型都在多语言数据集上进行了充分训练。
开发阶段从
text-embedding-3-small(1536 维)起步,成本低、速度快。如果发现检索精度不够,再升级到text-embedding-3-large或评估开源方案。不要在项目初期就为"最优模型"做过度优化------在《第12章:RAG 系统评估与指标体系》中会教你用数据驱动的评估来决策。
3. 维度选择的权衡
OpenAI 的 text-embedding-3 系列允许你通过 dimensions 参数指定输出维度。这是一个重要的优化手段,但很多人不知道如何使用。
维度越低,存储空间更小(1536 维 vs 512 维,节省 2/3 空间),相似度计算更快(向量点积的运算量与维度成正比),但可能损失部分语义信息,检索精度略有下降。
维度越高:
- 能编码更丰富的语义信息
- 检索精度理论上更高
- 存储和计算成本增加
实际测试表明,对于大多数应用场景,512 维已经足够。只有在对精度有极致要求的场景下,才需要使用完整的 1536 维或 3072 维。
typescript
// 指定输出维度为 512
const response = await openai.embeddings.create({
model: "text-embedding-3-small",
input: "这是一段测试文本",
dimensions: 512, // 可选参数,默认为 1536
});
console.log(`向量维度: ${response.data[0].embedding.length}`); // 输出: 512
四、理解 Embedding 在 RAG 中的位置
Embedding 在 RAG 中出现两次:索引时(文档 → 向量)、检索时(问题 → 向量)。两次使用同一个 Embedding 模型至关重要------换模型会导致向量空间不一致,相似度计算结果失去意义。
索引管道的特点:
- 离线执行:通常在文档更新时触发,可以是定时任务或事件驱动
- 批量处理:一次性处理大量文档,注重吞吐量而非延迟
- 可缓存:生成的向量可以持久化存储,避免重复计算
- 容错性强:如果某次调用失败,可以重试,不影响用户体验
检索管道的特点:
- 在线执行:每次用户提问时触发,直接影响响应速度
- 单次调用:只为一个问题生成向量,注重低延迟
- 不可缓存:每个问题都是新的,无法预先计算
- 容错性弱:如果 Embedding 调用失败,整个检索流程中断
由于这两个管道的特性不同,在生产系统中通常会采用不同的优化策略。例如,索引管道可以使用批量接口和异步处理来提高吞吐量,而检索管道则需要使用连接池和超时控制来保证低延迟。
五、Embedding 的常见陷阱
1. 截断问题
Embedding 模型有最大输入长度限制。如果文本超过限制,超出的部分会被截断。这可能导致语义丢失,特别是当关键信息位于文本末尾时。
typescript
// 错误做法:直接传入超长文本
const longText = "一篇超过 10000 token 的长文章...";
const embedding = await getEmbedding(longText);
// 超出 8191 token 的部分被静默截断,你可能根本不知道
// 正确做法:先切分再生成向量
const chunks = splitText(longText, 500); // 见第 5 章
const embeddings = await getBatchEmbeddings(chunks);
在《第5章:文档切分策略》中,我们会详细讨论文档切分的最佳实践。
2. 空值和异常值处理
在实际工程中,你可能会遇到空字符串、纯标点符号、特殊字符等异常情况。这些输入可能生成无效的向量或导致 API 调用失败。
typescript
async function safeGetEmbedding(text: string): Promise<number[] | null> {
// 过滤空值和无效输入
if (!text || text.trim().length === 0) {
console.warn("跳过空文本");
return null;
}
// 过滤纯标点符号
if (/^[^\w\u4e00-\u9fa5]+$/.test(text)) {
console.warn("跳过纯标点文本:", text);
return null;
}
try {
return await getEmbedding(text);
} catch (error) {
console.error("Embedding 生成失败:", error);
return null;
}
}
3. 批处理的边界
虽然批量接口能提高效率,但要注意单次请求的限制:
- OpenAI 单次最多 2048 条文本
- 总 token 数不能超过模型的最大上下文窗口
- 过大的批次会增加单次请求的失败风险
推荐的批处理策略:
typescript
async function batchEmbedWithRetry(
texts: string[],
batchSize: number = 100
): Promise<(number[] | null)[]> {
const results: (number[] | null)[] = [];
for (let i = 0; i < texts.length; i += batchSize) {
const batch = texts.slice(i, i + batchSize);
// 重试机制:最多重试 3 次
let retries = 3;
while (retries > 0) {
try {
const embeddings = await getBatchEmbeddings(batch);
results.push(...embeddings);
break; // 成功则跳出循环
} catch (error) {
retries--;
if (retries === 0) {
console.error(`批次 ${i}-${i + batchSize} 处理失败,填充 null`);
results.push(...Array(batch.length).fill(null));
} else {
console.warn(`重试剩余次数: ${retries}`);
await sleep(1000 * (4 - retries)); // 指数退避
}
}
}
}
return results;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
FAQ
Q1:Embedding 维度越高越好吗?
不一定。维度越高,理论上能编码更多语义信息,但也意味着存储成本更高、相似度计算更慢。实际效果取决于模型的训练质量而非维度数。text-embedding-3-small 的 512 维在很多场景下已经足够。
更重要的是,维度和精度之间不是线性关系。从 512 维提升到 1536 维,精度提升可能只有 5%-10%,但存储和计算成本增加了 3 倍。在资源受限的场景下,这种权衡是不划算的。
Q2:可以同时用多个不同的 Embedding 模型吗?
不建议。所有文档和查询必须用同一个模型生成向量,因为不同模型映射到的向量空间不同,跨空间的相似度没有意义。如果确实需要切换模型,你需要重新索引整个知识库。
不过,有一种高级用法叫"多向量检索":为同一段文本生成多个不同模型的向量,分别存储在不同的索引中,检索时从多个索引中召回结果再合并。这种方案能结合不同模型的优势,但实现复杂度高,适合对精度有极致要求的场景。
Q3:中文 Embedding 选哪个好?
目前 text-embedding-3-small/large 的中文表现不错,是生产环境中常见的默认选择。BGE-M3 作为开源多语言模型也是值得评估的选项。关键是用你自己的数据做评估------在《第12章:RAG 系统评估与指标体系》中会教你怎么做。
如果你主要处理中文文本,还可以考虑专门针对中文优化的模型,如 Alibaba 的 text2vec-base-chinese 或 SentenceTransformers 的 paraphrase-multilingual-MiniLM-L12-v2。但这些模型的通用性不如 text-embedding-3 系列,需要仔细评估。
Q4:Embedding 能捕捉到什么粒度的语义?词语?句子?段落?
取决于模型的设计和输入长度。大多数 Embedding 模型对句子级别(几十到几百字)的语义捕捉最好。过短的输入(一两个词)缺乏足够的上下文,过长的输入(几千字)中的细节语义容易被稀释。
研究表明,Embedding 模型在以下粒度上表现最佳:
- 短语级别(5-20 词):能捕捉核心概念
- 句子级别(20-100 词):能捕捉完整语义
- 短段落级别(100-300 词):能捕捉上下文关系
超过 500 词的长文本,Embedding 倾向于捕捉整体主题,而忽略局部细节。这就是为什么在 RAG 系统中需要将长文档切分成小块的原因。
Q5:Embedding 能否理解代码?
专门的代码 Embedding 模型(如 OpenAI 的 text-embedding-ada-002 对代码有一定理解能力,但效果不如自然语言。如果需要处理代码库问答,建议使用专门训练的代码 Embedding 模型,如 CodeBERT 或 GraphCodeBERT。
在实际应用中,混合使用自然语言 Embedding 和代码 Embedding 能获得更好的效果。例如,用自然语言 Embedding 处理注释和文档,用代码 Embedding 处理代码本身,然后在检索时合并结果。
Q6:如何评估 Embedding 模型的质量?
最直接的方法是在你的实际数据上做 A/B 测试:
- 准备一组测试问题及其对应的正确答案(来自哪些文档)
- 用不同的 Embedding 模型生成向量
- 对每个问题执行检索,检查正确答案是否在 Top-K 结果中
- 计算召回率、精确率等指标
在《第12章:RAG 系统评估与指标体系》中,我们会详细介绍评估方法和工具。
练习
练习 1:编写相似度排序函数
给定一个查询和多段文本,编写一个函数返回按语义相似度从高到低排序的文本列表。
typescript
async function rankBySimilarity(
query: string,
candidates: string[]
): Promise<Array<{ text: string; score: number }>> {
// 你的实现
// 提示:先生成查询和所有候选文本的向量,然后计算相似度并排序
}
验证标准:用四条文本测试(其中两条语义相关、两条无关),相关文本排在前面且分数 > 0.7。
参考实现:
typescript
async function rankBySimilarity(
query: string,
candidates: string[]
): Promise<Array<{ text: string; score: number }>> {
// 生成查询向量
const queryVec = await getEmbedding(query);
// 批量生成候选文本向量
const candidateVecs = await getBatchEmbeddings(candidates);
// 计算相似度并排序
const scored = candidates.map((text, index) => ({
text,
score: cosineSimilarity(queryVec, candidateVecs[index]),
}));
return scored.sort((a, b) => b.score - a.score);
}
// 测试
const query = "如何部署 Node.js 应用?";
const candidates = [
"使用 Docker 容器化部署 Node.js 应用的步骤",
"Python Flask 框架入门教程",
"Node.js 生产环境最佳实践:PM2、Nginx 配置",
"JavaScript 数组方法详解",
];
const ranked = await rankBySimilarity(query, candidates);
ranked.forEach((item, index) => {
console.log(`${index + 1}. [${item.score.toFixed(4)}] ${item.text}`);
});
预期输出中,第 1 和第 3 条应该排在前两位,分数高于 0.7。
练习 2:对比不同模型的输出
用 text-embedding-3-small 和 text-embedding-3-large 对同一组 10 段文本生成向量,对比两组向量的语义排序结果是否一致。
验证标准:两份排序结果的 Top-3 应该基本一致;如果有差异,分析可能的原因。
提示:
- 准备 1 个查询和 10 个候选文本
- 分别用两个模型生成向量
- 对每个模型计算相似度排序
- 比较两个排序结果的差异
练习 3:理解归一化
修改余弦相似度函数,让它先对输入向量做 L2 归一化,再用点积计算相似度。验证归一化后点积结果等于余弦相似度。
L2 归一化公式:
less
normalized[i] = vector[i] / sqrt(sum(vector[j]^2 for j in range(len(vector))))
验证方法:
typescript
function l2Normalize(vector: number[]): number[] {
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
return vector.map(v => v / norm);
}
function normalizedDotProduct(a: number[], b: number[]): number {
const normA = l2Normalize(a);
const normB = l2Normalize(b);
let sum = 0;
for (let i = 0; i < normA.length; i++) {
sum += normA[i] * normB[i];
}
return sum;
}
// 验证
const a = [1, 2, 3];
const b = [4, 5, 6];
const cosine = cosineSimilarity(a, b);
const dotProd = normalizedDotProduct(a, b);
console.log(`余弦相似度: ${cosine.toFixed(6)}`);
console.log(`归一化点积: ${dotProd.toFixed(6)}`);
console.log(`差异: ${Math.abs(cosine - dotProd) < 1e-10 ? '一致' : '不一致'}`);
练习 4:批处理性能测试
编写一个性能测试脚本,对比以下三种方式的 Embedding 生成速度:
- 逐条调用 API(串行)
- 逐条调用 API(并行,Promise.all)
- 批量调用 API
测试数据:100 条文本,每条 50-100 token。
验证标准:记录每种方式的总耗时,分析性能差异的原因。批量调用应该明显快于其他方式。
📚 延伸阅读
- OpenAI Embeddings Documentation --- OpenAI 官方文档,详细介绍了 Embedding API 的使用方法和最佳实践
- MTEB Leaderboard --- 大规模文本嵌入基准评测排行榜,可以查看各种 Embedding 模型在不同任务上的表现
- BGE-M3 论文 --- 智源研究院发布的开源多语言 Embedding 模型论文,介绍了混合检索的技术细节
- Cohere Embeddings Guide --- Cohere 的 Embedding 使用指南,适合需要了解替代方案的读者
- Sentence Transformers Documentation --- 开源的 Sentence Transformer 库文档,适合想本地部署 Embedding 模型的读者
- Lost in the Middle: How Language Models Use Long Contexts --- 研究论文,揭示了 LLM 在处理长上下文时的"中间位置忽视"现象,对理解 Embedding 的局限性有帮助