Embedding 向量模型:从语义表示到相似度计算

0 阅读5分钟

系列第三篇。前两篇我们搭好了 TypeScript 工程、跑通了 invoke 与 stream 两种生成式调用,本篇进入 RAG 的地基——文本如何变成向量,以及向量之间如何比较。

前言

大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个高维向量,再用几何方法比较。这一步就是 Embedding。

本篇基于我本地 nana-ima/1.basic 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。


一、为什么需要 Embedding

传统关键词检索是「字面匹配」:

查询:水果有哪些好吃的
文档:苹果是蔷薇科水果   →  无「好吃的」关键词 → 匹配失败

Embedding 做的是「语义匹配」:

查询向量 ← "水果有哪些好吃的"
文档向量 ← "苹果是蔷薇科水果"
余弦相似度 ≈ 0.82          →  语义高度相关,命中

文本被映射成向量后,语义相近的文本在向量空间里方向接近,这是 RAG(检索增强生成)能「找对文档」的根本原因。


二、什么是 Embedding 向量

Embedding 模型(如 BAAI/bge-m3)把一段文本压缩成一个定长浮点数组:

"苹果" → [0.12, -0.05, 0.33, ..., 0.08]   (1024 维)
"李子" → [0.10, -0.02, 0.31, ..., 0.09]   (1024 维)
"汽车" → [-0.40, 0.70, -0.10, ..., 0.30]  (1024 维)

数组里的数字本身无意义,但它的几何方向承载语义:水果类的向量彼此靠近,和「汽车」夹角很大。

关键特性:

  • 定长:无论输入多长,输出维度固定(bge-m3 是 1024 维)
  • 可计算:向量之间能算距离 / 夹角,距离越小语义越近
  • 同一模型才可比:不同模型出来的向量维度、坐标系不同,不能混着比

三、实战:用 LangChain 调用向量模型

3.1 选型:DeepSeek 没有向量模型

一个常见误解是「用 DeepSeek 的 key 也能做 embedding」。实际上 DeepSeek 只提供 deepseek-chat / deepseek-reasoner 生成模型,没有 embedding 接口,直接填会 404。

国内好用的向量模型选型:

平台模型特点
硅基流动BAAI/bge-m3中文效果好、送代金券、一个 key 通用
智谱 AIembedding-3国内注册方便
阿里百炼text-embedding-v3中文强
OpenAItext-embedding-3-small需海外卡

本篇用硅基流动BAAI/bge-m3,生成模型继续走 DeepSeek 官方,互不影响。

3.2 配置 .env

# 生成模型(DeepSeek 官方)
NANA_API_KEY=sk-你的deepseekkey
NANA_MODEL=deepseek-chat
NANA_API_BASE_URL=https://api.deepseek.com/v1

# 向量模型(硅基流动 SiliconFlow)
NANA_EMBEDDING_API_KEY=sk-你的硅基流动key
NANA_EMBEDDING_MODEL=BAAI/bge-m3
EMBEDDING_BASE_URL=https://api.siliconflow.cn/v1

硅基流动的代金券自动抵扣,调用时不用手动选,费用优先从券余额扣。

3.3 代码:文本 → 向量

import "dotenv/config";
import { OpenAIEmbeddings } from "@langchain/openai";

const getEnv = (key: string) => process.env[key] || "";

const embedding = new OpenAIEmbeddings({
    model: getEnv("NANA_EMBEDDING_MODEL"),
    apiKey: getEnv("NANA_EMBEDDING_API_KEY"),
    configuration: {
        baseURL: getEnv("EMBEDDING_BASE_URL"),
    },
});

// 单条文本 → 一个向量
const vector1 = await embedding.embedQuery("李子");
const vector2 = await embedding.embedQuery("杏子");

// 多条文本 → 向量数组(内部批处理,比循环调用快)
const vectors = await embedding.embedDocuments(["苹果", "李子"]);

两个核心 API:

方法入参返回用途
embedQuery单条字符串number[]给「查询问题」向量化
embedDocuments字符串数组number[][]给「知识库文档」批量向量化

3.4 踩坑:Promise pending

最初我这样写:

const res = embedding.embedQuery("李子");
console.log("res", res);   // Promise { <pending> }

打印出 Promise { <pending> }——因为 embedQuery异步方法,返回的是 Promise 而不是结果。必须加 await

const vector1 = await embedding.embedQuery("李子");
console.log("向量维度:", vector1.length);   // 1024

向量数组很长(1024 个数字),直接 console.log 会喷满终端,调试时打印 .length.slice(0,5) 即可。


四、相似度计算:余弦相似度

拿到向量后,怎么判断「李子」和「杏子」谁更接近?用余弦相似度——比较两个向量方向的接近程度。 余弦相似度:向量夹角与语义相似.png

4.1 数学公式

cos(θ) = (A · B) / (|A| × |B|)
  • A · B:点积,对应位置相乘再累加
  • |A||B|:向量模长,√(各分量平方和)
  • θ:两个向量的夹角

除以模长是为了归一化——只比方向、不比长度,否则长文本向量天然数值大,会不公平。

4.2 代码逐行解析

const cosineSimilarity = (v1: number[], v2: number[]) => {
    // ① 点积 A·B:v1、v2 对应位置相乘累加
    const dotProduct = v1.reduce(
        (acc, cur, index) => acc + cur * (v2[index] ?? 0),
        0
    );
    // ② 模长 |A|:v1 各分量平方和开根
    const magnitude1 = Math.sqrt(v1.reduce((acc, cur) => acc + cur * cur, 0));
    // ③ 模长 |B|:v2 各分量平方和开根
    const magnitude2 = Math.sqrt(v2.reduce((acc, cur) => acc + cur * cur, 0));
    // ④ 余弦相似度 = 点积 / (模长乘积)
    return dotProduct / (magnitude1 * magnitude2);
};

关于 ?? 0:我的 tsconfig.json 开了 noUncheckedIndexedAccess: true,TS 认为 v2[index] 可能取不到值(类型 number | undefined),相乘会报类型错。?? 0 是兜底(两个向量维度相同,实际不会越界),既不影响逻辑又让编译通过。

4.3 运行与结果解读

const similarity = cosineSimilarity(vector1, vector2);
console.log(similarity);

cos.png

余弦值含义
1.0方向完全相同,语义极相似
0.8 ~ 0.9很相似(李子 vs 杏子)
0.3 ~ 0.5有点关系
0完全无关
-1.0语义相反

预期「李子」与「杏子」的余弦值在 0.8 以上,因为它们都是水果、方向接近。


五、它在 RAG 里的位置

flowchart LR
    A[知识库文档] --> B[切分 Chunk]
    B --> C[embedDocuments 向量化]
    C --> D[(向量库)]
    E[用户问题] --> F[embedQuery 向量化]
    F --> G{余弦相似度排序}
    D --> G
    G --> H[取 TopK 相关文档]
    H --> I[拼进 Prompt 喂给 LLM]

Embedding 负责「建库」和「查库」两步的向量化,余弦相似度负责「排序找最相关」。本篇只实现了相似度函数,下一篇会接上 MemoryVectorStore 和文档切分,跑通一个最小可问答的 RAG。


六、小结

  • Embedding 把文本变成定长向量,让语义可计算
  • DeepSeek 无 embedding,向量模型选硅基流动 BAAI/bge-m3
  • embedQuery 单条、embedDocuments 批量,二者都是异步需 await
  • 余弦相似度 = 点积 ÷ 模长乘积,值越接近 1 语义越相似
  • TS 开了 noUncheckedIndexedAccess 时,索引取值用 ?? 0 兜底

下一篇预告:从 0 搭一个 RAG——文档切分、向量存储与检索增强问答


如果这篇对你有帮助,欢迎点赞收藏,系列持续更新中。