0%

使用向量化embedding来做RAG

之前我完成了“web框架 + GPU 启动 Qwen大模型识别图片并返回信息”的功能,但是我们的目标是让ai用rag能力去查公司内部文档,如果内部文档没有再去外部文档查。

之前我们用的方法比较粗糙,就是“把用户问题按照某个正则规则拆开,提取出关键词,然后去文档里对关键词进行查询,关键字出现次数越多的TOP N文档再给到ai,让ai去理解语意返回内容”。但是这个思路有几个问题:

  1. 文章太多的话,提取关键词并排序这一步消耗时间很多。
  2. 技术圈很多是中英文,而且甚至还是有英文缩写,所以拿关键词去查询其实是会漏掉的。

于是,就考虑用“向量数据库”的方法来替代上面那个原始的正则方法。

这里就需要两个开源工具:faiss 和 paraphrase-multilingual-MiniLM-L12-v2。

四个阶段

第一阶段:数据清洗以及其他准备工作
清洗/预处理:去掉 HTML 标签、特殊字符等。

分片 (Chunking):按照你设定的字段大小(Chunk Size)和重叠度(Overlap)将长文本切开。

为啥要分片?所有的 Embedding 模型(包括你可能在用的 bge-small-zh 或同类模型)都有一个输入长度限制(通常是 512 或 1024 个 Token)。如果你不先做 Chunking,直接把几万字的系统日志或超长技术文档塞进去,模型会自动截断后面的内容。这会导致你的向量只代表了文档的开头,丢失了关键的故障信息。

Chunk 过大:一段文本里如果既讲了“Pod 驱逐”,又讲了“GPU 调度”,还讲了“网络优化”,那么生成的向量会非常“模糊”,检索时的精度会大幅下降。

Chunk 适中:向量能精准指向“磁盘满导致的 Pod 驱逐”这个具体语义。

第二阶段:向量化 (The Embedding)
在代码里,我们用了 SentenceTransformer 模型。它的作用是把非结构化的文字(比如“机房掉电”)转化成固定长度的数字列表(向量)。

MiniLM 模型的奥秘:用paraphrase-multilingual-MiniLM-L12-v2会把任何长度的句子变成一个 384 维 的数组。

语义空间:这个数组就像是文字在“AI 脑海”里的经纬度。语义相近的词(比如“掉电”和“断电”),在这个 384 维空间里的距离会非常近。

1
清洗 -> 分片 (Chunking) -> 向量化 (Embedding) ->  存入向量数据库。

第三阶段:归一化 (The Normalization) —— 这是调优的灵魂。

faiss.normalize_L2(embeddings)
为什么要归一化?
原始生成的向量,每个向量的“长度”(模长)是不一样的。有的长,有的短。

如果不归一化:用欧氏距离(L2)计算时,向量的“长度”会严重干扰“方向”。一个很长但方向偏差一点的向量,和一个很短但方向完全一致的向量,算出来的距离可能非常大。

归一化之后:所有向量都被“强行”拉到了一个半径为 1 的单位圆/球上。此时,我们不再看两个点之间的物理距离,而是看两个向量之间的夹角。

第四阶段:相似度计算 (Cosine Similarity via Inner Product)
归一化后,代码切换到了faiss.IndexFlatIP(内积索引)。

逻辑转换:在向量长度都是 1 的情况下,内积(Inner Product)就等于余弦相似度(Cosine Similarity)。

数值含义:
1.0:方向完全一致,语义完全相同。
0.0:方向垂直,语义完全无关。
-1.0:方向相反,语义截然不同。

一般来说,相似度达到0.7左右就OK。为什么 0.7 左右效果好?因为 0.7 左右代表两个向量的夹角大约是 45°。在 384 维这种极高维度下,能撞到 45° 以内的夹角,说明两句话的关键词和语境已经高度重合了。

最后效果

相关的代码就在 https://github.com/BruceWayne2099/gpu-test-main/blob/main/backend.py 里了。

最后附上实际效果:

有些内部文档ai还是搜不出来,这里还是要继续优化,一方面是优化chunk的长度和取关键词的逻辑,另一方面是文档要规范,尽可能多的体现出关键词,这样命中率更高。

我看字节是有一个自带的RAG平台,这个功能单独做出来一个能力,真的很方便,可以让使用方快速落地。

由于我这个是相对测试的功能,所以用爬虫来获取内部文档信息,爬到ai服务器上。如果是大量内部文档,比如飞书或者钉钉语雀那种级别,我记得他们是有文档api的,直接搭配应用,可以快速返回“标题、摘要、链接”,这样让ai更快拿到关键词的信息。

注意一下,可能代码里写了一些print打印信息,但是在kubectl logs -f看不到输出的话,可以在print打印语句后面加上 flush=True,或者在 K8s 环境变量中设置 PYTHONUNBUFFERED: "1"。因为Python 容器的标准输出存在缓冲区机制,小字节的日志(如一行 Dist 分数)会被憋在内存里不喷出来。

引入rerank升级一下

完成了之后我发现,AI取到内部文档的成功率比较低,除非内部文档有大量的关键词。于是我引入了rerank,解决“搜得到”但“搜不准”的问题。

选择的重排模型是:https://huggingface.co/BAAI/bge-reranker-v2-m3/tree/main

1
2
3
4
5
6
7
Embedding:计算开销低,适合在数百万数据的“大海”里捞针。

Rerank:计算开销高(需要 GPU 或高性能 CPU),不适合处理大数据量。

先用 Embedding 从几千条 K8s 运维文档里,快速捞出最像答案的 20 条。,再用 Rerank 对这 20 条进行“精挑细选”。比如用户问“如何处理磁盘满”,Embedding 可能会捞出“如何清理日志”,也会捞出“如何挂载云盘”。Reranker 会根据语义深度,把真正解决问题的“清理日志”排到第一名。

这种**“快慢结合”**的方案,是目前生产环境 AIOps 系统的标准工业实践。

具体的分数判断:

  1. 第一道门:海选分 (coarse_dist) —— 语义“像不像”
    在代码中,你使用了 faiss.normalize_L2 配合 IndexFlatIP,这在数学上计算的就是余弦相似度 (Cosine Similarity)。

数值范围:0 到 1 之间(归一化后)。这个数值跟代码设定变化而变化。

SRE 判定标准:

0.80 左右或以上:说明用户的问题在向量空间里离你的知识库文档(比如那篇磁盘故障博客)非常近。这意味着“话题对上了”。
低于 0.80:说明用户可能在聊一些知识库里完全没提到的东西(比如问天气),语义距离较远。

  1. 第二道门:精排分 (rerank_score) —— 逻辑“对不对”
    这是 bge-reranker-v2-m3 模型给出的深度分。与海选分不同,它不是简单的空间距离,而是模型通过深度神经网络强行理解 Q 和 A 之后的相关性打分。

数值范围:通常在 -10 到 10 之间(取决于模型,BGE 系列通常以 0 为强弱分水岭)。

SRE 判定标准:
1.0 左右甚至更高:这代表“极其精准的命中”。模型认为不仅关键词像,逻辑上也完全匹配,这就是你的“内部私有数据”。
0 左右:属于“疑似相关”。可能聊的是同一个领域,但细节未必匹配。
低于 -1.0:虽然海选可能因为“K8s”字眼给了分,但精排模型发现语义逻辑完全不同,直接判定为“误报”。

Rerank(精排)并不在“归一化”阶段,它处于整个检索链路的最后一步,也就是“计算/检索”之后、送入大模型(LLM)回答之前。简单说是这样的:

1
2
3
4
5
6
7
8
9
海选 (Retrieval):用户提问向量化,去数据库里按相似度(余弦相似度等)捞出前 10-20 条最相关的 Chunk。

归一化 (Normalization):这是海选算法内部的数学步骤,用来保证不同量级的向量能在同一标准下比较。

精排 (Rerank) <--- 在这里!:将海选出的 20 条结果和用户问题一起塞给 bge-reranker-v2-m3 模型。它不看向量距离,而是深度理解两句话的语义关系,重新打分排序。

截断 (Top-K):取精排分最高的前 3-5 条。

生成 (Generation):把这最准的几条塞给 Llama3,让它组织语言回答。
感谢你请我喝咖啡~

欢迎关注我的其它发布渠道