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:
@@ -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"
|
||||
Reference in New Issue
Block a user