前情回顾
上篇文章最后遗留了一个问题:
RAG私有知识库共有里有三篇文档——曹操介绍、诸葛亮介绍、赤壁之战介绍。问曹操能答,问诸葛亮却回答"文档中未提及"。
打印检索日志后发现:问"诸葛亮是哪里人"时,检索到的 3 个片段全部来自 caocao.txt。
第一反应是调参——把 chunk_size 调小(从400到300再到200),希望每个片段语义更纯粹;把 top_k 调大(3-5),希望覆盖更多候选。但反复试验后效果依然不理想,诸葛亮的问题还是答不对。
问题出在更上游:这三篇文档本身就高度交织。诸葛亮的介绍里多次提到曹操,曹操的介绍里也提到了赤壁和诸葛亮。无论 chunk 切得多细,只要文档内容互相穿插,向量检索计算的整体语义相似度就会"认错人"——它找到的是语义最近的片段,而不是名字最匹配的片段。
这是向量检索的结构性缺陷,调参只是在错误的方向上用力。真正的解法需要从检索策略上改进。本文将记录几个改进检索效果的方法。
一、混合检索
纯向量搜索的缺陷
向量搜索擅长语义相似,但对精确的专有名词处理不好。比如搜索"BGE-M3 的参数量",向量搜索可能把"大规模语言模型参数"排在最前面,但 BGE-M3 这个型号就是没出现在结果里。BM25 是传统的关键词搜索算法,会优先匹配完全相同的词——两者互补,结合起来能兼顾语义和精确匹配。
安装依赖
pip install rank_bm25 langchain-community
核心代码:
from langchain_community.retrievers import BM25Retriever
from langchain.retrievers.ensemble import EnsembleRetriever
def build_hybrid_retriever(chunks: list, vectorstore, k=5):
"""
构建混合检索器
chunks: split_documents() 返回的 Document 列表
vectorstore: 已建好的 ChromaDB
"""
# BM25 检索器(关键词)
bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = k
# 向量检索器(语义)
vector_retriever = vectorstore.as_retriever(
search_kwargs={"k": k}
)
# EnsembleRetriever:两路结果融合
# weights 控制两路的权重,0.5/0.5 表示等权重
# 如果你的文档专有名词多,可以调高 BM25 权重:[0.6, 0.4]
hybrid_retriever = EnsembleRetriever(
retrievers=[bm25_retriever, vector_retriever],
weights=[0.5, 0.5]
)
return hybrid_retriever
# 用法:直接替换原来的 vectorstore.as_retriever()
# hybrid_retriever.invoke(question) 即可
但混合检索也有局限,虽然能提高召回率,但是检索到的片段与问题的相关性程度没有个高低排序,top-k片段未必是最合适的结果,这就需要下面的方法。
二、Reranking重排序
Reranking-根据回答相关度打分,重新排序
向量检索召回的是余弦相似度高的片段,但余弦相似度是一个粗粒度的指标,结果里会有不少"相关但没那么相关"的噪音。Reranker 是一个专门做交叉注意力(Cross-Attention) 打分的模型,它把问题和每个文档片段一起输入,给出精确的相关度分数。计算量比 Embedding 大,所以只用来对召回结果精排,不用来全库扫描。典型用法:检索召回 20 条,Reranker 打分后取 Top-3。
硅基流动提供 BGE-Reranker 的 API,直接调用,和 Embedding 一样的方式。
核心代码:
import requests, os
SILICONFLOW_KEY = os.environ.get("SILICONFLOW_API_KEY")
RERANK_URL = "https://api.siliconflow.cn/v1/rerank"
def rerank_docs(query: str, docs: list, top_n=3):
"""
用 BGE-Reranker 对候选文档重排序
docs: Document 对象列表(从检索器拿到的)
top_n: 最终返回几条
返回:按相关度从高到低排序的 Document 列表
"""
if not docs:
return docs
texts = [doc.page_content for doc in docs]
response = requests.post(
RERANK_URL,
headers={
"Authorization": f"Bearer {SILICONFLOW_KEY}",
"Content-Type": "application/json"
},
json={
"model": "BAAI/bge-reranker-v2-m3",
"query": query,
"documents": texts,
"top_n": top_n,
"return_documents": True
}
)
result = response.json()
# 按 reranker 返回的顺序重组 Document 对象
reranked = []
for item in result.get("results", []):
idx = item["index"]
doc = docs[idx]
reranked.append(doc)
return reranked
但有时不是检索策略不够好,输入的问题太模糊也会导致检索不到内容,Query也可以优化。
三、Query改写
用户问: "他打赤壁那次用了什么计策?"
问题里没有"曹操""赤壁之战""火攻"这些关键词,向量检索依赖语义相似度,但这种口语化表达和文档里的书面表达差距很大,容易检索到不相关片段。
Query改写后:
扩展成多个精确的检索问题 改写成 3 个查询: ① "赤壁之战中曹操失败的原因" ② "周瑜火攻的战术策略" ③ "208年赤壁之战经过"
分别检索,结果合并去重,大幅提高命中率。
问题本身是合法的,只是表达不完整,开销是多几次 LLM 调用
核心代码:
REWRITE_PROMPT = """你是一个检索优化助手。请将用户的问题改写成 {n} 个不同角度的检索查询,
每个查询单独一行,用于在历史文献数据库中检索相关内容。
只输出查询本身,不要编号,不要解释。
用户问题:{question}"""
def rewrite_query(question: str, n=3) -> list[str]:
"""把一个用户问题改写成 n 个不同角度的检索词"""
prompt = ChatPromptTemplate.from_template(REWRITE_PROMPT)
chain = prompt | llm
result = chain.invoke({"question": question, "n": n})
queries = [q.strip() for q in result.content.strip().split("\n") if q.strip()]
# 始终把原始问题也加进去
queries.insert(0, question)
return queries[:n + 1]
def multi_query_retrieve(question: str, vectorstore, k=5):
"""改写 + 多路检索 + 去重合并"""
queries = rewrite_query(question)
retriever = vectorstore.as_retriever(search_kwargs={"k": k})
seen_ids = set()
all_docs = []
for q in queries:
for doc in retriever.invoke(q):
doc_id = doc.page_content[:50] # 用前50字符作去重key,(有局限性,有两段前50字相同但内容不同的情况,暂且用这个key举例)
if doc_id not in seen_ids:
seen_ids.add(doc_id)
all_docs.append(doc)
return all_docs
# 测试
if __name__ == "__main__":
q = "他打赤壁那次用了什么计策?"
rewrites = rewrite_query(q)
for r in rewrites:
print(r)
四、总结
| 方法 | 解决的问题 | 额外开销 | 推荐场景 |
|---|---|---|---|
| 混合检索 | 专有名词漏召回 | 低 | 默认开启 |
| Reranking | 召回噪音多 | 中(一次 API) | 召回 top-k 较大时 |
| Query 改写 | 口语化/模糊提问 | 高(多次 LLM) | 面向 C 端用户 |
当然,上面三种方法不是孤立的,完全可以结合起来:先用 Query 改写扩展查询 → 再用混合检索召回候选 → 最后用 Reranker 精排。
经过上面的优化,再问诸葛亮的时候,RAG系统不再找错人了,都能检索到相应的文档片段,给出回答。
五、预告--优化效果评估
不过具题的优化效果没有量化评估。我找到一个专业的评估 RAG 系统的框架,可以从多个角度给系统打分。
下一篇文章将继续探讨 RAG 系统评估框架--RAGAS, 回见。