Files
NotesAgentic/backend/app/retrieval/reranker.py
T
yxxandClaude Code c6cde2500b fix(backend): 落实 PR #9 评审意见
- 检索调优参数(rrf_k/rerank/rerank_candidates/score_threshold)透传到引擎实际执行
- Recall 去重,避免同一 Note 多 Block 重复导致 Recall 超 1
- RAG 运行改为后台异步执行:创建即 queued + 202,支持取消与 SSE 实时事件
- 数据集元数据校验,坏文件隔离跳过;citation_required 语义修正
- modes 空/重复校验;配置快照记录模型版本与索引元信息

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-02 23:20:34 +08:00

63 lines
2.0 KiB
Python

"""Reranker 统一接口与轻量实现。
真实默认是 BGE reranker 类 Cross-Encoder,第一阶段先用词面重叠 + 原始分数加权的
确定性精排跑通链路;后续替换实现即可。
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
from app.textutils import tokens
@dataclass
class RankedCandidate:
block_id: str
score: float
text: str = "" # 块正文,供轻量精排计算词面重叠
@runtime_checkable
class RerankerProvider(Protocol):
"""统一 Reranker 接口:输入候选块,输出按相关性重排后的候选块。"""
model_id: str
version: str
async def rerank(self, query: str, candidates: list[RankedCandidate]) -> list[RankedCandidate]: ...
class LexicalReranker:
"""轻量精排:query 与块正文的词面重叠度,与归一化后的原始分数加权求和。"""
model_id = "lexical-v1"
version = "1"
def __init__(self, lexical_weight: float = 0.5) -> None:
self.lexical_weight = lexical_weight
async def rerank(self, query: str, candidates: list[RankedCandidate]) -> list[RankedCandidate]:
if not candidates:
return []
# 把原始分数(RRF 等)归一化到 [0,1],便于与重叠度同量纲加权
scores = [c.score for c in candidates]
lo, hi = min(scores), max(scores)
span = (hi - lo) or 1.0
query_tokens = set(tokens(query))
ranked: list[RankedCandidate] = []
for c in candidates:
norm = (c.score - lo) / span
if query_tokens:
overlap = len(query_tokens & set(tokens(c.text))) / len(query_tokens)
else:
overlap = 0.0
final = self.lexical_weight * overlap + (1 - self.lexical_weight) * norm
ranked.append(RankedCandidate(block_id=c.block_id, score=final, text=c.text))
ranked.sort(key=lambda c: c.score, reverse=True)
return ranked