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

系列第三篇。前两篇我们搭好了 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 里的位置

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------文档切分、向量存储与检索增强问答


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

相关推荐
北斗落凡尘3 小时前
LangGraph 入门实战(4)
python·langchain
闲猫3 小时前
LangChain / Advanced usage / Long-term memory
langchain
GitLqr3 小时前
拒绝 AI 乱写代码:手把手教你玩转 Claude Code 的工程化技能集
openai·ai编程·claude
JaydenAI3 小时前
[基于OpenEvals的自动化评估-12]Agent执行轨迹评估[无LLM参与]
ai·langchain·agent·evaluation·openevals
做前端的娜娜子4 小时前
第二个 AI 调用——invoke 阻塞式与 stream 流式生成
人工智能·llm·掘金·金石计划
9i编程4 小时前
AI 只解决眼前那个坑【上篇】:来源、图片、鲁棒性,把能聊一处一处补齐
人工智能·openai·ai编程
云卷云舒___________6 小时前
Grok4.6上线Cursor、GPT5.6-Cyber发布、谷歌搁置Gemini3.5Pro | 8月11日 AI日报
openai·谷歌·cursor·ai日报·grok46·gpt56cyber·daybreak
北斗落凡尘7 小时前
LangGraph 入门实战(3)
python·langchain
lzjava20247 小时前
LangChain集成MCP
langchain