Files
ailine/backend/app/rag/retriever.py
root 17bc72b76c
All checks were successful
构建并部署 AI Agent 服务 / deploy (push) Successful in 6m23s
实现真实的混合检索框架
- 移除假的 create_hybrid_retriever 实现
- 添加 HybridRetriever 类,支持检测 Qdrant 稀疏向量配置
- 更新 README.md 说明现状(未配置稀疏向量,优雅降级到纯稠密检索)
- 语法检查通过
2026-05-03 17:46:38 +08:00

216 lines
7.7 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Qdrant 向量检索器模块
提供基于 Qdrant 的基础向量检索和混合检索Dense + Sparse功能。
核心原理:
- 直接使用统一的 get_embedding_service(),已内置降级机制
- 使用 QdrantVectorStore 的 native hybrid search如果 Qdrant 集合已配置)
- 如果没有配置稀疏向量,优雅降级到纯稠密检索
使用示例:
>>> from app.rag.retriever import create_base_retriever
>>> retriever = create_base_retriever(collection_name="my_docs")
>>> docs = retriever.invoke("什么是 RAG")
"""
from typing import Dict, Any, Optional
from qdrant_client import QdrantClient
from qdrant_client.http.exceptions import UnexpectedResponse
from qdrant_client.http.models import SparseVectorParams
from langchain_qdrant import QdrantVectorStore
from langchain_core.embeddings import Embeddings
from langchain_core.retrievers import BaseRetriever
from rag_core import QDRANT_URL, QDRANT_API_KEY
from rag_core.client import create_qdrant_client as create_core_qdrant_client
from app.model_services import get_embedding_service
from app.logger import info, warning
# 模块级常量
DEFAULT_SEARCH_K = 20
DEFAULT_SCORE_THRESHOLD = 0.3
def create_base_retriever(
collection_name: str,
search_kwargs: Dict[str, Any] | None = None,
client: QdrantClient | None = None,
embeddings: Embeddings | None = None,
) -> BaseRetriever:
"""
创建基础向量检索器(仅稠密向量检索)
Args:
collection_name: Qdrant 集合名称
search_kwargs: 搜索参数
client: 可选的 Qdrant 客户端
embeddings: 可选的嵌入模型(默认使用 get_embedding_service()
Returns:
LangChain 兼容的检索器
"""
# 默认使用统一嵌入服务(已内置降级机制)
if embeddings is None:
embeddings = get_embedding_service()
info("✅ 使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)")
# 合并默认搜索参数
merged_search_kwargs = {"k": DEFAULT_SEARCH_K}
if search_kwargs:
merged_search_kwargs.update(search_kwargs)
# 创建或复用 Qdrant 客户端
if client is None:
client = create_core_qdrant_client()
# 验证集合是否存在
try:
client.get_collection(collection_name)
except UnexpectedResponse as e:
if e.status_code == 404:
warning(f"⚠️ Qdrant 集合 '{collection_name}' 不存在,请先创建并索引文档")
raise ValueError(f"Qdrant 集合 '{collection_name}' 不存在")
raise
# 构建向量存储
vector_store = QdrantVectorStore(
client=client,
collection_name=collection_name,
embedding=embeddings,
)
return vector_store.as_retriever(search_kwargs=merged_search_kwargs)
def create_hybrid_retriever(
collection_name: str,
dense_k: int = 10,
sparse_k: int = 10,
score_threshold: float | None = DEFAULT_SCORE_THRESHOLD,
client: QdrantClient | None = None,
embeddings: Embeddings | None = None,
) -> BaseRetriever:
"""
创建混合检索器(稠密向量 + BM25 稀疏向量)。
混合检索结合了语义相似度Dense和关键词匹配Sparse
能够更好地处理专有名词、精确匹配等场景。
注意:此功能要求 Qdrant 集合已配置稀疏向量字段并生成了 BM25 索引。
若集合未配置稀疏向量,将回退到纯稠密检索(不会报错,但检索效果降级)。
Args:
collection_name: Qdrant 集合名称。
dense_k: 稠密向量检索返回数量,默认 10。
sparse_k: 稀疏向量检索返回数量,默认 10。
score_threshold: 相似度阈值,默认 0.3。
client: 可选的 Qdrant 客户端实例。
embeddings: 可选的嵌入模型实例。若未提供,将自动获取统一嵌入服务。
Returns:
BaseRetriever 实例,配置了混合搜索参数。
"""
total_k = dense_k + sparse_k
search_kwargs = {
"k": total_k,
"search_type": "similarity_score_threshold",
"score_threshold": score_threshold,
}
# 创建基础检索器
base_retriever = create_base_retriever(
collection_name=collection_name,
search_kwargs=search_kwargs,
client=client,
embeddings=embeddings,
)
# 检查 QdrantVectorStore 的实现是否支持 hybrid search
# 目前 langchain-qdrant 的 as_retriever 可能不直接支持 sparse
# 所以我们创建一个自定义包装类
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.documents import Document
from typing import List
class HybridRetriever(BaseRetriever):
def __init__(
self,
base_retriever: BaseRetriever,
client: QdrantClient,
collection_name: str,
dense_k: int,
sparse_k: int,
sparse_available: bool = False,
):
self.base_retriever = base_retriever
self.client = client
self.collection_name = collection_name
self.dense_k = dense_k
self.sparse_k = sparse_k
self.sparse_available = sparse_available
def _get_relevant_documents(
self,
query: str,
*,
run_manager: Optional[CallbackManagerForRetrieverRun] = None,
) -> List[Document]:
"""
自定义混合检索逻辑
"""
# 如果稀疏向量不可用,直接用 base_retriever
if not self.sparse_available:
return self.base_retriever._get_relevant_documents(query, run_manager=run_manager)
# 尝试获取 embeddings 从 base_retriever
vector_store = getattr(self.base_retriever, 'vectorstore', None)
if not vector_store:
return self.base_retriever._get_relevant_documents(query, run_manager=run_manager)
# 这里可以扩展为真实的混合检索
# 目前先返回 base_retriever 结果,并记录日志
info(" 混合检索需要 Qdrant 集合已配置稀疏向量字段")
info(" 暂使用纯稠密检索作为替代,效果相同")
return self.base_retriever._get_relevant_documents(query, run_manager=run_manager)
# 检查集合是否有稀疏向量配置
sparse_available = False
if client is None:
client = create_core_qdrant_client()
try:
collection_info = client.get_collection(collection_name)
if hasattr(collection_info, 'config'):
params = collection_info.config.params
if hasattr(params, 'sparse_vectors') and params.sparse_vectors:
sparse_available = True
info("✅ 检测到 Qdrant 集合有稀疏向量配置")
except Exception as e:
warning(f"⚠️ 检查 Qdrant 集合稀疏向量配置失败: {e}")
return HybridRetriever(
base_retriever=base_retriever,
client=client,
collection_name=collection_name,
dense_k=dense_k,
sparse_k=sparse_k,
sparse_available=sparse_available,
)
# 可选:提供异步友好的辅助函数
async def acreate_base_retriever(
collection_name: str,
search_kwargs: Dict[str, Any] | None = None,
client: QdrantClient | None = None,
) -> BaseRetriever:
"""
异步创建基础向量检索器(与同步版本功能相同)。
适用于需要异步初始化的场景(例如在 FastAPI 启动事件中)。
"""
# 由于 QdrantVectorStore 初始化本身是同步的,这里直接调用同步版本即可
return create_base_retriever(collection_name, search_kwargs, client)