一、前言
在企业知识库、智能客服、内部文档问答等场景中,RAG 最常见、最致命的问题就是:召回不准。
用户明明问的是 “退款规则”,向量库却返回 “发票流程”“会员权益”“账号注销” 等不相关内容。召回错了 → 大模型回答必错。
本文不讲理论,只讲工程可落地、能直接提升召回准确率的实操方法,包含:
- 为什么召回会不准
- 文档切片优化
- 混合检索(向量 + BM25)
- 结果重排与过滤
- 元数据过滤
- 完整可运行代码
二、召回不准的 5 个核心原因
1. 文档切片太粗暴
直接按字符长度硬切,导致一个文本块包含多个知识点,向量混乱,匹配自然不准。
2. 只依赖向量检索
向量擅长 “语义相似”,但不擅长 “精确匹配”。 例如:退款 ≠ 退费 ≠ 报销,纯向量很容易混淆。
3. 没有重排过滤
向量库返回的 Top5 结果里,往往只有 1~2 条真正相关,其余都是噪音。
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_size=250, overlap=50): 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_name="all-MiniLM-L6-v2" ) collection = client.get_or_create_collection( name="company_kb", embedding_function=embedding_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( documents=chunks, metadatas=metadatas, ids=[f"id_{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_k=5): # 向量检索 vec_res = collection.query( query_texts=[query], n_results=top_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_score=0.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(reverse=True, key=lambda 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 条实操经验
- 块大小控制在200~300 字符最合适
- 中文场景优先使用bge-small-zh或m3e
- 必须加元数据:类型、产品、有效期、状态
- 必须使用向量 + BM25 混合检索
- 相似度过滤阈值建议0.35~0.5
- 规则更新后及时删除旧向量
- 短查询 → top_k=6;长查询 → top_k=3
七、总结
RAG 召回不准,90% 不是大模型的问题,而是检索环节的问题。 只要做好以下四点,准确率立刻提升:
- 语义切片
- 混合检索
- 重排过滤
- 元数据筛选