返回博客
·AI技术

RAG 检索召回率只有 60%?我是这样把它提到 85% 的

RAG 落地没有银弹。分块策略、嵌入模型选型、查询改写、重排序、反馈闭环——五个关键优化点,把召回率从 60% 提到 85% 的实战记录。

#RAG#LLM#向量检索#Embedding#知识库

# RAG 检索召回率只有 60%?我是这样把它提到 85% 的

RAG(Retrieval-Augmented Generation)这几年被炒得很热,几乎所有大模型产品都号称支持。但实际落地的时候,大多数团队的检索效果都不理想——召回率低、相关度差、答案经常牛头不对马嘴。

我带团队做了一个内部知识库问答系统,用了三个月,把召回率从 60% 提到了 85%。这篇文章不讲原理,只讲我踩过的坑和实际的优化手段。

先搞清楚:什么是"召回率"

在这个场景里,召回率指的是:用户问了一个问题,你的检索系统能不能从知识库中找到正确答案所在的文档。

举个例子,用户问"如何重置密码",知识库里有 100 篇文档,其中 3 篇是正确的答案。如果你的系统只召回了其中 1 篇,召回率就是 33%。

60% 的召回率意味着,用户每问 10 个问题,有 4 个你压根没找到答案文档。这还怎么玩?

坑一:分块策略太粗暴

我们最初的方案是:把每篇文档按 500 字切一块,嵌入后存向量数据库。

问题很快暴露了。

有些文档是表格或者代码片段,500 字一刀切,可能把一个完整的代码块切成两半。嵌入模型只看到半截代码,生成的向量没法代表这段代码的语义。

解决方案:按语义边界分块,而不是按字符数。

def smart_chunk(text, max_tokens=500):

"""按段落和标题边界分块,不硬性截断"""

chunks = []

# 先按二级标题分割

sections = re.split(r'(?=##\s)', text)

for section in sections:

if len(section) < max_tokens * 2:

chunks.append(section.strip())

else:

# 再按段落分割

paragraphs = section.split('\n\n')

current = ""

for p in paragraphs:

if len(current) + len(p) > max_tokens:

if current:

chunks.append(current.strip())

current = p

else:

current += "\n\n" + p

if current:

chunks.append(current.strip())

return [c for c in chunks if len(c) > 50]

切完之后,再给每块加上元数据——原文档标题、章节名、最后更新时间。这些信息在检索时能帮上大忙。

坑二:只用一种嵌入模型

我们最初用的是 OpenAI 的 text-embedding-3-small。效果好,但贵,而且只能处理英文语义——我们的知识库大部分是中文技术文档。

后来换成了 BAAI/bge-large-zh,中文效果好很多,但在代码片段和公式的检索上表现拉胯。

最终方案:双模型混合检索。

from langchain.embeddings import HuggingFaceEmbeddings

# 通用语义嵌入

general_embeddings = HuggingFaceEmbeddings(

model_name="BAAI/bge-large-zh"

)

# 代码专用嵌入

code_embeddings = HuggingFaceEmbeddings(

model_name="sentence-transformers/all-MiniLM-L6-v2"

)

检索时,先判断用户问题是代码相关还是通用问题(用关键词匹配快速分类),然后分别用对应的嵌入模型做向量检索,最后合并结果去重。

这个方法把代码类问题的召回率从 45% 提升到了 72%。

坑三:忽略了查询改写

用户的问题往往很短、很模糊。"密码不对怎么办"——这种问题,你的知识库里有"密码重置流程"、"登录失败处理"、"账户安全设置"等多篇相关文档,但向量检索可能只匹配到最字面上相关的那一篇。

我们在检索前加了一个查询改写步骤:

from langchain.prompts import PromptTemplate

from langchain.chains import LLMChain

rewrite_prompt = PromptTemplate(

input_variables=["question"],

template="""你是一个知识库检索助手。用户的问题是:{question}

请把它改写成3个不同的检索查询,覆盖可能的关键词和语义角度。

只输出查询列表,每行一个,不要解释。

改写后的查询:"""

)

chain = LLMChain(llm=llm, prompt=rewrite_prompt)

queries = chain.run(question=user_question).strip().split('\n')

# 对每个改写后的查询做向量检索,合并去重

这一步把通用类问题的召回率从 55% 提升到了 78%。

坑四:向量检索后没有重排序

向量检索的问题是:它只关心语义相似度,不关心文档质量和相关性排序。

我们的知识库里有 1000 篇文档,向量检索返回 top-10,但这 10 篇里可能只有 3 篇是真正相关的。

解决方案:用 Cross-Encoder 做重排序。

from sentence_transformers import CrossEncoder

cross_encoder = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')

# 向量检索召回 top-20

candidate_docs = vector_search(query, top_k=20)

# Cross-Encoder 重新打分排序

pairs = [[query, doc.content] for doc in candidate_docs]

scores = cross_encoder.predict(pairs)

# 取 top-5

ranked_docs = sorted(

zip(candidate_docs, scores),

key=lambda x: x[1],

reverse=True

)[:5]

Cross-Encoder 比 Bidirectional Encoder 慢,但准确率高很多。我们只在最终排序阶段用,召回阶段还是用向量检索。这个组合把最终答案的相关性从 60% 提升到了 85%。

坑五:没有反馈闭环

这是最后补上的一步,也是效果最明显的一步。

我们在每次问答之后,让用户打分(有用/没用),并记录检索结果和最终答案。三个月积累了 2000+ 条反馈,我拿这些数据做了两件事:

  • 把"没用"的查询和对应的失败检索结果拿出来,手动修正分块策略和嵌入参数。
  • 2. 把"有用"的高质量问题作为种子,用 LLM 生成更多类似的测试用例,持续评估检索效果。

    没有这个闭环,你根本不知道自己优化了哪里、效果如何。

    总结

    RAG 落地没有银弹,但有个思路是对的:

    > 召回率 = 好的分块 + 合适的嵌入模型 + 查询改写 + 重排序 + 反馈迭代

    我们花了三个月才把召回率从 60% 提到 85%,如果一开始就知道这些坑,可能两周就够了。

    最重要的是:不要只调一个参数,要系统地看整个 pipeline。召回率低的原因可能不在检索层,而在分块层或者查询层。


    *写于 2026-08-18,一个被 RAG 召回率折磨三个月后的顿悟*