向量库搭配 RAG 开发:解决知识库问答召回不准的实操技巧
一、前言在企业知识库、智能客服、内部文档问答等场景中RAG 最常见、最致命的问题就是召回不准。用户明明问的是 “退款规则”向量库却返回 “发票流程”“会员权益”“账号注销” 等不相关内容。召回错了 → 大模型回答必错。本文不讲理论只讲工程可落地、能直接提升召回准确率的实操方法包含为什么召回会不准文档切片优化混合检索向量 BM25结果重排与过滤元数据过滤完整可运行代码二、召回不准的 5 个核心原因1. 文档切片太粗暴直接按字符长度硬切导致一个文本块包含多个知识点向量混乱匹配自然不准。2. 只依赖向量检索向量擅长 “语义相似”但不擅长 “精确匹配”。 例如退款 ≠ 退费 ≠ 报销纯向量很容易混淆。3. 没有重排过滤向量库返回的 Top5 结果里往往只有 12 条真正相关其余都是噪音。4. 没有元数据过滤旧规则、废弃文档、无关分类文档一起参与检索导致结果混乱。5. Embedding 模型不匹配通用英文模型或轻量模型对行业术语、业务关键词支持很差。三、最优解决方案四步稳定提升召回率语义化切片一个块只保留一个知识点混合检索向量 BM25 关键词重排过滤剔除低相关片段元数据前置过滤缩小检索范围这套方案在实际项目中能让召回准确率提升 50%90%。四、完整可运行代码结构清晰版1. 安装依赖bash运行pip install chromadb sentence-transformers rank_bm25 numpy2. 完整代码干净格式python运行import chromadb import re import numpy as np from chromadb.utils import embedding_functions from rank_bm25 import BM25Okapi # # 1. 语义分块避免知识点混乱 # def split_by_semantic(text, chunk_size250, overlap50): sentences re.split(r[。\n], text) sentences [s.strip() for s in sentences if s.strip()] chunks [] current_chunk [] current_length 0 for sent in sentences: if current_length len(sent) chunk_size and current_chunk: chunks.append(.join(current_chunk)) overlap_text .join(current_chunk)[-overlap:] current_chunk [overlap_text] current_length len(overlap_text) current_chunk.append(sent) current_length len(sent) if current_chunk: chunks.append(.join(current_chunk)) return chunks # # 2. 初始化向量库与嵌入模型 # client chromadb.PersistentClient(path./rag_db) embedding_model embedding_functions.SentenceTransformerEmbeddingFunction( model_nameall-MiniLM-L6-v2 ) collection client.get_or_create_collection( namecompany_kb, embedding_functionembedding_model ) # # 3. 知识库示例 # knowledge_text 会员退款规则开通7天内未使用权益可全额退款。 发票申请流程订单完成30天内可在个人中心申请电子发票。 会员等级权益黄金会员每月5次免费提问铂金会员20次。 账号注销规则注销前需清空余额数据不可恢复。 密码找回流程登录页点击忘记密码手机验证后重置。 # 切片 chunks split_by_semantic(knowledge_text) # 元数据标记 metadatas [] for chunk in chunks: if 退款 in chunk: cat refund elif 发票 in chunk: cat invoice elif 会员 in chunk: cat member elif 注销 in chunk: cat account elif 密码 in chunk: cat password else: cat other metadatas.append({category: cat, status: valid}) # 入库 collection.add( documentschunks, metadatasmetadatas, ids[fid_{i} for i in range(len(chunks))] ) # # 4. BM25 关键词检索 # tokenized_corpus [c.split() for c in chunks] bm25 BM25Okapi(tokenized_corpus) # # 5. 混合检索向量 BM25 # def hybrid_search(query, top_k5): # 向量检索 vec_res collection.query( query_texts[query], n_resultstop_k, where{status: valid} ) vec_docs vec_res[documents][0] # BM25 检索 tokens query.split() scores bm25.get_scores(tokens) top_idx np.argsort(scores)[::-1][:top_k] bm25_docs [chunks[i] for i in top_idx] # 合并去重 combined list(dict.fromkeys(vec_docs bm25_docs)) return combined[:top_k] # # 6. 重排与过滤去掉不相关 # def rerank(query, docs, min_score0.4): q_emb embedding_model([query])[0] scored [] for doc in docs: d_emb embedding_model([doc])[0] sim np.dot(q_emb, d_emb) / (np.linalg.norm(q_emb) * np.linalg.norm(d_emb)) if sim min_score: scored.append((sim, doc)) scored.sort(reverseTrue, keylambda x: x[0]) return [d for s, d in scored] # # 7. 最终 RAG 检索入口 # def rag_retrieve(query): docs hybrid_search(query) final rerank(query, docs) return \n---\n.join(final) if final else 未找到相关内容 # # 测试 # if __name__ __main__: question 会员多久可以全额退款 result rag_retrieve(question) print(用户问题, question) print(\n召回结果\n, result)五、每一步优化的作用清晰易懂1. 语义分块避免一个块包含多个知识点让向量更 “纯粹”匹配更准。2. 元数据过滤可以按业务类型、状态、时间过滤从源头减少无关内容。3. 混合检索向量抓语义BM25抓关键词 两者互补召回准确率大幅提升。4. 重排过滤过滤掉低相似度片段不让噪音进入大模型。六、上线必看的 7 条实操经验块大小控制在200300 字符最合适中文场景优先使用bge-small-zh或m3e必须加元数据类型、产品、有效期、状态必须使用向量 BM25 混合检索相似度过滤阈值建议0.350.5规则更新后及时删除旧向量短查询 → top_k6长查询 → top_k3七、总结RAG 召回不准90% 不是大模型的问题而是检索环节的问题。 只要做好以下四点准确率立刻提升语义切片混合检索重排过滤元数据筛选