fischer-agentkit/src/agentkit/rag_platform/rerank.py

211 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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.

"""Rerank 模型集成 — 支持 Cohere Rerank 和 BGE-Reranker本地部署
数据出境风险Cohere Rerank 将文档 chunks 发送到第三方 API。
敏感数据 KB 应使用 BGE-Reranker via Xinference本地部署
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from pydantic import BaseModel, ConfigDict
from agentkit.rag_platform.models import QueryResult
if TYPE_CHECKING:
from llama_index.core.schema import NodeWithScore
logger = logging.getLogger(__name__)
class RerankConfig(BaseModel):
"""Rerank 配置 — 可按 KB 覆盖。
provider:
- "cohere": Cohere Rerank API数据出境
- "bge": BGE-Reranker via Xinference本地部署敏感数据 KB 推荐)
- "none": 不重排
"""
model_config = ConfigDict()
provider: str = "none"
api_key: str | None = None
base_url: str | None = None # Xinference URL for BGE
model_name: str | None = None # 模型名(如 "bge-reranker-base"
top_n: int = 5
# True 表示当前 KB 使用了云端 rerank存在数据出境风险
data_export_warning: bool = False
class Reranker:
"""Rerank 引擎 — 包装 LlamaIndex rerankers。
使用方式:
config = RerankConfig(provider="cohere", api_key="...", top_n=5)
reranker = Reranker(config)
reranked = await reranker.rerank(query, results)
"""
def __init__(self, config: RerankConfig) -> None:
self._config = config
self._reranker: object | None = None # 延迟初始化,避免 import 失败
def _get_reranker(self) -> object | None:
"""延迟加载 reranker 实例 — 避免在 import 时失败。"""
if self._reranker is not None:
return self._reranker
cfg = self._config
if cfg.provider == "cohere":
self._reranker = self._build_cohere_reranker(cfg)
elif cfg.provider == "bge":
self._reranker = self._build_bge_reranker(cfg)
elif cfg.provider == "none":
self._reranker = None
else: # pragma: no cover — 配置校验已穷尽
raise ValueError(f"Unsupported rerank provider: {cfg.provider}")
return self._reranker
def _build_cohere_reranker(self, cfg: RerankConfig) -> object:
"""构建 CohereRerank — 数据出境,需 api_key。"""
if not cfg.api_key:
raise ValueError("Cohere rerank requires api_key")
try:
from llama_index.postprocessor.cohere_rerank import CohereRerank
except ImportError as e:
raise ImportError(
"CohereRerank requires llama-index-postprocessor-cohere-rerank. "
"Install: pip install llama-index-postprocessor-cohere-rerank"
) from e
kwargs: dict[str, object] = {
"api_key": cfg.api_key,
"top_n": cfg.top_n,
}
if cfg.model_name:
kwargs["model"] = cfg.model_name
return CohereRerank(**kwargs)
def _build_bge_reranker(self, cfg: RerankConfig) -> object:
"""构建 BGE-Reranker via Xinference本地部署无数据出境
使用 SentenceTransformerRerank 作为本地 BGE-Reranker 的封装。
若 base_url 指向 Xinference调用方应在 KB 设置中标注为本地部署。
"""
try:
from llama_index.postprocessor.sentence_transformers_rerank import (
SentenceTransformerRerank,
)
except ImportError as e:
raise ImportError(
"SentenceTransformerRerank requires "
"llama-index-postprocessor-sentence-transformers-rerank. "
"Install: pip install llama-index-postprocessor-sentence-transformers-rerank"
) from e
model = cfg.model_name or "BAAI/bge-reranker-base"
kwargs: dict[str, object] = {
"model": model,
"top_n": cfg.top_n,
}
return SentenceTransformerRerank(**kwargs)
async def rerank(
self,
query: str,
results: list[QueryResult],
) -> list[QueryResult]:
"""对检索结果重排,返回按相关性排序的 top_n 结果。
Args:
query: 查询文本
results: 原始检索结果列表
Returns:
重排后的 QueryResult 列表(按相关性降序),分数已更新为 rerank 分数。
若 provider == "none" 或 results 为空,原样返回。
"""
# 空结果或关闭重排 — 直接返回
if not results or self._config.provider == "none":
return list(results)
reranker = self._get_reranker()
if reranker is None:
return list(results)
# 将 QueryResult 转为 LlamaIndex NodeWithScore
nodes_with_scores = self._to_nodes_with_scores(results)
try:
# LlamaIndex reranker 的 postprocessnodes 是同步方法
reranked_nodes = reranker.postprocessnodes(
nodes_with_scores,
query_str=query,
)
except Exception as e:
logger.warning("Rerank failed, returning original results: %s", e)
return list(results)
# 将重排结果映射回 QueryResult更新分数
return self._from_nodes_with_scores(reranked_nodes, results)
@staticmethod
def _to_nodes_with_scores(
results: list[QueryResult],
) -> "list[NodeWithScore]":
"""将 QueryResult 列表转为 LlamaIndex NodeWithScore 列表。"""
from llama_index.core.schema import NodeWithScore, TextNode
out: list[NodeWithScore] = []
for r in results:
node = TextNode(
id_=r.chunk_id,
text=r.content,
metadata=r.metadata,
)
out.append(NodeWithScore(node=node, score=r.score))
return out
@staticmethod
def _from_nodes_with_scores(
nodes: "list[NodeWithScore]",
original: list[QueryResult],
) -> list[QueryResult]:
"""将重排后的 NodeWithScore 列表转回 QueryResult更新分数。
通过 node_id 匹配原始 QueryResult 的元数据document_id, kb_id
"""
original_map = {r.chunk_id: r for r in original}
out: list[QueryResult] = []
for nws in nodes:
node_id = nws.node.node_id if hasattr(nws.node, "node_id") else None
original_r = original_map.get(node_id) if node_id else None
if original_r is None:
# 兜底:通过内容匹配(理论上不应触发)
content = nws.node.get_content() if hasattr(nws.node, "get_content") else ""
original_r = next(
(r for r in original if r.content == content),
None,
)
if original_r is None:
continue
new_score = float(nws.score) if nws.score is not None else original_r.score
out.append(
QueryResult(
chunk_id=original_r.chunk_id,
content=original_r.content,
score=new_score,
metadata=original_r.metadata,
document_id=original_r.document_id,
kb_id=original_r.kb_id,
)
)
return out
__all__ = ["RerankConfig", "Reranker"]