之前我完成了“web框架 + GPU 启动 Qwen大模型识别图片并返回信息”的功能,但是我们的目标是让ai用rag能力去查公司内部文档,如果内部文档没有再去外部文档查。
之前我们用的方法比较粗糙,就是“把用户问题按照某个正则规则拆开,提取出关键词,然后去文档里对关键词进行查询,关键字出现次数越多的TOP N文档再给到ai,让ai去理解语意返回内容”。但是这个思路有几个问题:
- 文章太多的话,提取关键词并排序这一步消耗时间很多。
- 技术圈很多是中英文,而且甚至还是有英文缩写,所以拿关键词去查询其实是会漏掉的。
于是,就考虑用“向量数据库”的方法来替代上面那个原始的正则方法。
这里就需要两个开源工具: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 | Embedding:计算开销低,适合在数百万数据的“大海”里捞针。 |
具体的分数判断:
- 第一道门:海选分 (coarse_dist) —— 语义“像不像”
在代码中,你使用了 faiss.normalize_L2 配合 IndexFlatIP,这在数学上计算的就是余弦相似度 (Cosine Similarity)。
数值范围:0 到 1 之间(归一化后)。这个数值跟代码设定变化而变化。
SRE 判定标准:
0.80 左右或以上:说明用户的问题在向量空间里离你的知识库文档(比如那篇磁盘故障博客)非常近。这意味着“话题对上了”。
低于 0.80:说明用户可能在聊一些知识库里完全没提到的东西(比如问天气),语义距离较远。
- 第二道门:精排分 (rerank_score) —— 逻辑“对不对”
这是 bge-reranker-v2-m3 模型给出的深度分。与海选分不同,它不是简单的空间距离,而是模型通过深度神经网络强行理解 Q 和 A 之后的相关性打分。
数值范围:通常在 -10 到 10 之间(取决于模型,BGE 系列通常以 0 为强弱分水岭)。
SRE 判定标准:
1.0 左右甚至更高:这代表“极其精准的命中”。模型认为不仅关键词像,逻辑上也完全匹配,这就是你的“内部私有数据”。
0 左右:属于“疑似相关”。可能聊的是同一个领域,但细节未必匹配。
低于 -1.0:虽然海选可能因为“K8s”字眼给了分,但精排模型发现语义逻辑完全不同,直接判定为“误报”。



Rerank(精排)并不在“归一化”阶段,它处于整个检索链路的最后一步,也就是“计算/检索”之后、送入大模型(LLM)回答之前。简单说是这样的:
1 | 海选 (Retrieval):用户提问向量化,去数据库里按相似度(余弦相似度等)捞出前 10-20 条最相关的 Chunk。 |