首页/文章/八股文

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

2026-08-20
17936 分钟
...

前言

大模型「读懂」文字靠的是 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) 即可。

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

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

Image 1: 余弦相似度:向量夹角与语义相似.png

Image 1: 余弦相似度:向量夹角与语义相似.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);

 

Image 2: cos.png

Image 2: cos.png

 

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

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

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2026-08-27
定时器按顺序播放多个音频,切后台音频会乱
本身不会补触发,但 它会被系统级冻结 ——iOS Safari 后台完全停止计时,切回前台后 只补执行一次 (不是不补,而是"缺失的中间状态补不上")。间隔短的不会出大问题, 间隔长的会出现"音频流被压缩" ——切回来后本该播 5 分钟的间隙,实际只过了 3 分钟,结果整段对不上。 核心原则 :定时器只能用来"提醒一次", 真正决定"该不该播"的是绝对时间戳 。 setTimeout 在后台会发生什么(按平台) | 平台 | 后台行为...
面试
八股文2026-08-27
线上项目白屏的原因
白屏的本质是 渲染管线某一环断了 ——可能是资源、JS、接口、路由、样式、兼容性任一环节出问题。排查按"控制台 → 网络 → DOM → 环境"四步定位。 本质(前端类比) 把网页想成一栋楼: 白屏 ≠ 一定是同一种原因——这是面试想听的层次。 六大类原因(按出现频率) | 类别 | 典型表现 | 真实案例 | | : | : | : | | ① 资源加载失败 | DOM 是空的 | 入口 JS 404、CDN 挂了、CSS 阻塞 |...
面试
八股文2026-08-27
背景图就是 1MB 大图,怎么优化
1MB 大背景图优化分 三步 :压缩体积(10 30x)、按需加载(按设备/视口/网速)、渲染期优化(GPU 合成)。背景图跟 不同——它是 CSS,不走浏览器的原生懒加载机制,得手动处理。 背景图 vs 的关键区别(先讲清楚这个) 这是面试官想听的"针对性认知"——背景图不是普通图片,不能套通用方案。 三步优化(按优先级) 第 1 步:压缩体积(最重要,立竿见影) 1MB 的来源一般是这几种 ,对应解决方案: | 原始问题 | 体积来...
面试
八股文2026-08-27
项目里很多图片和视频,怎么优化
图片视频优化分 四层 :网络层(CDN/格式)、加载层(懒加载/预加载)、渲染层(解码/缓存)、业务层(按需/降级)。面试要把这四层都讲到位才算有体系。 四层优化模型 第 1 层:网络层(省钱、省时间) 核心目标:让资源体积小、让用户拿到资源快 | 手段 | 作用 | 关键点 | | : | : | : | | 图片格式 | WebP/AVIF 比 JPEG 小 25 50% | 兼容 fallback | | 视频格式 | H.265...
面试
八股文2026-08-19
前端面试100题
前端面试 100 题 适用方向:中高级前端 / React / Vue / Next.js / Nuxt / TypeScript / 工程化 / 实时通信 / Electron / Node.js / AI 应用前端 使用方式:优先掌握“标准回答”,再练“面试官追问”,最后把“结合你的简历怎么答”组织成自己的项目故事。 说明 “结合你的简历怎么答”只使用你简历中已经出现的项目与技术事实。 “标准回答 / 追问”属于通用前端知识总结,用...
面试
八股文2026-08-19
深入理解 JavaScript 原型与原型链:从关系图到核心逻辑 - heshanwan - 博客园
在 JavaScript 世界里, 原型(Prototype)和原型链(Prototype Chain) 是理解对象继承、属性查找机制的基石。很多开发者初学时对它们 “又爱又恨”,这篇文章将结合经典关系图,用通俗易懂的方式拆解原型与原型链的核心逻辑,帮你彻底掌握这套机制! &nbsp; 一、先搞懂几个核心概念 在分析关系图前,先明确 JavaScript 中与原型相关的关键概念,避免后续混淆: 1\. 函数对象与普通对象 函数对象 :由...
面试

评论

请登录后发表评论

去登录
加载评论中...

目录