fix(benchmark): 记录实际Embedding空间与逐样本回退信息

This commit is contained in:
2026-09-04 07:25:12 +08:00
parent 5dd5a46aae
commit a75d81a7d9
12 changed files with 157 additions and 7 deletions
+4
View File
@@ -23,6 +23,7 @@ from app.retrieval.embedding import EmbeddingProvider, HashEmbeddingProvider
from app.retrieval.hybrid import normalize_scores, rrf_fuse
from app.retrieval.reranker import LexicalReranker, RankedCandidate, RerankerProvider
from app.retrieval import routed_vectors
from app.retrieval.provenance import record_embedding
from app.retrieval.vectorstore import SqliteVecStore, VectorStore
from app.textutils import make_snippet, match_query
@@ -80,6 +81,7 @@ class RetrievalEngine:
fts_scores = {h.block_id: -h.bm25 for h in fts_hits}
if request.mode in (SearchMode.vector, SearchMode.hybrid):
record_embedding(source="unavailable")
vec_hits = None
if (
self._routed_defaults is not None
@@ -90,6 +92,8 @@ class RetrievalEngine:
if vec_hits is None:
query_vec = await self.embedding.embed_query(request.query)
vec_hits = await self.vector_store.search(query_vec, top_k=recall)
record_embedding(source="local", model_id=self.embedding.model_id,
dimensions=self.embedding.dim, version=self.embedding.version)
vec_ranked = [v.id for v in vec_hits]
vec_scores = {v.id: v.score for v in vec_hits}
+21
View File
@@ -0,0 +1,21 @@
"""Task-local observations of the embedding path actually used by a search."""
from contextlib import contextmanager
from contextvars import ContextVar
_observation: ContextVar[dict | None] = ContextVar("embedding_observation", default=None)
@contextmanager
def capture_embedding():
result = {"source": "not_used"}
token = _observation.set(result)
try:
yield result
finally:
_observation.reset(token)
def record_embedding(**fields) -> None:
result = _observation.get()
if result is not None:
result.update(fields)
+10 -1
View File
@@ -20,6 +20,7 @@ from typing import Protocol
from app.database.db import connect, transaction
from app.retrieval.vectorstore import VectorHit
from app.retrieval.provenance import record_embedding
logger = logging.getLogger(__name__)
@@ -80,6 +81,7 @@ async def embed_remote(texts: list[str]) -> RemoteEmbeddings | None:
return None
result = await runtime.embed(texts)
if result.source != "api":
record_embedding(fallback_reason=result.fallback_reason)
return None
if not isinstance(result.model_id, str) or not result.model_id or result.model_id == "hash-v1":
raise ValueError("API embedding needs a distinct space ID")
@@ -94,6 +96,7 @@ async def embed_remote(texts: list[str]) -> RemoteEmbeddings | None:
)
except Exception as exc:
# Avoid logging provider exceptions containing credentials or note text.
record_embedding(fallback_reason="REMOTE_EMBEDDING_UNAVAILABLE")
logger.warning("Remote embedding unavailable (%s); using local index", type(exc).__name__)
return None
@@ -158,6 +161,7 @@ async def search_remote(query: str, *, top_k: int) -> list[VectorHit] | None:
batch = await embed_remote([query])
if batch is None:
return None
record_embedding(attempted_space={"model_id": batch.space_id, "dimensions": batch.dimensions})
try:
conn = connect()
try:
@@ -166,6 +170,7 @@ async def search_remote(query: str, *, top_k: int) -> list[VectorHit] | None:
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'routed_block_vectors'"
).fetchone()
if exists is None:
record_embedding(fallback_reason="REMOTE_INDEX_MISSING")
return None
rows = conn.execute(
"""SELECT b.block_id, r.vector
@@ -184,9 +189,13 @@ async def search_remote(query: str, *, top_k: int) -> list[VectorHit] | None:
score = math.fsum(a * b for a, b in zip(batch.vectors[0], vector))
yield VectorHit(id=row["block_id"], score=max(0.0, min(1.0, score)))
return heapq.nlargest(top_k, hits(), key=lambda hit: hit.score)
result = heapq.nlargest(top_k, hits(), key=lambda hit: hit.score)
record_embedding(source="api", model_id=batch.space_id,
dimensions=batch.dimensions, fallback_reason=None)
return result
finally:
conn.close()
except Exception as exc:
record_embedding(fallback_reason="REMOTE_INDEX_UNAVAILABLE")
logger.debug("Remote vector search unavailable (%s); using local index", type(exc).__name__)
return None