系列第三篇。前两篇我们搭好了 TypeScript 工程、跑通了 invoke 与 stream 两种生成式调用,本篇进入 RAG 的地基------文本如何变成向量,以及向量之间如何比较。
前言
大模型「读懂」文字靠的是 token,但 token 之间只有离散的编号关系,模型并不知道「苹果」和「李子」在语义上很近。要让程序能「理解」两段文字的相似程度,必须先把文本映射成一个高维向量,再用几何方法比较。这一步就是 Embedding。
本篇基于我本地 nana-ima/1.basic 今天的真实代码,从语义表示讲到余弦相似度,并复盘几个踩过的真实坑。
一、为什么需要 Embedding
传统关键词检索是「字面匹配」:
text
查询:水果有哪些好吃的
文档:苹果是蔷薇科水果 → 无「好吃的」关键词 → 匹配失败
Embedding 做的是「语义匹配」:
text
查询向量 ← "水果有哪些好吃的"
文档向量 ← "苹果是蔷薇科水果"
余弦相似度 ≈ 0.82 → 语义高度相关,命中
文本被映射成向量后,语义相近的文本在向量空间里方向接近,这是 RAG(检索增强生成)能「找对文档」的根本原因。
二、什么是 Embedding 向量
Embedding 模型(如 BAAI/bge-m3)把一段文本压缩成一个定长浮点数组:
text
"苹果" → [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 通用 |
| 智谱 AI | embedding-3 |
国内注册方便 |
| 阿里百炼 | text-embedding-v3 |
中文强 |
| OpenAI | text-embedding-3-small |
需海外卡 |
本篇用硅基流动 的 BAAI/bge-m3,生成模型继续走 DeepSeek 官方,互不影响。
3.2 配置 .env
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 代码:文本 → 向量
ts
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
最初我这样写:
ts
const res = embedding.embedQuery("李子");
console.log("res", res); // Promise { <pending> }
打印出 Promise { <pending> }------因为 embedQuery 是异步方法 ,返回的是 Promise 而不是结果。必须加 await:
ts
const vector1 = await embedding.embedQuery("李子");
console.log("向量维度:", vector1.length); // 1024
向量数组很长(1024 个数字),直接
console.log会喷满终端,调试时打印.length或.slice(0,5)即可。
四、相似度计算:余弦相似度
拿到向量后,怎么判断「李子」和「杏子」谁更接近?用余弦相似度 ------比较两个向量方向的接近程度。 
4.1 数学公式
text
cos(θ) = (A · B) / (|A| × |B|)
A · B:点积,对应位置相乘再累加|A|、|B|:向量模长,√(各分量平方和)θ:两个向量的夹角
除以模长是为了归一化------只比方向、不比长度,否则长文本向量天然数值大,会不公平。
4.2 代码逐行解析
ts
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 运行与结果解读
ts
const similarity = cosineSimilarity(vector1, vector2);
console.log(similarity);

| 余弦值 | 含义 |
|---|---|
1.0 |
方向完全相同,语义极相似 |
0.8 ~ 0.9 |
很相似(李子 vs 杏子) |
0.3 ~ 0.5 |
有点关系 |
0 |
完全无关 |
-1.0 |
语义相反 |
预期「李子」与「杏子」的余弦值在 0.8 以上,因为它们都是水果、方向接近。
五、它在 RAG 里的位置
Embedding 负责「建库」和「查库」两步的向量化,余弦相似度负责「排序找最相关」。本篇只实现了相似度函数,下一篇会接上 MemoryVectorStore 和文档切分,跑通一个最小可问答的 RAG。
六、小结
- Embedding 把文本变成定长向量,让语义可计算
- DeepSeek 无 embedding ,向量模型选硅基流动
BAAI/bge-m3等 embedQuery单条、embedDocuments批量,二者都是异步需await- 余弦相似度 = 点积 ÷ 模长乘积,值越接近 1 语义越相似
- TS 开了
noUncheckedIndexedAccess时,索引取值用?? 0兜底
下一篇预告:从 0 搭一个 RAG------文档切分、向量存储与检索增强问答。
如果这篇对你有帮助,欢迎点赞收藏,系列持续更新中。