feat(v1): T-R1 采纳 ai-model-router 可解释路由评分——五维加权+硬过滤带拒绝理由+置信度分差推导

- 新增 router_system/explain.py:CandidateProfile(capability/cost/latency/
  reliability/difficulty 画像)+ explain_routing(硬过滤先行、逐条人话拒绝理由、
  五维归一评分、同分按领域字典序、confidence=0.8+分差推导封顶 0.99)
- Router._explain 纯解释层装配(解释层任何异常不影响主链路,D-G4 同款纪律);
  路由胜负与既有决策完全等价,/chat 响应新增 route_explanation 字段(缓存命中为 None)
- experts:Expert.nominal_latency_ms 标称延迟元数据(mock 1/hf 300/api 800)
- stats:upgraded_by_domain 计数 + domain_reliability()(无数据给 0.9 中性先验)
- .gitignore 登记 extra/(参考项目目录不入库)
- 新增 tests/test_explain.py 8 项;全量 33 passed(基线 25)
This commit is contained in:
tzt
2026-09-19 09:23:04 +08:00
parent 5b28c2a583
commit a9f60159e7
8 changed files with 488 additions and 232 deletions
+90
View File
@@ -0,0 +1,90 @@
"""可解释路由评分层单元测试(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"