在【大模型学习 | RAG & DeepSeek 实战】-腾讯云开发者社区-腾讯云文章中,已经实现了基于RAG建立了本地知识库,通过检索相似度最高的知识来辅助大模型的问答系统。但是,在知识检索和向量存储方面,依然存在着许多不足。例如,在检索向量方面,原文只采用了向量召回的方式,无法满足对于关键词的检索;在存储方面,原文是没有采用任何存储策略的,这也意味着每次加载pdf知识库时都需要重新向量化。为此,基于以上问题,本章对问答系统实现进一步的优化。
def retrieve_docs(query, k=3):
"""检索最相关的k个段落"""
query_vec = model.encode([query])
D, I = index.search(query_vec, k=k) # D是距离,I是索引
return [paragraphs[i] for i in I[0]]
✅ 能理解语义:能找到与 query 相近含义的段落,即使不含相同词语
❌ 容易忽略语义不相近,但有明显的关键词重合;语义相似≠语用相近,尤其在法律、金融等领域问题突出
因此,向量召回可能遇到以下问题:
用户输入 | 单向量召回可能错在哪 |
---|---|
“2024年湖北省大学生竞赛奖金是多少?” | 文中提的是“省部级奖励金额”,但向量模型可能找不到这段 |
“我想了解奖助学金政策” | 向量模型找“奖学金”段落,但“助学金”是关键词 |
“获得奖项后的政策支持?” | 多个段落提到奖项,但真正写“支持”措施的段落可能被漏掉 |
BM25是一个词项的检索匹配算法,通过对比查询与目标段落之间的词项,相同词项越多越相关,但例如the、a等关键词会频繁出现,因此在关键词出现太多时贡献度也会衰减。是TF-IDF
算法的改进版。
def retrieve(self, query, k=5, bm25_k=10):
bm25_scores = self.bm25.get_scores(query.split())
bm25_topk_idx = np.argsort(bm25_scores)[::-1][:bm25_k]
目前可以分别从两个召回器(Faiss + BM25)拿到了不同或者相同的段落,从原先的单通道召回变成了多通道召回。但是,还需要分别从两个召回器中判断哪几段是“最适合回答 query 的。这时候引入rerank 模型(如 BGE-Reranker、ColBERT),对多个段落的得分进行重排序,具体实现:
1️⃣ 将 (query, paragrap
h) 成对送入 BERT/Transformer 分类器
2️⃣ 得到语义相关性分数
3️⃣ 排序并只保留前几条
def retrieve(self, query, k=5, bm25_k=10):
# 向量召回
query_vec = model.encode([query])
D, I = index.search(query_vec, k=k) # D是距离,I是索引
vec_topk_idx = I[0]
# return [paragraphs[i] for i in I[0]]
bm25_scores = self.bm25.get_scores(query.split())
bm25_topk_idx = np.argsort(bm25_scores)[::-1][:bm25_k]
candidate_idxs = list(set(bm25_topk_idx.tolist() + vec_topk_idx.tolist()))
candidate_vecs = self.embeddings[candidate_idxs]
query_vec = query_vec / np.linalg.norm(query_vec) # 归一化
candidate_vecs = candidate_vecs / np.linalg.norm(candidate_vecs, axis=1, keepdims=True)
scores = np.dot(candidate_vecs, query_vec.T).squeeze()
sorted_idx = np.argsort(scores)[::-1]
reranked_idxs = [candidate_idxs[i] for i in sorted_idx[:k]]
return [self.paragraphs[i] for i in reranked_idxs]
模块 | 能力 |
---|---|
Faiss 向量检索 | 找泛义相近文本( |
BM25 检索 | 找关键词重合文本( |
Reranker | 对 Recall 候选排序,挑出真正语义匹配( |
🔴 每次都要重新 encode → 慢、重复。
doc_embeddings = model.encode(paragraphs)
index.add(doc_embeddings)
import pickle
with open("docs.pkl", "wb") as f:
pickle.dump({"paragraphs": paragraphs, "embeddings": doc_embeddings}, f)
# 加载
with open("docs.pkl", "rb") as f:
cache = pickle.load(f)
paragraphs = cache["paragraphs"]
doc_embeddings = cache["embeddings"]
index = faiss.IndexFlatL2(dimension)
index.add(doc_embeddings)
当保存的向量超过几千上万条,内存和查询速度都成问题. 为此,需要对存储向量进行压缩.在原来的代码中,我们没有采用任何的向量压缩,采用了最简单和最基础的检索方式IndexFlatL2
,通过欧氏距离进行相似度计算,为了加快检索速度, faiss
提供了多种压缩方式:
✅ 必须了解底层原理(尤其你是大模型工程师/向量检索方向):
算法 | 核心思想 | 应用场景 |
---|---|---|
PQ(Product Quantization) | 将向量拆分成多个子向量 → 用 8bit 表示每个子块 | 超大数据量,压缩向量 |
IVF(Inverted File) | 建立多个“中心点”(聚类),每次只在最近的几个中心中搜索 | 百万级向量库 |
HNSW(Hierarchical Navigable Small World) | 建图搜索 → 多层邻居结构加速搜索 | 实时性高、近似搜索 |
OPQ(优化 PQ) | 比 PQ 更强的压缩(旋转+重组) | 更高精度压缩 |
index = faiss.IndexPQ(d, M=8, nbits=8)
d
: 向量维度(如 384)M
: 子向量数量(压缩块)nbits
: 每块用几位表示(8bit)IVF
: 建立多个“中心点”(聚类),每次只在最近的几个中心中搜索HNSW
: 建图搜索 → 多层邻居结构加速搜索quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFFlat(quantizer, d, nlist=100)
# index = faiss.IndexIVFPQ(quantizer, dimension, nlist, m, nbits) 压缩 + 近似
index.train(doc_embeddings)
index.add(doc_embeddings)
✅ 本章中实现了对检索和存储方面的优化, 加强了问答系统的检索能力以及存储能力;
✅ 若有该项目的代码需求,可以三连获取 ❗❗❗
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。