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
+6 -1
View File
@@ -24,6 +24,7 @@ from app.contracts import (
SearchRequest,
)
from app.retrieval.engine import engine
from app.retrieval.provenance import capture_embedding
logger = logging.getLogger(__name__)
@@ -90,8 +91,10 @@ async def _evaluate_one(
score_threshold=request.retrieval.score_threshold,
)
start = time.perf_counter()
embedding = {}
try:
response = await engine.search(search_request)
with capture_embedding() as embedding:
response = await engine.search(search_request)
latency_ms = (time.perf_counter() - start) * 1000.0
except Exception as exc: # 单个样本失败不中断整个 Benchmark
# 详细异常只进日志,公开响应只带项目错误码与安全消息,避免泄露路径/SQL 等敏感信息
@@ -100,6 +103,7 @@ async def _evaluate_one(
exc_info=exc,
)
return RAGCaseResult(
embedding=embedding,
case_id=case.case_id,
mode=mode,
repeat=repeat,
@@ -115,6 +119,7 @@ async def _evaluate_one(
k = request.retrieval.top_k
return RAGCaseResult(
embedding=embedding,
case_id=case.case_id,
mode=mode,
repeat=repeat,
+2 -1
View File
@@ -86,7 +86,8 @@ def _config_snapshot(request: RAGRunRequest, dataset: RAGDataset) -> dict:
"modes": [m.value for m in request.modes],
"retrieval": request.retrieval.model_dump(),
"repeat": request.repeat,
"embedding": {
"embedding": {"policy": "per_case", "details": "cases[].embedding"},
"local_embedding": {
"model_id": engine.embedding.model_id,
"version": engine.embedding.version,
"dim": engine.embedding.dim,
+1
View File
@@ -1131,6 +1131,7 @@ class BenchmarkEvent(Contract):
class RAGCaseResult(Contract):
embedding: dict[str, Any] = Field(default_factory=dict)
case_id: str
mode: SearchMode
repeat: int
+5 -1
View File
@@ -24,6 +24,7 @@ from app.providers.base import ProviderError
from app.providers.credentials import CredentialResolver, CredentialStoreError
from app.providers.registry import ProviderNotFoundError, ProviderRegistry
from app.retrieval.embedding import EmbeddingProvider, HashEmbeddingProvider
from app.retrieval.provenance import record_embedding
CAPABILITIES = ("embedding", "transcription", "speaker_matching")
HTTP_TYPES = {ProviderType.openai_chat, ProviderType.openai_compatible}
@@ -175,7 +176,10 @@ class ModelRoutingService:
return data, url
async def embed(self, texts: list[str]) -> EmbeddingResult:
binding = self.configuration().embedding
config = self.configuration()
binding = config.embedding
record_embedding(route_version=config.version,
requested_route=binding.model_dump() if binding else None)
reason = None
if binding and texts:
try:
+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