深入浅出RAG——第2章:文本嵌入 —— 让计算机理解语义的桥梁

📖 本章学习目标

  • 用自己的话解释 Embedding 如何将文本语义映射到高维向量空间
  • 使用 TypeScript 调用 Embedding API 生成文本向量
  • 编写余弦相似度函数,计算两段文本的语义相似度
  • 根据业务需求,从维度、成本、语言支持等维度选型 Embedding 模型

《第1章:RAG 是什么,为什么需要它》中,我们建立了对 RAG 的整体认知:检索 → 增强 → 生成。但你可能会问------"检索"是怎么知道一段文字和我的问题是相关的?计算机如何判断"语义相似"而不是简单的关键词匹配?

答案就是本章的主角:文本嵌入(Embedding)。它是整个 RAG 系统的基石,检索质量的上限由它决定。如果 Embedding 无法准确捕捉语义,后续的向量搜索、文档召回都会失效,再强大的 LLM 也无能为力。

一、Embedding 是什么

1. 从"字符串"到"向量"

计算机处理文本时,看到的是字符串,但字符串之间没有"距离"这个概念。"苹果很好吃"和"这个水果味道不错",在字符层面几乎没有重叠,但语义上显然高度相关。传统的关键词匹配算法(如 TF-IDF、BM25)只能基于词汇重叠度来判断相关性,对于这种词汇不同但含义相近的情况无能为力。

Embedding 做的事情就是将一段文本映射为高维空间中的一个点(向量)。映射的核心规则是:语义相近的文本,在向量空间中距离也相近;语义无关的文本,距离则较远。

graph LR A[苹果很好吃] --> B[Embedding 模型] C[这个水果味道不错] --> B B --> D["[0.12, -0.34, 0.78, ...]"] B --> E["[0.15, -0.31, 0.75, ...]"] D -.- F["距离 ≈ 0.05(很接近)"] E -.- F

这个映射过程不是手工设计的规则,而是通过深度学习模型自动学习得到的。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)的方式训练。核心思想是:

  1. 准备大量文本对,包括"正样本对"(语义相似的文本)和"负样本对"(语义无关的文本)
  2. 将文本输入模型,生成对应的向量
  3. 调整模型参数,使得正样本对的向量距离尽可能小,负样本对的向量距离尽可能大

例如,训练数据可能包含:

  • 正样本对:("苹果公司发布新品", "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));
}

代码解读

  1. 点积计算dotProduct 是两个向量对应维度乘积的累加和
  2. 范数计算normAnormB 分别是两个向量的 L2 范数的平方(即各维度平方和)
  3. 归一化:用点积除以两个向量范数的乘积,得到余弦值

如果向量已经是归一化的(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. 选型决策流程

graph TD A[选择 Embedding 模型] --> B{预算和数据敏感性?} B -->|预算有限或数据敏感| C{是否需要多语言?} B -->|预算充足且可接受云服务| D{精度要求?} C -->|需要| E[BGE-M3 自部署] C -->|不需要| F[BGE-large-en 自部署] D -->|极致精度| G[text-embedding-3-large] D -->|高性价比| H[text-embedding-3-small] E --> I[评估运维成本] F --> I G --> J[开始使用] H --> J

决策要点

(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 中的位置

graph TD subgraph &#34;索引管道(离线执行)&#34; A[原始文档] --> B[文档切分] B --> C[每段文本 → Embedding] C --> D[(向量数据库<br/>存储向量+原文)] end subgraph &#34;检索管道(在线执行)&#34; E[用户问题] --> F[问题 → Embedding] F --> G[向量相似度搜索] G --> H[返回 Top-K 原文片段] end D --> G

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 测试:

  1. 准备一组测试问题及其对应的正确答案(来自哪些文档)
  2. 用不同的 Embedding 模型生成向量
  3. 对每个问题执行检索,检查正确答案是否在 Top-K 结果中
  4. 计算召回率、精确率等指标

《第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 生成速度:

  1. 逐条调用 API(串行)
  2. 逐条调用 API(并行,Promise.all)
  3. 批量调用 API

测试数据:100 条文本,每条 50-100 token。

验证标准:记录每种方式的总耗时,分析性能差异的原因。批量调用应该明显快于其他方式。

📚 延伸阅读

相关推荐
mit6.8241 小时前
VLM Wiki
人工智能
工业甲酰苯胺1 小时前
AI×JNPF实战:3个案例拆解企业数智转型核心逻辑
大数据·数据库·人工智能·低代码
李顿波1 小时前
Ai IDE / Ai Agent 一览!
ide·人工智能
洛卡卡了1 小时前
Claude Code 项目级 Skill:团队协作场景下的使用场景
人工智能·后端·claude
QYR-分析1 小时前
柔性智造赋能升级!机器人视觉检测系统行业高速扩容,国产替代迎来新机遇
人工智能·机器人
ZZZMMM.zip1 小时前
基于鸿蒙PC与鸿蒙Flutter框架构建AI本地生活服务平台
人工智能·flutter·华为·harmonyos·鸿蒙
带娃的IT创业者1 小时前
从 iptv-org/iptv 看开源协作:如何像 AI Agent 一样思考工程化实践
人工智能·开源·github·软件开发·ai agent·工程化实践·开源协作
一生有你20202 小时前
Spring AI ChatMemory 监控与排查:从指标到排错全链路
java·人工智能·spring
NiceCloud喜云2 小时前
Claude API 价格完全指南:多少钱?怎么算?怎么省?(2026最新)
人工智能·gpt·ai·aigc