基于 LlamaIndex 实现 Hybrid‑RAG 召回指南

前文《基于 LlamaIndex+PostgreSQL 实现 RAG 持久化》已完成向量数据库持久化部署,并封装了 RAGService 基础检索服务,解决了 RAG 系统的向量存储与索引持久化核心能力。但单纯依赖基础向量检索,普遍存在语义漂移、关键词精准匹配能力弱、场景适配性差等工业落地问题,难以适配企业复杂知识库的检索诉求。基于此,本节将重点讲解工业界主流的 Hybrid‑RAG 混合检索方案,并提供完整落地实现方案。

在企业RAG落地场景中,检索召回拥有十余种组合方式,其中5种架构适配90%以上企业业务场景,是工业界通用的落地标准。

组合名称 核心组件 典型场景 落地定位
Hybrid‑RAG 向量+BM25+RRF+Rerank 绝大多数企业知识库(制度、产品文档、技术手册) 通用基线(首选,最广泛)
Naive+Rerank 向量检索+Rerank 小型知识库、FAQ,文档量少,预算有限 轻量替代基线
Hybrid+ParentDocument 混合检索+父子文档 长文档、白皮书、报告,信息分散 基线之上场景增强
Hybrid+Sentence‑Window 混合检索+句子窗口 合同、法务、论文,需要局部上下文 基线之上场景增强
RouterQueryEngine 多索引路由 多套隔离知识库(人力 / 产品 / 法务分开) 多知识库场景专用

环境配置

检索方案依赖BM25检索、重排模型、向量存储、重试机制等工具,统一安装依赖,使用官方镜像源保证安装稳定:

CMD> pip install llama-index-retrievers-bm25 sentence-transformers tenacity psycopg2-binary -i https://pypi.org/simple
CMD> pip list
llama-index-retrievers-bm25              0.8.0
sentence-transformers                    6.0.0
tenacity                                 9.1.4
psycopg2-binary                          2.9.13

所有含Rerank的架构均依赖高精度重排模型,推荐工业界通用的BAAI/bge-reranker-v2-m3模型,支持中英文、精度高、适配企业场景。

建议本地部署模型,代码中可直接加载本地模型路径。

代码调用

架构流程:BM25关键词检索 + 向量语义检索并行召回 → RRF倒数排序融合统一结果 → Rerank模型精细打分过滤假阳性 → 输出最优TopK结果,兼顾语义泛化能力与关键词精准度,检索准确率远超单一方案。

模型召回案例演示

import os
import requests
from typing import List
from llama_index.core.node_parser import SentenceSplitter
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.retrievers import QueryFusionRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.postprocessor import SentenceTransformerRerank
from llama_index.core.embeddings import BaseEmbedding
from llama_index.llms.openai_like import OpenAILike
from llama_index.core import (
    VectorStoreIndex,
    SimpleDirectoryReader,
    Settings,
    StorageContext,
    load_index_from_storage,
)

class LocalLlamaServerEmbedding(BaseEmbedding):
    api_base: str
    api_key: str = "dummy"
    # embedding 模型最大上下文 token 数(Qwen3-Embedding-0.6B 默认 512)
    max_tokens: int = 400

    def _get_embedding(self, text: str) -> List[float]:
        url = f"{self.api_base}/embeddings"
        payload = {
            "input": text,
            "model": "Qwen3-Embedding-0.6B-Q8_0.gguf",
        }
        headers = {"Authorization": f"Bearer {self.api_key}"}
        resp = requests.post(url, json=payload, headers=headers, timeout=120)
        if resp.status_code != 200:
            raise RuntimeError(
                f"Embedding 接口请求失败 status={resp.status_code} body={resp.text}"
            )
        data = resp.json()
        return data["data"][0]["embedding"]

    def _get_text_embedding(self, text: str) -> List[float]:
        return self._get_embedding(text)

    def _get_query_embedding(self, query: str) -> List[float]:
        return self._get_embedding(query)

    async def _aget_query_embedding(self, query: str) -> List[float]:
        return self._get_embedding(query)

    async def _aget_text_embedding(self, text: str) -> List[float]:
        return self._get_embedding(text)

def setup_base_env():
    os.environ["OPENAI_API_KEY"] = "dummy"
    os.environ["OPENAI_BASE_URL"] = "http://127.0.0.1:11433/v1"

    llm = OpenAILike(
        model="qwen2.5-1.5b-instruct-q4_k_m.gguf",
        api_base=os.environ["OPENAI_BASE_URL"],
        api_key=os.environ["OPENAI_API_KEY"],
        is_chat_model=True,
        context_window=1024,
        temperature=0.1,
        system_prompt="你是企业文档助手,严格依据检索文档回答,文档没有相关信息请明确告知。",
    )

    # 关键:限制每个 chunk 的 token 数,必须小于 embedding 模型的 512 上限
    Settings.node_parser = SentenceSplitter(chunk_size=380, chunk_overlap=50)

    Settings.llm = llm
    Settings.embed_model = LocalLlamaServerEmbedding(
        api_base="http://127.0.0.1:11434/v1"
    )

# ============================================================
# Hybrid RAG:向量 + BM25 + RRF 融合 + Rerank
# ============================================================
class HybridRAG:
    """Hybrid-RAG:向量检索 + BM25 + RRF 倒数排序融合 + Rerank"""

    def __init__(self, data_dir, persist_dir="./storage_hybrid"):
        self.data_dir = data_dir
        self.persist_dir = persist_dir
        setup_base_env()
        self.index = self._load_or_build_index()
        self.query_engine = self._build_engine()

    def _load_or_build_index(self):
        if os.path.exists(self.persist_dir):
            try:
                storage_context = StorageContext.from_defaults(persist_dir=self.persist_dir)
                idx = load_index_from_storage(storage_context)
                print("HybridRAG 索引加载成功")
                return idx
            except Exception as e:
                print(f"索引加载失败: {e},删除旧索引并重建")
                import shutil
                shutil.rmtree(self.persist_dir, ignore_errors=True)
        try:
            documents = SimpleDirectoryReader(
                self.data_dir,
                required_exts=[".pdf", ".docx", ".txt"],
                recursive=False,
            ).load_data()
        except Exception as err:
            print(f"[文档读取解析失败] {err}")
            raise err

        print(f"读取文档片段 {len(documents)}")
        idx = VectorStoreIndex.from_documents(documents, show_progress=True)
        idx.storage_context.persist(persist_dir=self.persist_dir)
        return idx

    def _build_engine(self):
        vector_retriever = self.index.as_retriever(similarity_top_k=10)
        bm25_retriever = BM25Retriever.from_defaults(
            docstore=self.index.docstore, similarity_top_k=10
        )
        fusion_retriever = QueryFusionRetriever(
            retrievers=[vector_retriever, bm25_retriever],
            similarity_top_k=8,
            mode="reciprocal_rerank",
            num_queries=1,
        )
        post_processors = [
            # 确认该路径下已下载 bge-reranker-v2-m3 模型
            SentenceTransformerRerank(
                model="E:/llamacpp/models/BAAI--bge-reranker-v2-m3", top_n=4
            )
        ]
        engine = RetrieverQueryEngine.from_args(
            retriever=fusion_retriever,
            node_postprocessors=post_processors,
            response_mode="compact",
        )
        return engine

    def query(self, question: str):
        resp = self.query_engine.query(question)
        return {
            "answer": str(resp),
            "sources": [n.metadata for n in resp.source_nodes],
        }

if __name__ == "__main__":

    # 指定文档目录
    rag = HybridRAG("./company_docs")

    # 提问
    res = rag.query("公司报销流程是什么?")
    print("=" * 60)
    print(res["answer"])
    print("=" * 60)
    print("引用来源:")
    for s in res["sources"]:
        print(" -", s.get("file_name", s))

代码运行输出提示信息:

读取文档片段 1
Applying transformations: 100%|███████████████████████| 1/1 [00:02<00:00,  2.89s/it]
Generating embeddings: 100%|██████████████████████████
Generating embeddings: 100%|██████████████████████████| 3/3 [00:06<00:00,  2.03s/it]
Loading weights: 100%|███████████████████████████| 393/393 [00:00<00:00, 4975.27it/s]

============================================================
报销流程如下:

1. **初审**:直属领导审核业务真实性,确认是否为公出,1 个工作日内完成审批。
2. **财务审核**:财务部审核票据合规性、金额计算、预算是否充足。票据不合规直接退回员工修改重提。
3. **部门负责人终审**:金额≥2000 元需要部门负责人审批;金额<2000 元可口头报备直属领导。
4. **打款结算**:审批全部通过后,财务在 5 个工作日内,将报销款项转账至员工本人工资卡。
============================================================
引用来源:
 - post1.txt
 - post1.txt
 - post1.txt
posted @ 2026-09-21 18:21  lyshark  阅读(10)  评论(0)    收藏  举报