基于 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
文章出处:https://www.cnblogs.com/LyShark/p/23067873
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

浙公网安备 33010602011771号