找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2277

积分

0

好友

321

主题
发表于 3 天前 | 查看: 9| 回复: 0

刚开始搭建智能体(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 场景中,也可能存在类似情况。我曾见过一些系统,文档向量的模长可能与“信息密度”、“权威性得分”等指标存在一定关联。如果无脑地对所有向量进行归一化,这个潜在的有用信号就会被抹除,反而可能导致排序效果下降。

更稳妥的做法是:如果长度信息有用,就把它单独提取出来作为一个特征使用,不要让它干扰方向相似度的计算。

工程实践中,归一化步骤放在哪里?

回顾我的踩坑经历,归一化放在以下环节最为合适:

  1. 入库前处理:在将文档向量存入向量数据库之前,统一进行 L2 归一化。这样做的好处是只计算一次,无需每次查询时重复计算。
  2. 查询时处理:对查询向量进行完全相同的归一化处理。
  3. 使用点积检索:由于向量均已归一化,直接使用点积(内积)进行相似度计算即可,其结果等价于余弦相似度,且计算更高效。

这种做法对 HNSW、IVF 等常用索引结构更为友好,也使查询时的逻辑保持简洁。

回到最初的问题

最初那个 RAG 检索效果差的问题,最终就是通过以下步骤解决的:

  1. 文档向量入库前,统一进行 L2 归一化。
  2. 查询向量也进行同样的归一化处理。
  3. 使用余弦相似度(归一化后简化为点积)进行检索。

修改之后效果立竿见影,那篇 500 字的 Redis 配置文档,终于在「如何配置 Redis 集群」这个查询下排到了首位。

后来我才了解到,其实很多向量数据库的内部实现已经默认包含了归一化步骤。只是我一开始没有留意,想当然地认为“模型输出什么就存什么”,这算是一个典型的工程实践认知坑。

最后一点总结

归一化这个概念本身并不深奥,但它直指一个核心问题:你希望“相似度”这个度量究竟反映什么?

  • 如果你关心的是方向一致性(例如主题分布),那就应该进行归一化。
  • 如果向量长度本身承载了重要信息(如强度、权重),那就不要盲目归一化,可以考虑将长度信息单独建模。

想清楚你究竟要比较什么,远比死记硬背几个公式或操作步骤更重要。希望我在智能体项目里踩过的这个坑,能帮你避开类似的陷阱。对于更多关于相似度计算、embedding 优化等算法与实践的讨论,也欢迎到云栈社区与大家交流。




上一篇:面试官:高并发下缓存一致性方案如何设计?
下一篇:深度解析Linux文件系统核心:inode结构与文件目录原理详解
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 01:45 , Processed in 0.413635 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表