refactor!: 完全异步化 RAG 系统,移除 LangChain ParentDocumentRetriever 依赖
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Failing after 6m34s

- 重写 rag_core/vector_store.py:完全异步实现 aadd_documents、asimilarity_search
- 重写 app/rag/retriever.py:异步混合检索,移除同步兼容代码
- 修改 rag_indexer/index_builder.py:全链路异步调用
- 删除 rag_core/retriever_factory.py:不再使用 LangChain ParentDocumentRetriever
- 清理冗余导入和代码:移除 model_services 兼容、不需要的异常导入
- 更新 rag_indexer/README.md:反映新架构

核心改进:
- 完全异步化:索引构建和检索全链路 async/await
- 自定义实现:不再依赖 LangChain 的 ParentDocumentRetriever
- 双向量支持:子文档同时存储 dense + sparse 向量到 Qdrant
- 架构清晰:rag_core 公共组件、rag_indexer 索引、app/rag 检索
This commit is contained in:
2026-05-04 14:33:12 +08:00
parent 4209386c77
commit a07e398739
14 changed files with 651 additions and 592 deletions

View File

@@ -1,5 +1,5 @@
"""
Qdrant 混合检索器模块
Qdrant 混合检索器模块(完全异步)
提供基于 Qdrant 的混合检索Dense + Sparse功能包括
- 纯混合检索(无子父文档)
@@ -12,15 +12,15 @@ Qdrant 混合检索器模块
"""
from typing import Dict, Any, Optional, List
from qdrant_client import QdrantClient, models
from qdrant_client import AsyncQdrantClient, models
from qdrant_client.http.exceptions import UnexpectedResponse
from langchain_core.documents import Document
from langchain_core.embeddings import Embeddings
from langchain_core.retrievers import BaseRetriever, RetrieverOutput
from langchain_core.retrievers import BaseRetriever
from pydantic import Field, PrivateAttr
from rag_core import QdrantVectorStore, get_sparse_embedder, create_docstore
from rag_core.client import create_qdrant_client as create_core_qdrant_client
from rag_core import QdrantHybridStore, get_sparse_embedder, create_docstore
from rag_core.client import create_async_qdrant_client
from app.model_services import get_embedding_service
from app.logger import info, warning, debug
@@ -32,13 +32,13 @@ DEFAULT_PARENT_SEARCH_K = 5
class HybridRetriever(BaseRetriever):
"""
混合检索器:稠密向量 + BM25 稀疏向量 RRF 分数融合
混合检索器:稠密向量 + BM25 稀疏向量 RRF 分数融合(异步)
使用 Qdrant Universal Query API (query_points)
"""
collection_name: str = Field(description="Qdrant 集合名称")
search_k: int = Field(default=DEFAULT_SEARCH_K, description="检索返回结果数")
_vector_store: Any = PrivateAttr()
_client: Any = PrivateAttr()
_sparse_embedder: Any = PrivateAttr()
@@ -46,13 +46,13 @@ class HybridRetriever(BaseRetriever):
def __init__(
self,
collection_name: str,
vector_store: QdrantVectorStore,
vector_store: QdrantHybridStore,
search_k: int = DEFAULT_SEARCH_K,
):
"""
Args:
collection_name: Qdrant 集合名称
vector_store: QdrantVectorStore 实例
vector_store: QdrantHybridStore 实例
search_k: 检索返回结果数
"""
super().__init__(
@@ -60,46 +60,40 @@ class HybridRetriever(BaseRetriever):
search_k=search_k
)
self._vector_store = vector_store
self._client = vector_store.get_qdrant_client()
self._client = vector_store.get_async_qdrant_client()
self._sparse_embedder = get_sparse_embedder()
def _get_relevant_documents(
async def _aget_relevant_documents(
self, query: str, **kwargs
) -> List[Document]:
"""
同步检索相关文档
Args:
query: 查询字符串
Returns:
相关文档列表
异步混合检索相关文档
"""
# 1. 生成向量
dense_query = self._vector_store.embeddings.embed_query(query)
# 1. 生成查询向量
dense_query = await self._vector_store._aembed_query(query)
sparse_query = self._sparse_embedder.embed_query(query)
sparse_vec = models.SparseVector(
indices=sparse_query["indices"],
values=sparse_query["values"]
)
# 2. 使用官方的 query_points API(推荐方式)
response = self._client.query_points(
# 2. 使用 Qdrant 的 query_points API
response = await self._client.query_points(
collection_name=self.collection_name,
prefetch=[ # 并行预取多个检索源
prefetch=[
models.Prefetch(
query=dense_query,
using="dense", # 使用稠密向量进行语义搜索
using="dense",
limit=self.search_k
),
models.Prefetch(
query=sparse_vec,
using="sparse", # 使用稀疏向量进行关键词搜索
using="sparse",
limit=self.search_k
)
],
query=models.FusionQuery(fusion=models.Fusion.RRF), # 指定融合算法为 RRF
limit=self.search_k, # 最终返回的结果数量
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=self.search_k,
with_payload=True
)
@@ -112,20 +106,13 @@ class HybridRetriever(BaseRetriever):
)
results.append(doc)
debug(f"混合检索返回 {len(results)} 个文档")
debug(f"混合检索返回 %d 个文档", len(results))
return results
async def _aget_relevant_documents(
self, query: str, **kwargs
) -> List[Document]:
"""异步检索(当前调用同步版本)"""
# Qdrant 客户端没有原生 async这里用同步版本
return self._get_relevant_documents(query, **kwargs)
class ParentHybridRetriever(BaseRetriever):
"""
父子文档混合检索器:
父子文档混合检索器(异步)
1. 先用混合检索找到相关子文档
2. 根据子文档的 parent_id 找到对应的父文档
@@ -134,7 +121,7 @@ class ParentHybridRetriever(BaseRetriever):
collection_name: str = Field(description="Qdrant 集合名称")
search_k: int = Field(default=DEFAULT_PARENT_SEARCH_K, description="检索返回结果数")
_vector_store: Any = PrivateAttr()
_client: Any = PrivateAttr()
_sparse_embedder: Any = PrivateAttr()
@@ -143,14 +130,14 @@ class ParentHybridRetriever(BaseRetriever):
def __init__(
self,
collection_name: str,
vector_store: QdrantVectorStore,
vector_store: QdrantHybridStore,
search_k: int = DEFAULT_PARENT_SEARCH_K,
docstore: Optional[Any] = None,
):
"""
Args:
collection_name: Qdrant 集合名称
vector_store: QdrantVectorStore 实例
vector_store: QdrantHybridStore 实例
search_k: 最终返回的父文档数量
docstore: 文档存储(如果父文档在 PostgreSQL可选
"""
@@ -159,24 +146,18 @@ class ParentHybridRetriever(BaseRetriever):
search_k=search_k
)
self._vector_store = vector_store
self._client = vector_store.get_qdrant_client()
self._client = vector_store.get_async_qdrant_client()
self._sparse_embedder = get_sparse_embedder()
self._docstore = docstore
def _get_relevant_documents(
async def _aget_relevant_documents(
self, query: str, **kwargs
) -> List[Document]:
"""
步检索相关父文档
Args:
query: 查询字符串
Returns:
相关父文档列表
步检索相关父文档
"""
# 1. 生成查询向量
dense_query = self._vector_store.embeddings.embed_query(query)
# 1. 生成查询向量
dense_query = await self._vector_store._aembed_query(query)
sparse_query = self._sparse_embedder.embed_query(query)
sparse_vec = models.SparseVector(
indices=sparse_query["indices"],
@@ -187,7 +168,7 @@ class ParentHybridRetriever(BaseRetriever):
search_limit = self.search_k * 2
# 3. 使用 query_points API 进行混合检索
response = self._client.query_points(
response = await self._client.query_points(
collection_name=self.collection_name,
prefetch=[
models.Prefetch(
@@ -216,30 +197,27 @@ class ParentHybridRetriever(BaseRetriever):
child_point_map = {} # 保存子文档点用于降级
for point in response.points:
# 先复制 payload避免修改原始对象
payload_copy = point.payload.copy()
parent_id = payload_copy.get("parent_id", point.id)
score = point.score
# 同一个 parent_id 只保留最高得分
if parent_id not in parent_score_map or score > parent_score_map[parent_id]:
parent_score_map[parent_id] = score
parent_ids.add(parent_id)
child_point_map[parent_id] = point
# 5. 批量查询父文档
# 首先尝试从 Qdrant 直接查询(因为父文档可能也在 Qdrant 中)
parent_docs = []
found_parent_ids = set()
# 先尝试从 Qdrant 直接查询(如果父文档也在 Qdrant 中)
try:
parent_points = self._client.retrieve(
parent_points = await self._client.retrieve(
collection_name=self.collection_name,
ids=list(parent_ids),
with_payload=True
)
# 处理找到的父文档
for point in parent_points:
payload_copy = point.payload.copy()
doc = Document(
@@ -250,24 +228,24 @@ class ParentHybridRetriever(BaseRetriever):
found_parent_ids.add(point.id)
except Exception as e:
warning(f"从 Qdrant 查询父文档失败: {e}")
warning(f"从 Qdrant 查询父文档失败: %s", e)
# 6. 如果有 docstore尝试从 docstore 查询剩余的父文档
if self._docstore and len(found_parent_ids) < len(parent_ids):
missing_parent_ids = parent_ids - found_parent_ids
try:
docstore_docs = self._docstore.mget(missing_parent_ids)
docstore_docs = await self._docstore.amget(missing_parent_ids)
for doc_id, doc in zip(missing_parent_ids, docstore_docs):
if doc is not None:
parent_docs.append(doc)
found_parent_ids.add(doc_id)
except Exception as e:
warning(f"从 docstore 查询父文档失败: {e}")
warning(f"从 docstore 查询父文档失败: %s", e)
# 7. 降级:对于仍未找到的父文档,用子文档本身代替
missing_parent_ids = parent_ids - found_parent_ids
if missing_parent_ids:
warning(f"以下 parent_id 未找到对应的父文档,将返回子文档本身: {missing_parent_ids}")
warning(f"以下 parent_id 未找到对应的父文档,将返回子文档本身: %s", missing_parent_ids)
for parent_id in missing_parent_ids:
child_point = child_point_map.get(parent_id)
if child_point:
@@ -280,22 +258,16 @@ class ParentHybridRetriever(BaseRetriever):
# 8. 按照得分降序排序,返回前 k 个
parent_docs_with_scores = [
(doc, parent_score_map.get(doc.metadata.get("id", doc.id), 0.0))
(doc, parent_score_map.get(doc.metadata.get("id", doc.id if hasattr(doc, "id") else ""), 0.0))
for doc in parent_docs
]
parent_docs_with_scores.sort(key=lambda x: x[1], reverse=True)
final_docs = [doc for doc, _ in parent_docs_with_scores[:self.search_k]]
debug(f"父子文档混合检索返回 {len(final_docs)} 个父文档")
debug(f"父子文档混合检索返回 %d 个父文档", len(final_docs))
return final_docs
async def _aget_relevant_documents(
self, query: str, **kwargs
) -> List[Document]:
"""异步检索(当前调用同步版本)"""
return self._get_relevant_documents(query, **kwargs)
def create_hybrid_retriever(
collection_name: str,
@@ -303,7 +275,7 @@ def create_hybrid_retriever(
embeddings: Optional[Embeddings] = None,
) -> BaseRetriever:
"""
创建混合检索器(稠密向量 + BM25 稀疏向量)。
创建混合检索器(稠密向量 + BM25 稀疏向量)- 异步版本
这是默认推荐的检索方式,效果最优。
@@ -315,15 +287,12 @@ def create_hybrid_retriever(
Returns:
HybridRetriever 实例
"""
# 默认使用统一嵌入服务
if embeddings is None:
embeddings = get_embedding_service()
info("使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)")
info("使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)")
# 创建向量存储
vector_store = QdrantVectorStore(collection_name=collection_name, embeddings=embeddings)
vector_store = QdrantHybridStore(collection_name=collection_name, embeddings=embeddings)
# 验证集合是否存在
try:
vector_store.get_client().get_collection(collection_name)
except UnexpectedResponse as e:
@@ -347,7 +316,7 @@ def create_parent_hybrid_retriever(
use_docstore: bool = True,
) -> BaseRetriever:
"""
创建父子文档混合检索器(默认推荐)。
创建父子文档混合检索器(默认推荐)- 异步版本
检索流程:
1. 混合检索找到相关子文档
@@ -363,15 +332,12 @@ def create_parent_hybrid_retriever(
Returns:
ParentHybridRetriever 实例
"""
# 默认使用统一嵌入服务
if embeddings is None:
embeddings = get_embedding_service()
info("使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)")
info("使用统一嵌入服务(本地 llama.cpp → 智谱 API 自动降级)")
# 创建向量存储
vector_store = QdrantVectorStore(collection_name=collection_name, embeddings=embeddings)
vector_store = QdrantHybridStore(collection_name=collection_name, embeddings=embeddings)
# 验证集合是否存在
try:
vector_store.get_client().get_collection(collection_name)
except UnexpectedResponse as e:
@@ -380,14 +346,13 @@ def create_parent_hybrid_retriever(
raise ValueError(f"Qdrant 集合 '{collection_name}' 不存在")
raise
# 创建 docstore如果需要
docstore = None
if use_docstore:
try:
docstore, _ = create_docstore()
info("✅ 文档存储初始化成功PostgreSQL")
except Exception as e:
warning(f"⚠️ 文档存储初始化失败,将不使用 docstore: {e}")
warning(f"⚠️ 文档存储初始化失败,将不使用 docstore: %s", e)
info(f"✅ Qdrant 父子文档混合检索器初始化成功search_k={search_k}")
return ParentHybridRetriever(
@@ -404,24 +369,9 @@ def create_base_retriever(
embeddings: Optional[Embeddings] = None,
) -> BaseRetriever:
"""
创建基础稠密检索器(向后兼容)。
Args:
collection_name: Qdrant 集合名称
search_k: 检索返回结果数
embeddings: 可选的嵌入模型实例
Returns:
LangChain 的 BaseRetriever 实例
创建基础检索器(向后兼容)- 实际上返回混合检索器
"""
# 默认使用统一嵌入服务
if embeddings is None:
embeddings = get_embedding_service()
vector_store = QdrantVectorStore(collection_name=collection_name, embeddings=embeddings)
info(f"✅ Qdrant 基础稠密检索器初始化成功search_k={search_k}")
return vector_store.as_langchain_vectorstore().as_retriever(k=search_k)
return create_hybrid_retriever(collection_name, search_k, embeddings)
# 别名:默认就是父子文档混合检索