feat: 接入 BGE-M3 本地嵌入默认配置 + 新增 Qwen3-Reranker 重排
- .env.example 嵌入默认改为本地 BGE-M3(EMBEDDING_PROVIDER=local, dim=1024),NAS 全新部署即用 - 新增 app/services/reranker.py:基于 Ollama /api/rerank 的 cross-encoder 重排服务 - retriever 在 chunk 候选阶段接入语义精排,调用失败优雅降级为 RRF 顺序 - docker-compose 拉取 qwen3-reranker:0.6b,OLLAMA_MAX_LOADED_MODELS 提至 3 - .gitignore 排除 .workbuddy/ 与 .trae/(防止误提交项目记忆与 IDE spec) - 新增 reranker 单元与集成测试,全量测试 529 passed
This commit is contained in:
+26
-3
@@ -16,7 +16,7 @@ from qdrant_client import models
|
||||
from app.config import settings
|
||||
from app.core.embeddings import EmbeddingService, create_embedding_service
|
||||
from app.core.query_parser import QueryParser, RouteDecision
|
||||
from app.core.ranker import finalize, rrf_fuse
|
||||
from app.core.ranker import rrf_fuse
|
||||
from app.core.result_summarizer import ResultSummarizer
|
||||
from app.core.sparse import SparseEncoder
|
||||
from app.models.knowledge import load_taxonomy
|
||||
@@ -30,6 +30,7 @@ from app.services.qdrant import (
|
||||
SPARSE_COLLECTIONS,
|
||||
QdrantService,
|
||||
)
|
||||
from app.services.reranker import RerankerService, create_reranker_service
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
@@ -49,6 +50,7 @@ class Retriever:
|
||||
embedding: EmbeddingService | None = None,
|
||||
sparse_encoder: SparseEncoder | None = None,
|
||||
result_summarizer: ResultSummarizer | None = None,
|
||||
reranker: RerankerService | None = None,
|
||||
) -> None:
|
||||
self.qdrant = qdrant or QdrantService()
|
||||
self.query_parser = query_parser or QueryParser(
|
||||
@@ -58,6 +60,7 @@ class Retriever:
|
||||
self.embedding = embedding or create_embedding_service()
|
||||
self.sparse_encoder = sparse_encoder or SparseEncoder()
|
||||
self.result_summarizer = result_summarizer or ResultSummarizer()
|
||||
self.reranker = reranker if reranker is not None else create_reranker_service()
|
||||
|
||||
async def search(self, request: SearchRequest) -> SearchResponse:
|
||||
"""分层检索主流程"""
|
||||
@@ -77,7 +80,7 @@ class Retriever:
|
||||
# L1 无候选文档 → 全库 chunk 兜底
|
||||
chunk_hits = await self._search_collection(COLLECTION_CHUNKS, dense, sparse, settings.retrieval_top_k, None)
|
||||
logger.info("L1 无命中,全库 chunk 兜底", hits=len(chunk_hits))
|
||||
hits = self._to_hits(finalize(chunk_hits, self._final_k(request)))
|
||||
hits = self._to_hits(await self._rerank(query_text, chunk_hits, self._final_k(request)))
|
||||
return await self._build_response(request, route, hits, fallback=True)
|
||||
|
||||
doc_ids = _unique((p.payload or {}).get("doc_id") for p in l1_hits)
|
||||
@@ -123,7 +126,7 @@ class Retriever:
|
||||
)
|
||||
logger.info("chunk 检索完成", hits=len(chunk_hits))
|
||||
|
||||
final_points = finalize(rrf_fuse([chunk_hits]), self._final_k(request))
|
||||
final_points = await self._rerank(query_text, chunk_hits, self._final_k(request))
|
||||
hits = self._to_hits(final_points)
|
||||
return await self._build_response(request, route, hits, fallback=route.fallback)
|
||||
|
||||
@@ -145,6 +148,26 @@ class Retriever:
|
||||
"""最终返回数:请求指定优先,否则用配置默认值"""
|
||||
return request.top_k or settings.retrieval_final_k
|
||||
|
||||
async def _rerank(
|
||||
self, query_text: str, points: list[models.ScoredPoint], final_k: int
|
||||
) -> list[models.ScoredPoint]:
|
||||
"""对 chunk 候选池做语义精排(cross-encoder)
|
||||
|
||||
重排未启用、候选为空或调用异常时,退化为 RRF 融合的原始顺序(取前 final_k),
|
||||
保证检索链路在任何情况下都不因重排失败而中断。
|
||||
"""
|
||||
if self.reranker is None or not points:
|
||||
return points[:final_k]
|
||||
documents = [(p.payload or {}).get("text", "") for p in points]
|
||||
try:
|
||||
scores = await self.reranker.rerank(query_text, documents)
|
||||
except Exception:
|
||||
logger.warning("重排调用失败,退化为 RRF 融合顺序", exc_info=True)
|
||||
return points[:final_k]
|
||||
# 按相关性分数降序重排,写回 score 字段为相关性分数
|
||||
ordered = sorted(range(len(points)), key=lambda i: scores[i], reverse=True)
|
||||
return [points[i].model_copy(update={"score": scores[i]}) for i in ordered[:final_k]]
|
||||
|
||||
@staticmethod
|
||||
def _to_hits(points: list[models.ScoredPoint]) -> list[SearchHit]:
|
||||
"""chunk 点组装为 SearchHit,字段取自 chunk payload(doc_summary 仅上下文标注)"""
|
||||
|
||||
Reference in New Issue
Block a user