刚开始搭建智能体(Agent)项目时,我用向量进行相似性匹配,结果遇到了一个相当诡异的问题。
我构建了一个简单的RAG系统:将技术文档切块,计算 embedding,然后存入向量数据库。整个流程看起来没问题,但检索效果就是不理想——无论我换用 OpenAI 的 embedding、阿里云的 text-embedding,还是 m3e 模型,结果都一样糟糕。
例如,我查询「如何配置 Redis 集群」,排名最前的永远是一篇几千字的架构设计文档,里面只是顺带提到了 Redis。而真正讲解 Redis 集群配置的那篇短文档,却总是排在后面。
起初我以为是模型能力不行,折腾了好几天,调整参数、更换模型,全都试了一遍,毫无作用。最后才发现,问题根源根本不在模型上——是我在计算相似度时,直接使用了欧氏距离,而向量根本没有经过归一化处理。
说实话,“归一化”这个概念,对于算法工程师而言可能是常识。但我更偏向工程开发,之前没踩过这个坑,这次算是实实在在“肉身滚雷”了。
问题到底出在哪?
先把结论放在前面:向量的模长(长度)本身会影响相似度计算的结果,而在很多场景下,我们其实并不关心长度,只关心向量的方向。
举一个当时最直观的例子。
假设有两篇文档:
文档 A:5000 字的架构设计文档
提到 [Redis:50 次, 集群:50 次, 配置:50 次]
文档 B:500 字的配置说明文档
提到 [Redis:5 次, 集群:5 次, 配置:5 次]
如果直接用欧氏距离去计算它们向量的距离:
distance = √[(50-5)² + (50-5)² + (50-5)²]
= √[45² × 3]
≈ 78
数值看起来相差很大,仿佛这两篇文档毫不相关。但你仔细一想就会发现,它们讨论的主题分布其实是一模一样的——都是在讲 Redis、集群和配置,仅仅是篇幅长短不同。
问题就在这里:我真正想比较的是文档“在讲什么主题”,结果却被“写了多少字”(即向量长度)这个无关因素带偏了。
因此,开篇提到的检索异常现象就很好解释了:长文档因为对应的向量更长,在欧氏距离度量下天然更“突出”;短文档哪怕内容更精准,也很容易被挤到后面去。
归一化到底做了什么?
归一化本质上只做一件事:消除向量的长度差异,只保留方向信息。
最常见的是 L2 归一化,公式非常简单:
归一化后的向量 = 原向量 / 向量的模长 (L2范数)
还是上面那个例子,进行 L2 归一化之后:
文档 A:[50, 50, 50] → [0.577, 0.577, 0.577]
文档 B:[5, 5, 5] → [0.577, 0.577, 0.577]
你会发现,两篇文档的向量变得完全一致了。这其实才符合我们的直觉:它们的主题分布本来就是完全相同的。
用图形来理解,可以把归一化想象成:将所有向量都“压缩”或“投影”到同一个单位球面上。此时,比较的就只剩下它们的方向(即球面上的点与原点连线的指向)了。
不只是文档长短的问题
上面这个坑,是我最初遇到的。但随着系统继续开发,我逐渐意识到,归一化解决的远不止“文档篇幅影响权重”这一个问题。
例如,不同 embedding 模型输出的向量,其各个维度的数值范围可能差异巨大。有的维度数值在 [-1, 1] 区间,有的可能在 [-10, 10] 区间。如果不进行归一化,那些数值范围大的维度在相似度计算中就会占据主导地位,其他维度的贡献则被严重削弱。
再比如,在进行混合检索(Hybrid Search)时,这个问题会更加明显:
关键词匹配得分:0 ~ 100
向量相似度得分:0 ~ 1
如果直接将这两个分数加权融合,向量相似度得分基本就失去了意义。只有先将它们归一化,拉回到相近的数值尺度上,后续的融合才有意义。
关于余弦相似度的一个工程现实
有人可能会提出:余弦相似度的公式里不是已经除以向量模长了吗?
公式确实如此:
cos(θ) = (A · B) / (||A|| × ||B||)
但在工程实践中,我们通常还是会选择预先将向量归一化,原因非常实际。
第一是性能考量。如果向量已经归一化,那么余弦相似度的计算就可以简化为点积(内积):
cos(θ) = A · B (当 A 和 B 均为单位向量时)
当向量数据量级上来之后,省去每次计算模长(涉及平方根运算)和除法的开销,确实能节省可观的计算时间。
第二是省心。许多向量数据库(如 Milvus、Pinecone 等)在构建索引时,本身就要求输入的向量是归一化的。与其在查询时让数据库临时处理,不如在数据入库前就统一处理好,避免潜在的兼容性或性能问题。
注意:有些场景,不要随意归一化
当然,也并非所有向量都适合进行归一化。关键在于判断:向量长度本身是否携带了有意义的信息。
例如,物理中的速度向量,其模长代表速度大小。如果对其进行归一化,速度信息就丢失了,只剩下方向,这显然是错误的。
在 RAG 场景中,也可能存在类似情况。我曾见过一些系统,文档向量的模长可能与“信息密度”、“权威性得分”等指标存在一定关联。如果无脑地对所有向量进行归一化,这个潜在的有用信号就会被抹除,反而可能导致排序效果下降。
更稳妥的做法是:如果长度信息有用,就把它单独提取出来作为一个特征使用,不要让它干扰方向相似度的计算。
工程实践中,归一化步骤放在哪里?
回顾我的踩坑经历,归一化放在以下环节最为合适:
- 入库前处理:在将文档向量存入向量数据库之前,统一进行 L2 归一化。这样做的好处是只计算一次,无需每次查询时重复计算。
- 查询时处理:对查询向量进行完全相同的归一化处理。
- 使用点积检索:由于向量均已归一化,直接使用点积(内积)进行相似度计算即可,其结果等价于余弦相似度,且计算更高效。
这种做法对 HNSW、IVF 等常用索引结构更为友好,也使查询时的逻辑保持简洁。
回到最初的问题
最初那个 RAG 检索效果差的问题,最终就是通过以下步骤解决的:
- 文档向量入库前,统一进行 L2 归一化。
- 查询向量也进行同样的归一化处理。
- 使用余弦相似度(归一化后简化为点积)进行检索。
修改之后效果立竿见影,那篇 500 字的 Redis 配置文档,终于在「如何配置 Redis 集群」这个查询下排到了首位。
后来我才了解到,其实很多向量数据库的内部实现已经默认包含了归一化步骤。只是我一开始没有留意,想当然地认为“模型输出什么就存什么”,这算是一个典型的工程实践认知坑。
最后一点总结
归一化这个概念本身并不深奥,但它直指一个核心问题:你希望“相似度”这个度量究竟反映什么?
- 如果你关心的是方向一致性(例如主题分布),那就应该进行归一化。
- 如果向量长度本身承载了重要信息(如强度、权重),那就不要盲目归一化,可以考虑将长度信息单独建模。
想清楚你究竟要比较什么,远比死记硬背几个公式或操作步骤更重要。希望我在智能体项目里踩过的这个坑,能帮你避开类似的陷阱。对于更多关于相似度计算、embedding 优化等算法与实践的讨论,也欢迎到云栈社区与大家交流。