RAG 检索召回率只有 60%?我是这样把它提到 85% 的
RAG 落地没有银弹。分块策略、嵌入模型选型、查询改写、重排序、反馈闭环——五个关键优化点,把召回率从 60% 提到 85% 的实战记录。
# 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 召回率折磨三个月后的顿悟*