diff --git a/backend/app/rag/retriever.py b/backend/app/rag/retriever.py index 55fc3b0..8203dcd 100644 --- a/backend/app/rag/retriever.py +++ b/backend/app/rag/retriever.py @@ -1,12 +1,13 @@ """ Qdrant 向量检索器模块 -提供基于 Qdrant 的基础向量检索和混合检索(Dense + BM25)功能。 +提供基于 Qdrant 的基础向量检索和混合检索(Dense + Sparse)功能。 核心原理: -- 同时调用 Qdrant 稠密检索(语义理解)和 BM25Retriever(关键词匹配) -- 结果合并去重,获得更好的检索效果 -- 完全兼容现有代码,无需修改 Qdrant 集合配置 +- 使用 langchain-qdrant 的 RetrievalMode +- Qdrant 原生混合检索(如果集合已配置 sparse_vectors) +- 如果集合未配置,优雅回退到纯稠密检索 +- 完全兼容现有代码,无接口改动 使用示例: >>> from app.rag.retriever import create_hybrid_retriever @@ -14,14 +15,15 @@ Qdrant 向量检索器模块 >>> docs = retriever.invoke("什么是 RAG?") """ -from typing import Dict, Any, Optional, List +from typing import Dict, Any, Optional from qdrant_client import QdrantClient from qdrant_client.http.exceptions import UnexpectedResponse -from langchain_qdrant import QdrantVectorStore +from langchain_qdrant import ( + QdrantVectorStore, + RetrievalMode, +) from langchain_core.embeddings import Embeddings from langchain_core.retrievers import BaseRetriever -from langchain_core.documents import Document -from langchain_community.retrievers import BM25Retriever from rag_core import QDRANT_URL, QDRANT_API_KEY from rag_core.client import create_qdrant_client as create_core_qdrant_client @@ -93,17 +95,17 @@ def create_hybrid_retriever( embeddings: Embeddings | None = None, ) -> BaseRetriever: """ - 创建混合检索器(稠密向量 + BM25 稀疏向量)。 + 创建混合检索器(使用 Qdrant 自身的 RetrievalMode.HYBRID)。 - ⚡️ 真实实现: - - 同时调用 Qdrant 稠密检索(语义理解)和 BM25Retriever(关键词匹配) - - 结果合并去重,获得更好的检索效果 - - 完全兼容现有代码,无需修改 Qdrant 集合配置 + ⚡️ Qdrant 原生混合检索: + - 如果 Qdrant 集合已配置 sparse_vectors:启用 Qdrant 原生混合检索 + - 如果未配置:优雅回退到纯稠密检索 + - 完全兼容现有代码,接口不变 Args: collection_name: Qdrant 集合名称。 dense_k: 稠密向量检索返回数量,默认 10。 - sparse_k: BM25 检索返回数量,默认 10。 + sparse_k: 稀疏向量检索返回数量,默认 10。 score_threshold: 相似度阈值,默认 0.3。 client: 可选的 Qdrant 客户端实例。 embeddings: 可选的嵌入模型实例。若未提供,将自动获取统一嵌入服务。 @@ -111,63 +113,67 @@ def create_hybrid_retriever( Returns: BaseRetriever 实例,配置了混合搜索参数。 """ - # 创建基础稠密检索器 - dense_retriever = create_base_retriever( + total_k = dense_k + sparse_k + + search_kwargs = { + "k": total_k, + "search_type": "similarity_score_threshold", + "score_threshold": score_threshold, + } + + # 默认使用统一嵌入服务(已内置降级机制) + if embeddings is None: + embeddings = get_embedding_service() + info("✅ 使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)") + + # 创建或复用 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 + + # 检查 Qdrant 集合是否有稀疏向量配置 + sparse_available = False + 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 集合有稀疏向量配置,启用 Qdrant 原生混合检索") + except Exception as e: + warning(f"⚠️ 检查 Qdrant 集合稀疏向量配置失败: {e}") + + # 如果有稀疏向量配置,用 Qdrant 原生混合检索 + if sparse_available: + try: + vector_store = QdrantVectorStore( + client=client, + collection_name=collection_name, + embedding=embeddings, + retrieval_mode=RetrievalMode.HYBRID, + ) + info(f"✅ Qdrant 原生混合检索器初始化成功 (k={total_k})") + return vector_store.as_retriever(search_kwargs=search_kwargs) + except Exception as e: + warning(f"⚠️ Qdrant 原生混合检索初始化失败: {e},回退到纯稠密检索") + + # 如果没有稀疏向量配置,回退到纯稠密检索 + info("ℹ️ Qdrant 集合未配置稀疏向量,使用纯稠密检索(完全兼容)") + return create_base_retriever( collection_name=collection_name, - search_kwargs={"k": dense_k, "score_threshold": score_threshold}, + search_kwargs=search_kwargs, client=client, embeddings=embeddings, ) - # 从 Qdrant 加载所有文档到 BM25Retriever - bm25_retriever = None - if client is None: - client = create_core_qdrant_client() - - try: - # 尝试从 Qdrant 加载少量样本文档(用于演示 BM25) - # 实际使用中,建议从外部加载完整文档列表 - from langchain_core.vectorstores import VectorStoreRetriever - vector_store = getattr(dense_retriever, 'vectorstore', None) - - # 这里我们做一个简单的混合:先返回稠密结果,提示说明这是真实混合检索框架 - # 如果需要加载完整文档进行 BM25,请提供 bm25_documents 参数 - - class HybridRetriever(BaseRetriever): - def __init__( - self, - dense_retriever: BaseRetriever, - dense_k: int = 10, - sparse_k: int = 10, - ): - self.dense_retriever = dense_retriever - self.dense_k = dense_k - self.sparse_k = sparse_k - - def _get_relevant_documents( - self, - query: str, - *, - run_manager: Optional[Any] = None, - ) -> List[Document]: - # 获取稠密检索结果 - dense_docs = self.dense_retriever._get_relevant_documents(query, run_manager=run_manager) - - info(f"✅ 混合检索框架已启用,当前使用稠密检索({len(dense_docs)} 个结果)") - info(f"ℹ️ 若要启用完整 BM25 关键词检索,请提供 bm25_documents 参数") - - return dense_docs - - return HybridRetriever( - dense_retriever=dense_retriever, - dense_k=dense_k, - sparse_k=sparse_k, - ) - - except Exception as e: - warning(f"⚠️ 初始化 BM25Retriever 失败: {e},回退到纯稠密检索") - return dense_retriever - # 可选:提供异步友好的辅助函数 async def acreate_base_retriever( diff --git a/backend/requirements.txt b/backend/requirements.txt index 2edc6e8..d36b0ec 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,7 +18,6 @@ zhipuai==2.0.1 # Vector DB qdrant-client==1.17.1 -fastembed>=0.3.0 # 用于 Qdrant BM25 稀疏向量 # Memory mem0ai==1.0.11