去年双十一,我负责的电商 AI 客服系统在促销高峰期遭遇了严重的检索瓶颈。用户搜索"2024新款红色连衣裙"时,纯向量检索返回的结果虽然语义相关,但常常遗漏了时效性促销信息;而基于关键词的 BM25 检索又无法理解用户的真实意图。这个矛盾让我开始深入研究 Hybrid Search(混合检索)方案。

经过三个月的线上调优,我们最终基于 LlamaIndex 实现了 dense + sparse 双路检索,搜索相关性提升了 47%,平均响应延迟稳定在 120ms 以内。今天我将完整分享这套方案的工程实现细节。

一、为什么需要 Hybrid Search?

在企业级 RAG(检索增强生成)系统中,单一检索方式往往难以满足复杂场景需求:

HolySheep AI 的国内直连延迟 <50ms,配合我们的混合检索策略,可以在保证语义理解能力的同时,兼顾关键词精确匹配,这对于电商搜索场景至关重要。

二、环境准备与依赖安装

# Python 3.9+ 环境
pip install llama-index llama-index-retrievers-bm25 pymilvus sentence-transformers

向量数据库选择(这里用 FAISS 作为示例)

pip install faiss-cpu

HolySheep API SDK

pip install openai # HolySheep 兼容 OpenAI SDK

三、完整实现代码

3.1 初始化 HolySheep API 与向量模型

import os
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader
from llama_index.core.retrievers import BaseRetriever
from llama_index.retrievers.bm25 import BM25Retriever
from llama_index.core.vector_stores import SimpleVectorStore
from llama_index.core.schema import NodeWithScore
from typing import List, Optional
import faiss
from sentence_transformers import SentenceTransformer

HolySheep AI 配置(汇率 ¥1=$1,相比官方节省 >85%)

os.environ["OPENAI_API_KEY"] = "YOUR_HOLYSHEEP_API_KEY" os.environ["OPENAI_API_BASE"] = "https://api.holysheep.ai/v1"

初始化 embedding 模型(使用 BAAI/bge-small-zh-v1.5 中文优化模型)

embed_model = SentenceTransformer('BAAI/bge-small-zh-v1.5') class HolySheheEmbedding: """HolySheep AI 兼容的 Embedding 包装器""" def __init__(self, model_name: str = "bge-small-zh-v1.5"): self.model_name = model_name def encode(self, texts: List[str], **kwargs) -> List[List[float]]: return embed_model.encode(texts, normalize_embeddings=True).tolist() def get_text_embedding(self, text: str) -> List[float]: return self.encode([text])[0] def get_text_embeddings(self, texts: List[str]) -> List[List[float]]: return self.encode(texts) embedding = HolySheheEmbedding()

3.2 构建 Hybrid Retriever 核心类

class HybridRetriever(BaseRetriever):
    """
    Dense + Sparse 混合检索器
    支持可配置的权重融合与重排序
    """
    def __init__(
        self,
        nodes: List,
        dense_retriever: VectorStoreIndex,
        sparse_retriever: BM25Retriever,
        alpha: float = 0.5,  # alpha=0.5 表示 dense/sparse 各占 50%
        top_k: int = 10,
        rerank: bool = True
    ):
        self.nodes = nodes
        self.dense_index = dense_retriever
        self.sparse_retriever = sparse_retriever
        self.alpha = alpha  # 控制 dense/sparse 权重
        self.top_k = top_k * 2  # 多召回一些用于后续重排序
        self.rerank = rerank
        
        # 构建向量索引
        self._build_vector_index()
        
    def _build_vector_index(self):
        """构建 FAISS 向量索引"""
        dim = len(self.nodes[0].embedding) if self.nodes else 384
        self.faiss_index = faiss.IndexFlatIP(dim)
        
        # 批量添加向量
        if self.nodes:
            embeddings = [node.embedding for node in self.nodes]
            self.faiss_index.add(np.array(embeddings).astype('float32'))
    
    def _dense_search(self, query: str, top_k: int) -> List[NodeWithScore]:
        """稠密检索"""
        query_embedding = embedding.get_text_embedding(query)
        scores, indices = self.faiss_index.search(
            np.array([query_embedding]).astype('float32'), 
            top_k
        )
        
        results = []
        for score, idx in zip(scores[0], indices[0]):
            if idx < len(self.nodes):
                node = self.nodes[idx]
                results.append(NodeWithScore(node=node, score=float(score)))
        return results
    
    def _sparse_search(self, query: str, top_k: int) -> List[NodeWithScore]:
        """稀疏检索(BM25)"""
        results = self.sparse_retriever.retrieve(query, top_k=top_k)
        # 归一化 BM25 分数到 [0, 1]
        max_score = max(r.score for r in results) if results else 1.0
        for r in results:
            r.score = r.score / max_score if max_score > 0 else 0
        return results
    
    def _fusion_scores(
        self, 
        dense_results: List[NodeWithScore], 
        sparse_results: List[NodeWithScore]
    ) -> dict:
        """RRF (Reciprocal Rank Fusion) 分数融合"""
        k = 60  # RRF 超参数
        fused_scores = {}
        
        # Dense 结果打分
        for rank, result in enumerate(dense_results):
            doc_id = result.node.node_id
            score = (1 - self.alpha) * (1 / (k + rank + 1))
            fused_scores[doc_id] = fused_scores.get(doc_id, 0) + score
        
        # Sparse 结果打分
        for rank, result in enumerate(sparse_results):
            doc_id = result.node.node_id
            score = self.alpha * (1 / (k + rank + 1))
            fused_scores[doc_id] = fused_scores.get(doc_id, 0) + score
        
        return fused_scores
    
    def retrieve(self, query: str) -> List[NodeWithScore]:
        """执行混合检索"""
        # 1. 并行执行 dense 和 sparse 检索
        dense_results = self._dense_search(query, self.top_k)
        sparse_results = self._sparse_search(query, self.top_k)
        
        # 2. 分数融合
        fused_scores = self._fusion_scores(dense_results, sparse_results)
        
        # 3. 按融合分数排序
        all_results = {r.node.node_id: r for r in dense_results + sparse_results}
        ranked = sorted(fused_scores.items(), key=lambda x: x[1], reverse=True)
        
        # 4. 返回 Top K 结果
        final_results = []
        for doc_id, score in ranked[:self.top_k]:
            if doc_id in all_results:
                node = all_results[doc_id].node
                final_results.append(NodeWithScore(node=node, score=score))
        
        return final_results[:self.top_k]

使用示例

import numpy as np from llama_index.core.schema import TextNode

准备测试数据

nodes = [ TextNode(text="双十一促销:iPhone 15 Pro Max 直降 500 元", metadata={"type": "promotion"}), TextNode(text="Python 列表去重的 5 种方法详解", metadata={"type": "tutorial"}), TextNode(text="2024 新款红色连衣裙限时秒杀", metadata={"type": "product"}), TextNode(text="如何使用 LlamaIndex 构建 RAG 系统", metadata={"type": "tutorial"}), ]

为每个节点生成 embedding

for node in nodes: node.embedding = embedding.get_text_embedding(node.text)

初始化 BM25 检索器

from llama_index.retrievers.bm25 import BM25Retriever bm25_retriever = BM25Retriever.from_defaults(nodes=nodes, similarity_top_k=10)

构建 FAISS 索引

dim = len(nodes[0].embedding) faiss_index = faiss.IndexFlatIP(dim) faiss_index.add(np.array([n.embedding for n in nodes]).astype('float32'))

创建混合检索器

hybrid_retriever = HybridRetriever( nodes=nodes, dense_retriever=None, # 这里直接用 FAISS,跳过 VectorStoreIndex sparse_retriever=bm25_retriever, alpha=0.5, top_k=3 )

测试检索

results = hybrid_retriever.retrieve("Python 列表去重方法") for r in results: print(f"[{r.score:.4f}] {r.node.text[:50]}...")

四、与 HolySheep LLM 集成构建完整 RAG 管道

from llama_index.core import QueryPipeline
from llama_index.llms.openai_like import OpenAILike

初始化 HolySheep LLM(DeepSeek V3.2 价格仅 $0.42/MTok)

llm = OpenAILike( model="deepseek-chat", api_key="YOUR_HOLYSHEEP_API_KEY", api_base="https://api.holysheep.ai/v1", is_chat_model=True, )

构建 Query Pipeline

query_pipeline = QueryPipeline verbose=True) query_pipeline.add_modules({ 'retriever': hybrid_retriever, 'llm': llm })

定义提示模板

from llama_index.core.prompts import PromptTemplate template = """基于以下上下文信息回答用户问题。如果上下文中没有相关信息,请如实说明。 上下文信息: {context} 用户问题: {query} 请提供准确、详细的回答:""" prompt = PromptTemplate(template)

添加提示模板模块

query_pipeline.add_modules({'prompt': prompt})

连接管道

query_pipeline.link('retriever', 'prompt') query_pipeline.link('prompt', 'llm')

执行查询

response = query_pipeline.run(query="Python 列表去重有哪些方法?") print(str(response))

五、性能优化实战经验

在我负责的电商项目中,经过大量线上调优,总结出以下关键参数:

使用 HolySheep AI 的 立即注册 后,国内直连延迟稳定在 50ms 以内,配合我们上述的缓存优化策略,整体 P99 延迟可以控制在 150ms 以内。

六、常见报错排查

错误 1:Embedding 维度不匹配

# 错误信息
ValueError: dimension of query vector (768) does not match index dimension (384)

解决方案:确保 embedding 模型维度与索引维度一致

embedding_model = SentenceTransformer('BAAI/bge-small-zh-v1.5') # 输出 384 维

如果需要 768 维,使用:

embedding_model = SentenceTransformer('BAAI/bge-large-zh-v1.5') # 输出 1024 维

初始化时明确指定维度

dim = embedding_model.get_sentence_embedding_dimension() print(f"Embedding 维度: {dim}")

错误 2:BM25Retriever 初始化失败

# 错误信息
ImportError: cannot import name 'Stemmer' from 'nltk.stem'

解决方案:安装必要的 NLTK 数据

import nltk nltk.download('punkt') nltk.download('stopwords') nltk.download('punkt_tab')

如果仍有问题,使用自定义分词器

class ChineseBM25Retriever(BM25Retriever): def __init__(self, nodes, similarity_top_k=10): import jieba def tokenize(text): return list(jieba.cut(text)) super().__init__( nodes=nodes, similarity_top_k=similarity_top_k, tokenize_query_fn=tokenize, tokenize_nodes_fn=tokenize )

使用自定义分词器

bm25_retriever = ChineseBM25Retriever(nodes=nodes)

错误 3:HolySheep API Key 认证失败

# 错误信息
AuthenticationError: Invalid API key provided

解决方案:检查 API Key 配置

import os

方式 1:环境变量(推荐)

os.environ["OPENAI_API_KEY"] = "YOUR_HOLYSHEEP_API_KEY" os.environ["OPENAI_API_BASE"] = "https://api.holysheep.ai/v1"

方式 2:直接传入参数

llm = OpenAILike( model="deepseek-chat", api_key="YOUR_HOLYSHEEP_API_KEY", # 确认此处替换为真实 Key api_base="https://api.holysheep.ai/v1", is_chat_model=True, )

验证连接

from openai import OpenAI client = OpenAI( api_key="YOUR_HOLYSHEEP_API_KEY", base_url="https://api.holysheep.ai/v1" ) models = client.models.list() print("API 连接成功,可用的模型列表:", [m.id for m in models.data[:5]])

错误 4:RRF 融合分数异常偏低

# 错误信息:融合分数全部接近 0,结果随机

原因:alpha 参数与检索结果分布不匹配

解决方案:添加分数归一化与调试日志

def _normalize_scores(self, results: List[NodeWithScore]) -> List[NodeWithScore]: if not results: return results max_score = max(r.score for r in results) min_score = min(r.score for r in results) score_range = max_score - min_score if max_score != min_score else 1 for r in results: r.score = (r.score - min_score) / score_range return results

调试:打印中间结果

def retrieve_with_debug(self, query: str) -> List[NodeWithScore]: dense_results = self._dense_search(query, self.top_k) sparse_results = self._sparse_search(query, self.top_k) print(f"[DEBUG] Dense 结果数: {len(dense_results)}") print(f"[DEBUG] Sparse 结果数: {len(sparse_results)}") print(f"[DEBUG] Dense 分数范围: {[f'{r.score:.4f}' for r in dense_results[:3]]}") print(f"[DEBUG] Sparse 分数范围: {[f'{r.score:.4f}' for r in sparse_results[:3]]}") # 归一化后再融合 dense_results = self._normalize_scores(dense_results) sparse_results = self._normalize_scores(sparse_results) return self._retrieve_with_normalized(query, dense_results, sparse_results)

七、总结与成本估算

Hybrid Search 在企业级 RAG 场景中展现出显著优势:

以我们的电商场景为例,月均搜索请求 500 万次:

Hybrid Search 是企业级 RAG 系统的标配方案。如果你正在构建类似的 AI 应用,强烈建议从一开始就规划好 dense + sparse 双路检索架构。

👉 免费注册 HolySheep AI,获取首月赠额度