RAG生产环境调优:从准确率70%到85%,我踩过的那些坑

4 阅读5分钟

上线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周以后) :评估集 + 全链路日志 + 数据飞轮


踩坑小结

  1. 固定chunk不够用——换Recursive或语义切分
  2. 纯向量检索有gap——必须加BM25关键词检索
  3. Top-K候选噪音多——加reranker精排
  4. 口语和书面语不匹配——query改写一定要做
  5. 不测就不知道——评估集是调优的基础

80%的问题在数据质量,先把知识库文档搞干净比换模型管用。