51dc8dc4f6
- FastAPI + Qdrant + Redis + Ollama 技术栈 - L1→L2→L3→chunk 四层分层检索(dense + sparse RRF 融合) - 文档三级总结与 2.5 级回退 - query 解析路由与分类 - /admin 管理页面
95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""评测指标纯函数单元测试(不依赖 Qdrant / Ollama)"""
|
||
|
||
import pytest
|
||
|
||
from scripts.eval.metrics import (
|
||
aggregate,
|
||
entity_recall,
|
||
precision_at_k,
|
||
pruning_loss,
|
||
recall_at_k,
|
||
routing_f1,
|
||
)
|
||
|
||
|
||
class TestEntityRecall:
|
||
def test_all_entities_kept(self) -> None:
|
||
source = "使用 Qdrant 存储 1536 维向量,模型 bge-m3,见《部署手册》第三条。"
|
||
summary = "基于 Qdrant 与 bge-m3 的 1536 维向量检索,详见《部署手册》第三条。"
|
||
assert entity_recall(source, summary) == pytest.approx(1.0)
|
||
|
||
def test_partial_entities_kept(self) -> None:
|
||
source = "支持 text-embedding-3-small 和 bge-m3 两个模型。"
|
||
summary = "支持 bge-m3 模型。"
|
||
score = entity_recall(source, summary)
|
||
assert 0.0 < score < 1.0
|
||
|
||
def test_no_entities_returns_full_score(self) -> None:
|
||
assert entity_recall("这是一段没有任何实体的中文普通句子。", "很短") == 1.0
|
||
|
||
def test_empty_summary_drops_all(self) -> None:
|
||
source = "版本 v1.2.0 修复了 3 个问题。"
|
||
assert entity_recall(source, "") == pytest.approx(0.0)
|
||
|
||
|
||
class TestRoutingF1:
|
||
def test_perfect_routing(self) -> None:
|
||
golden = [{"d1"}, {"d2"}]
|
||
routed = [{"d1"}, {"d2"}]
|
||
result = routing_f1(golden, routed)
|
||
assert result == {"precision": 1.0, "recall": 1.0, "f1": 1.0}
|
||
|
||
def test_micro_average(self) -> None:
|
||
# query1: tp=1 fp=1;query2: tp=0 fn=1 → micro p=1/2 r=1/2 f1=1/2
|
||
golden = [{"d1"}, {"d2"}]
|
||
routed = [{"d1", "d3"}, set()]
|
||
result = routing_f1(golden, routed)
|
||
assert result["precision"] == pytest.approx(0.5)
|
||
assert result["recall"] == pytest.approx(0.5)
|
||
assert result["f1"] == pytest.approx(0.5)
|
||
|
||
def test_empty_routed(self) -> None:
|
||
result = routing_f1([{"d1"}], [set()])
|
||
assert result["precision"] == 0.0
|
||
assert result["recall"] == 0.0
|
||
|
||
|
||
class TestPruningLoss:
|
||
def test_mixed_cases(self) -> None:
|
||
assert pruning_loss([False, True, False, False]) == pytest.approx(0.25)
|
||
|
||
def test_empty(self) -> None:
|
||
assert pruning_loss([]) == 0.0
|
||
|
||
|
||
class TestPrecisionRecallAtK:
|
||
def test_precision_at_k(self) -> None:
|
||
hits = ["d1", "d2", "d3", "d4", "d5"]
|
||
assert precision_at_k(hits, {"d1", "d3"}, 5) == pytest.approx(0.4)
|
||
|
||
def test_precision_at_k_empty_hits(self) -> None:
|
||
assert precision_at_k([], {"d1"}, 5) == 0.0
|
||
|
||
def test_recall_at_k_dedup(self) -> None:
|
||
hits = ["d1", "d1", "d2"]
|
||
assert recall_at_k(hits, {"d1", "d2", "d3"}, 3) == pytest.approx(2 / 3)
|
||
|
||
def test_recall_at_k_empty_golden(self) -> None:
|
||
assert recall_at_k(["d1"], set(), 5) == 0.0
|
||
|
||
|
||
class TestAggregate:
|
||
def test_mean_per_key(self) -> None:
|
||
records = [{"a": 1.0, "b": 0.5}, {"a": 0.0, "b": 1.0}]
|
||
result = aggregate(records)
|
||
assert result["a"] == pytest.approx(0.5)
|
||
assert result["b"] == pytest.approx(0.75)
|
||
|
||
def test_partial_keys(self) -> None:
|
||
result = aggregate([{"a": 1.0}, {"a": 0.0, "b": 1.0}])
|
||
assert result["a"] == pytest.approx(0.5)
|
||
assert result["b"] == pytest.approx(1.0)
|
||
|
||
def test_empty(self) -> None:
|
||
assert aggregate([]) == {}
|