前情回顾

上篇文章最后遗留了一个问题:

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片段未必是最合适的结果,这就需要下面的方法。

二、Reranking重排序

硅基流动提供 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改写

问题本身是合法的,只是表达不完整,开销是多几次 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, 回见。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com