1. 核心结论
在 embedding 向量检索场景中,归一化(Normalization)和点积(Dot Product)是配合使用的一对操作:
先对向量做归一化,把它变成长度为 1 的单位向量;再通过点积计算两个归一化向量的相似度,此时点积的结果就直接等价于 cosine 相似度。
这套组合是向量检索里最常见、最基础的相似度计算方式,被广泛用于长期记忆、语义搜索、推荐系统等场景。
2. 归一化是什么
归一化指的是把向量按比例缩放,使其长度(模长)变为固定值(通常为 1),但方向保持不变。
2.1 计算公式
对向量 v=v1,v2,...,vnv = v_1, v_2, \\dots, v_nv=v1,v2,...,vn,先计算模长:
∥v∥=v12+v22+⋯+vn2 \|v\| = \sqrt{v_1^2 + v_2^2 + \dots + v_n^2} ∥v∥=v12+v22+⋯+vn2
再让每个分量除以模长:
v^=v1∥v∥,v2∥v∥,...,vn∥v∥ \hat{v} = \left\\frac{v_1}{\\\|v\\\|}, \\frac{v_2}{\\\|v\\\|}, \\dots, \\frac{v_n}{\\\|v\\\|}\\right v^=∥v∥v1,∥v∥v2,...,∥v∥vn
归一化后的向量 v^\hat{v}v^ 满足 ∥v^∥=1\|\hat{v}\| = 1∥v^∥=1。
2.2 代码实现
ts
function normalizeVector(vector: number[]): number[] {
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
if (norm === 0) {
return vector;
}
return vector.map((value) => value / norm);
}
2.3 举例说明
向量 v=3,4v = 3, 4v=3,4 的模长:
∥v∥=32+42=9+16=25=5 \|v\| = \sqrt{3^2 + 4^2} = \sqrt{9 + 16} = \sqrt{25} = 5 ∥v∥=32+42 =9+16 =25 =5
归一化后:
v^=35,45=0.6, 0.8 \hat{v} = \left\\frac{3}{5}, \\frac{4}{5}\\right = 0.6,\\ 0.8 v^=53,54=0.6, 0.8
验证新向量模长:
∥v^∥=0.62+0.82=0.36+0.64=1=1 \|\hat{v}\| = \sqrt{0.6^2 + 0.8^2} = \sqrt{0.36 + 0.64} = \sqrt{1} = 1 ∥v^∥=0.62+0.82 =0.36+0.64 =1 =1
模长确实变为 1,方向没有改变。
2.4 为什么需要对零向量做保护
ts
if (norm === 0) {
return vector;
}
如果输入文本为空或没有任何 token 命中分桶,向量可能是全零向量 [0, 0, ..., 0],此时模长为 0,除以 0 会得到 NaN。这里直接返回原向量,避免程序崩溃。
3. 点积是什么
点积是两个向量之间的运算:把对应位置的分量两两相乘,再把所有结果相加,得到一个标量(单个数字)。
3.1 计算公式
对于向量 A=a1,a2,...,anA = a_1, a_2, \\dots, a_nA=a1,a2,...,an 和 B=b1,b2,...,bnB = b_1, b_2, \\dots, b_nB=b1,b2,...,bn:
A⋅B=a1b1+a2b2+⋯+anbn=∑i=1naibi A \cdot B = a_1 b_1 + a_2 b_2 + \dots + a_n b_n = \sum_{i=1}^{n} a_i b_i A⋅B=a1b1+a2b2+⋯+anbn=i=1∑naibi
3.2 代码实现
ts
function cosineSimilarity(left: number[], right: number[]): number {
if (left.length !== right.length) {
throw new Error(`Vector dimension mismatch: ${left.length} !== ${right.length}`);
}
let score = 0;
for (let index = 0; index < left.length; index += 1) {
score += left[index] * right[index];
}
return score;
}
3.3 举例说明
设 A=1,2,3A = 1, 2, 3A=1,2,3,B=4,5,6B = 4, 5, 6B=4,5,6:
A⋅B=(1×4)+(2×5)+(3×6)=4+10+18=32 A \cdot B = (1 \times 4) + (2 \times 5) + (3 \times 6) = 4 + 10 + 18 = 32 A⋅B=(1×4)+(2×5)+(3×6)=4+10+18=32
3.4 点积的几何意义
点积本来不是专门为"方向相似度"设计的,它更本质的作用是衡量:
一个向量沿着另一个向量方向产生了多少有效贡献。
它的几何公式是:
A⋅B=∥A∥×∥B∥×cos(θ) A \cdot B = \|A\| \times \|B\| \times \cos(\theta) A⋅B=∥A∥×∥B∥×cos(θ)
其中:
- θ\thetaθ:向量 AAA 和 BBB 之间的夹角。
- cos(θ)\cos(\theta)cos(θ):两个向量方向的接近程度。
- ∥A∥\|A\|∥A∥:向量 AAA 的长度。
- ∥B∥\|B\|∥B∥:向量 BBB 的长度。
也可以把点积理解为:
A⋅B=A 在 B 方向上的投影长度×∥B∥ A \cdot B = \text{A 在 B 方向上的投影长度} \times \|B\| A⋅B=A 在 B 方向上的投影长度×∥B∥
而 AAA 在 BBB 方向上的投影长度是:
∥A∥cos(θ) \|A\|\cos(\theta) ∥A∥cos(θ)
所以:
A⋅B=(∥A∥cos(θ))×∥B∥ A \cdot B = \left(\|A\|\cos(\theta)\right) \times \|B\| A⋅B=(∥A∥cos(θ))×∥B∥
这说明点积同时包含两部分信息:
text
方向是否接近
+
两个向量本身有多长
3.5 点积原本用来做什么
点积常用于衡量"有效贡献"。例如物理中的功:
W=F⋅d W = F \cdot d W=F⋅d
其中:
- FFF:力的向量。
- ddd:位移向量。
- WWW:力沿位移方向实际做的功。
如果力和位移方向一致,做功最大;如果垂直,力没有沿位移方向产生有效贡献;如果方向相反,则做负功。
这正是点积的典型用途:
判断一个向量在另一个向量方向上到底贡献了多少。
3.6 如何借助点积判断方向相似
方向相似真正来自公式中的 cos(θ)\cos(\theta)cos(θ)。
| 情况 | 夹角 θ\thetaθ | cos(θ)\cos(\theta)cos(θ) | 方向关系 |
|---|---|---|---|
| 方向完全相同 | 0°0°0° | 111 | 最相似 |
| 方向有一定偏移 | 60°60°60° | 0.50.50.5 | 部分相似 |
| 方向垂直无关 | 90°90°90° | 000 | 基本无关 |
| 方向完全相反 | 180°180°180° | −1-1−1 | 相反 |
问题在于,原始点积还会受到 ∥A∥\|A\|∥A∥ 和 ∥B∥\|B\|∥B∥ 的影响。
例如两个向量方向完全相同,但长度不同:
A=3,0,B=2,0 A = 3, 0, \quad B = 2, 0 A=3,0,B=2,0
A⋅B=3×2=6 A \cdot B = 3 \times 2 = 6 A⋅B=3×2=6
如果 BBB 变得更长:
B=10,0 B = 10, 0 B=10,0
A⋅B=3×10=30 A \cdot B = 3 \times 10 = 30 A⋅B=3×10=30
这两个例子里方向完全一样,但点积结果不同,原因是长度不同。
所以,如果只想判断方向相似度,就需要先归一化:
∥A∥=1,∥B∥=1 \|A\| = 1, \quad \|B\| = 1 ∥A∥=1,∥B∥=1
代入点积公式:
A⋅B=1×1×cos(θ)=cos(θ) A \cdot B = 1 \times 1 \times \cos(\theta) = \cos(\theta) A⋅B=1×1×cos(θ)=cos(θ)
这时点积就不再受向量长度影响,只剩下方向关系。
因此在 embedding 检索中:
text
未归一化点积:方向相似 + 向量长度
归一化后点积:只表示方向相似
这就是为什么向量检索通常先做归一化,再用点积判断 query embedding 和 memory embedding 的语义方向是否接近。
4. 归一化 + 点积 = Cosine 相似度
4.1 标准 cosine 相似度公式
cos(θ)=A⋅B∥A∥×∥B∥ \cos(\theta) = \frac{A \cdot B}{\|A\| \times \|B\|} cos(θ)=∥A∥×∥B∥A⋅B
不做归一化时,每次计算相似度都需要额外求两个向量的模长,再做一次除法。
4.2 归一化后的简化
如果 AAA 和 BBB 都已经归一化,即 ∥A∥=∥B∥=1\|A\| = \|B\| = 1∥A∥=∥B∥=1,公式简化为:
cos(θ)=A⋅B1×1=A⋅B \cos(\theta) = \frac{A \cdot B}{1 \times 1} = A \cdot B cos(θ)=1×1A⋅B=A⋅B
也就是说:归一化之后,点积的结果直接等于 cosine 相似度,不需要再做除法。
4.3 为什么要这样设计
这样设计有两个好处:
- 计算更快:省去了每次查询时重复计算模长和除法的开销,尤其在大规模向量检索场景下能显著提升性能。
- 逻辑更简单:向量库和检索代码只需要维护一种运算(点积),不用同时处理模长计算和归一化逻辑的耦合。
工程上通常的做法是:写入时统一归一化,查询时只做点积。
5. 在长期记忆 Embedding 流程中的位置
结合长期记忆系统的检索流程,归一化和点积分别出现在不同阶段:
text
Memory Object
↓
buildEmbeddingText(memory) // 拼接语义文本
↓
embeddingModel.embed(text) // 生成原始向量
↓
normalizeVector(vector) // 归一化:写入阶段
↓
写入向量索引 / 向量库
用户 query
↓
embeddingModel.embed(query) // 生成原始向量
↓
normalizeVector(vector) // 归一化:查询阶段
↓
cosineSimilarity(queryVec, memVec) // 点积:相似度计算
↓
按分数排序,返回 topK
5.1 写入阶段:归一化
每条长期记忆写入向量索引前,都需要对其 embedding 做归一化,确保后续所有向量都处于同一个"单位长度"的比较基准上。
5.2 查询阶段:归一化 + 点积
用户的 query 生成向量后,同样需要归一化,然后与索引中的每条记忆向量做点积,点积结果就是该记忆与当前 query 的语义相似度分数。
5.3 排序阶段
拿到所有候选记忆的相似度分数后,按分数从高到低排序,取 topK 作为召回结果,再结合 importance、关键词召回等信号做进一步的 rerank。
6. 归一化的处理时机
归一化不一定是 embedding 模型自动完成的。它可能发生在三个地方:模型内部、业务代码、向量库。
6.1 模型输出已归一化
有些 embedding 模型或 SDK 会直接返回单位向量。
如果模型文档有类似说明:
text
returned embeddings are normalized
unit norm vectors
cosine similarity recommended
就不需要业务代码再归一化。
6.2 归一化在业务代码中处理
很多系统会在写入和查询时自己归一化,这是最可控的方式:
ts
// 写入时
const memoryEmbedding = normalizeVector(
await embeddingModel.embed(memoryText)
);
// 查询时
const queryEmbedding = normalizeVector(
await embeddingModel.embed(query)
);
不管 embedding 模型内部是否已经归一化,在外部再归一化一次是安全的(模度等于 1 的向量归一化后仍是它自身)。
6.3 归一化在向量库内部处理
有些向量库支持直接配置相似度度量:
text
metric = cosine
metric = dot / inner_product
metric = l2
- 选择
cosine:向量库可能内部处理归一化,也可能只是按 cosine 公式计算。需看文档确认。 - 选择
dot / inner_product:等价于直接用点积,建议业务代码自己归一化,否则长度会干扰相似度结果。 - 选择
l2:是 L2 距离度量,归一化后 L2 和 cosine 相关但不完全等价,需具体场景具体判断。
6.4 如何判断模型输出是否已归一化
如果模型文档不写明,可以直接测一下返回向量的模长:
ts
function vectorNorm(vector: number[]): number {
return Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
}
结果接近 1(如 0.999999 到 1.000001)说明已归一化;远大于 1(如 12.4、28.9)则没有归一化。
6.5 工程上最稳妥的做法
不论 embedding 模型是否内部归一化,建议写入和查询两侧都显式归一化一次:
text
写入时:
embeddingModel.embed(text) -> normalizeVector() -> 写入向量库
查询时:
embeddingModel.embed(query) -> normalizeVector() -> 点积检索
这样可以保证:
A⋅B=cos(θ) A \cdot B = \cos(\theta) A⋅B=cos(θ)
不会被向量长度影响。
| 归一化由谁处理 | 场景 | 注意事项 |
|---|---|---|
| embedding 模型自动归一化 | 模型文档说明返回单位向量 | 需确认文档,不能假设 |
| 业务代码自己归一化 | metric = dot,或对结果要求确定 | 最可控,建议默认采用 |
| 向量库内部处理 | metric = cosine | 不同库行为不一,需看文档 |
7. 核心作用总结
| 操作 | 作用 | 解决的问题 |
|---|---|---|
| 归一化 | 把向量统一缩放为单位长度,只保留方向信息 | 消除向量长度差异对相似度判断的干扰 |
| 点积 | 计算两个向量对应分量乘积之和 | 提供衡量向量方向接近程度的数值 |
| 归一化 + 点积 | 点积结果直接等价于 cosine 相似度 | 省去模长计算和除法,简化相似度计算流程 |
8. 一句话总结
归一化让向量只保留方向、去掉长度差异;点积衡量两个向量方向的接近程度;当写入和查询的向量都提前归一化后,点积的计算结果就直接是 cosine 相似度,这也是长期记忆向量检索里判断"语义是否相关"最核心、最基础的数学工具。