上线RAG系统三个月,准确率卡在70%上不去,用户反馈"答非所问"——这大概是2026年做RAG的工程师都经历过的噩梦。
我去年Q4上线了内部知识库问答系统,最初用固定chunk+向量检索,召回率68%。后面花了两个多月调优,爬到83%。踩的坑分享出来,给大家避避雷。
痛点在哪
准确率上不去,90%的情况问题在检索,不在生成。别急着换模型,先把检索链路跑通。
一、chunk策略:第一步走错后面全废
问题场景
固定chunk切出来经常是半句话:
# 原始文档
"查询用户信息的接口,请求参数包含userId和pageSize,返回用户列表和总数"
# 固定chunk切完
chunk1 = "查询用户信息的接口,请求参数包含userId"
chunk2 = "和pageSize,返回用户列表和总数"
用户问"查询接口的参数是什么",检索出来的chunk只有userId,漏掉了pageSize。
解决方案:递归切分
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n", ". ", " ", ""],
chunk_size=512,
chunk_overlap=64,
length_function=len,
add_start_index=True,
)
chunks = splitter.split_documents(documents)
按段落、句子、单词逐层切,会尽量在自然断点停手。
overlap设的64,大概10%多一点的chunk size。overlap太大浪费存储,候选集也会膨胀影响速度。
语义切分什么时候用
文档结构混乱、主题杂糅的情况才需要语义切分。常规场景Recursive够用,别过度设计。
二、混合检索:向量+BM25是标配
纯向量检索找的是"语义相似",不是"关键词匹配"。
问题场景
用户问"怎么处理空指针异常",系统返回"异常处理的最佳实践",语义相关但没有"空指针"三个字。
解决方案:RRF融合
from collections import defaultdict
def rrf_fusion(results_list, k=60):
"""
RRF (Reciprocal Rank Fusion) 融合多个检索结果
results_list: [[doc1, doc2, ...], [doc_a, doc_b, ...]]
"""
fused = defaultdict(float)
for results in results_list:
for rank, doc in enumerate(results):
fused[doc.id] += 1 / (k + rank + 1)
return sorted(fused.items(), key=lambda x: x[1], reverse=True)
# 使用
vector_results = vector_store.similarity_search(query, k=20)
bm25_results = bm25_index.search(query, k=20)
fused_results = rrf_fusion([vector_results, bm25_results], k=60)
权重向量:BM25 = 7:3。技术文档可以适当提高关键词权重,5:5也不是不行。
三、reranker:提升最明显的一步
向量检索是近似最近邻,会漏掉相关结果、也会塞进来噪音。
问题场景
Top-10里经常有2-3条是语义相邻但实际不相关的chunk,放到上下文里干扰生成。
解决方案:Cross-Encoder精排
from FlagEmbedding import BgeReranker
# 初始化reranker,本地部署
reranker = BgeReranker(
model_name="BAAI/bge-reranker-v2-m3",
device="cuda" # A10G跑这个完全OK
)
# 从50个候选中选5个最相关的
candidate_chunks = top_50_from_fusion
reranked = reranker.compress_documents(
query=query,
documents=candidate_chunks,
top_n=5
)
# 传Top-5给LLM,不要传太多
final_context = [doc.content for doc in reranked]
实测准确率提升8-15个百分点。A10G上延迟50-100ms,完全可接受。
2026年reranker选型
| 模型 | 部署 | 延迟 | 特点 |
|---|---|---|---|
| Cohere Rerank 3 | 云服务 | <50ms | 按量付费,快速上手 |
| BGE-Reranker-v2 | 本地 | 50-100ms | 开源,数据安全 |
| Voyage AI Rerank-2 | 云/本地 | 50-100ms | 长文档支持好 |
四、查询改写:提升检索质量的隐藏大招
用户口语 vs 知识库书面语,gap靠检索补不回来。
问题场景
同样query,改写前和改写后检索,相关文档重合率只有60%。
解决方案:HyDE
def hyde_retrieve(query: str, vector_store, llm) -> list:
"""
HyDE: 先让LLM生成假设答案,用答案去检索
原理:答案文本比问题更接近知识库的内容分布
"""
# 生成假设答案
hypothetical_prompt = f"""根据问题生成一个假设的答案。
问题:{query}
假设答案:"""
hypothetical_answer = llm.predict(hypothetical_prompt)
# 用假设答案去检索
docs = vector_store.similarity_search(hypothetical_answer, k=5)
return docs
# Multi-Query示例
def multi_query_retrieve(query: str, vector_store, llm) -> list:
"""生成多个角度的子查询,并行检索后合并"""
angle_prompt = f"""针对这个问题,生成3个不同角度的子查询。
问题:{query}
子查询(JSON数组格式):"""
sub_queries = json.loads(llm.predict(angle_prompt))
# 并行检索
all_results = []
for sq in sub_queries:
all_results.extend(vector_store.similarity_search(sq, k=5))
# 去重返回
return deduplicate(all_results)
实测HyDE对模糊query效果意外地好。
五、完整代码示例
from langchain_text_splitters import RecursiveCharacterTextSplitter
from FlagEmbedding import BgeReranker
from collections import defaultdict
class OptimizedRAG:
def __init__(self):
# 1. 切分器
self.splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=64
)
# 2. 向量库 (Milvus/Qdrant/Chroma都行)
self.vector_store = None # 初始化你的向量库
# 3. BM25索引
self.bm25_index = None # 初始化BM25
# 4. Reranker
self.reranker = BgeReranker(
model_name="BAAI/bge-reranker-v2-m3",
device="cuda"
)
def hybrid_search(self, query: str, k: int = 20):
"""混合检索:向量 + BM25 + RRF融合"""
# 向量检索
vector_results = self.vector_store.similarity_search(query, k=k)
# BM25检索
bm25_results = self.bm25_index.search(query, k=k)
# RRF融合
fused = self.rrf_fusion([vector_results, bm25_results])
return fused[:k*2] # 返回2k候选给reranker
def rrf_fusion(self, results_list, k=60):
fused = defaultdict(float)
for results in results_list:
for rank, doc in enumerate(results):
fused[doc.id] += 1 / (k + rank + 1)
return sorted(fused.items(), key=lambda x: x[1], reverse=True)
def retrieve(self, query: str, top_k: int = 5):
"""完整检索流程"""
# 1. 混合检索拿候选
candidates = self.hybrid_search(query)
# 2. Reranker精排
candidate_docs = [self.get_doc_by_id(doc_id) for doc_id, _ in candidates]
reranked = self.reranker.compress_documents(
query=query,
documents=candidate_docs,
top_n=top_k
)
return reranked
def get_doc_by_id(self, doc_id):
# 实现根据ID获取文档的逻辑
pass
六、调优顺序建议
第一阶段(第1-2周) :PyMuPDF + Recursive chunking + BGE-M3,跑通baseline
第二阶段(第3-4周) :混合检索 + BGE-Reranker-v2 + 查询改写,准确率提升最明显的阶段
第三阶段(第5周以后) :评估集 + 全链路日志 + 数据飞轮
踩坑小结
- 固定chunk不够用——换Recursive或语义切分
- 纯向量检索有gap——必须加BM25关键词检索
- Top-K候选噪音多——加reranker精排
- 口语和书面语不匹配——query改写一定要做
- 不测就不知道——评估集是调优的基础
80%的问题在数据质量,先把知识库文档搞干净比换模型管用。