神龙快打
91.05M · 2026-04-10
上篇文章最后遗留了一个问题:
RAG私有知识库共有里有三篇文档——曹操介绍、诸葛亮介绍、赤壁之战介绍。问曹操能答,问诸葛亮却回答"文档中未提及"。
打印检索日志后发现:问"诸葛亮是哪里人"时,检索到的 3 个片段全部来自 caocao.txt。
第一反应是调参——把 chunk_size 调小(从400到300再到200),希望每个片段语义更纯粹;把 top_k 调大(3-5),希望覆盖更多候选。但反复试验后效果依然不理想,诸葛亮的问题还是答不对。
问题出在更上游:这三篇文档本身就高度交织。诸葛亮的介绍里多次提到曹操,曹操的介绍里也提到了赤壁和诸葛亮。无论 chunk 切得多细,只要文档内容互相穿插,向量检索计算的整体语义相似度就会"认错人"——它找到的是语义最近的片段,而不是名字最匹配的片段。
这是向量检索的结构性缺陷,调参只是在错误的方向上用力。真正的解法需要从检索策略上改进。本文将记录几个改进检索效果的方法。
安装依赖
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片段未必是最合适的结果,这就需要下面的方法。
硅基流动提供 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也可以优化。
问题本身是合法的,只是表达不完整,开销是多几次 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, 回见。