"""可解释路由评分层单元测试(T-R1,采纳 ai-model-router 设计)。""" import pytest from router_system.explain import CandidateProfile, explain_routing from router_system.router import build_router def _cand(domain, capability, cost=1.0, latency=100.0, reliability=0.9, fail=None): return CandidateProfile(domain=domain, model=f"m-{domain}", capability=capability, cost_per_mtok=cost, nominal_latency_ms=latency, reliability=reliability, difficulty_score=0.5, hard_fail_reason=fail) def test_hard_filter_rejects_with_reason(): """硬过滤淘汰带人话理由,被拒者不进排名。""" exp = explain_routing([ _cand("code", 1.0), _cand("math", 0.0, fail="无规则命中(capability=0)"), ]) assert exp.winner == "code" assert any(r["domain"] == "math" and "无规则命中" in r["reason"] for r in exp.rejected) assert all(s["domain"] != "math" for s in exp.ranked) def test_all_rejected_falls_back(): """全部候选被硬过滤:决策者是大模型回退,置信度取基准 0.8。""" exp = explain_routing([_cand("code", 0.0, fail="x"), _cand("math", 0.0, fail="y")]) assert exp.winner == "fallback" assert exp.confidence == 0.80 assert len(exp.rejected) == 2 def test_zero_cost_local_beats_costly(): """成本效率维平滑映射:零成本恒 1.0,贵模型显著吃亏。""" cheap = explain_routing([_cand("code", 1.0, cost=0.0, latency=1.0)]).ranked[0] pricey = explain_routing([_cand("math", 1.0, cost=2.5, latency=800.0)]).ranked[0] assert cheap["dims"]["cost_efficiency"] == 1.0 assert pricey["dims"]["cost_efficiency"] < 0.5 assert cheap["total"] > pricey["total"] def test_confidence_gap_derivation(): """置信度 = 0.8 + 分差:独苗封顶 0.99,双候选同分回落 0.8。""" single = explain_routing([_cand("code", 1.0)]) tied = explain_routing([_cand("code", 1.0), _cand("math", 1.0)]) assert single.confidence == 0.99 assert tied.confidence == 0.80 def test_tie_break_is_deterministic(): """同分决胜按领域名字典序(与仓库既有确定性约定一致)。""" exp = explain_routing([_cand("zeta", 1.0), _cand("alpha", 1.0)]) assert exp.winner == "alpha" def test_weights_snapshot_and_rank_order(): """排名按 total 降序,权重快照随解释输出。""" exp = explain_routing([ _cand("code", 1.0, cost=0.0, latency=1.0), _cand("math", 0.5, cost=2.0, latency=900.0), ]) totals = [s["total"] for s in exp.ranked] assert totals == sorted(totals, reverse=True) assert exp.weights["capability"] == 0.35 @pytest.mark.asyncio async def test_route_attaches_explanation(): """主链路返回结构化 route_explanation;缓存命中路径为 None。""" router = build_router() r1 = await router.route("用 Python 写一个快速排序函数") assert r1.route_explanation is not None assert r1.route_explanation["winner"] == "code" assert any("explain:" in step for step in r1.route) assert any("无规则命中" in r["reason"] for r in r1.route_explanation["rejected"]) r2 = await router.route("用 Python 写一个快速排序函数") assert r2.cache_hit is True assert r2.route_explanation is None @pytest.mark.asyncio async def test_low_confidence_explains_fallback(): """低置信度直连回退:解释层给出 fallback 胜者与拒绝名单。""" router = build_router() r = await router.route("今天天气怎么样") assert r.upgraded is True assert r.route_explanation is not None assert r.route_explanation["winner"] == "fallback"