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:
+43
-7
@@ -45,6 +45,8 @@ class Router:
|
||||
self.low_confidence_threshold = rcfg.get("low_confidence_threshold", 0.60)
|
||||
self.judge_fallback_threshold = rcfg.get("judge_fallback_threshold", 0.70)
|
||||
self.cache_enabled = cfg.get("cache", {}).get("enabled", True)
|
||||
# T-R1:可解释路由评分层(纯解释,不影响决策;默认开启)
|
||||
self.explain_enabled = bool(rcfg.get("explain_enabled", True))
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
async def route(self, query: str) -> RouterResult:
|
||||
@@ -80,10 +82,16 @@ class Router:
|
||||
classification = self.classifier.classify(query)
|
||||
route.append(f"classify:{classification.domain}@{classification.confidence:.2f}/{classification.difficulty}")
|
||||
|
||||
# T-R1:候选装配与可解释评分(纯解释层,不改变后续任何决策分支)
|
||||
explanation = self._explain(classification) if self.explain_enabled else None
|
||||
if explanation is not None:
|
||||
route.append(f"explain:{explanation.winner}@{explanation.confidence:.2f}")
|
||||
|
||||
# 低置信度 -> 直接走大模型
|
||||
if self.classifier.should_fallback(classification, self.low_confidence_threshold):
|
||||
route.append("direct_fallback")
|
||||
return await self._fallback_result(query, classification, route, start)
|
||||
return await self._fallback_result(query, classification, route, start,
|
||||
explanation=explanation)
|
||||
|
||||
# ---- Step 3: 选择专家 ----
|
||||
domain = classification.domain
|
||||
@@ -101,7 +109,7 @@ class Router:
|
||||
self.stats.record_error()
|
||||
route.append(f"expert_error:{type(e).__name__}")
|
||||
return await self._fallback_result(query, classification, route, start,
|
||||
error=str(e))
|
||||
error=str(e), explanation=explanation)
|
||||
|
||||
# ---- Step 5: Judge 评估 ----
|
||||
try:
|
||||
@@ -123,19 +131,46 @@ class Router:
|
||||
latency = now_ms() - start
|
||||
result = self._finalize(query, classification, final_resp, quality_score=quality_score,
|
||||
upgraded=upgraded, route=route, latency_ms=latency,
|
||||
model_used=final_resp.model_used, cost_est=final_resp.cost_est)
|
||||
model_used=final_resp.model_used, cost_est=final_resp.cost_est,
|
||||
explanation=explanation)
|
||||
self._record(result, latency)
|
||||
|
||||
# 未升级的结果写缓存
|
||||
# 未升级的结果写缓存(解释不随缓存复用——缓存命中路径 explanation=None)
|
||||
if self.cache_enabled and not upgraded and result.response:
|
||||
self.cache.put(query, result.to_dict())
|
||||
|
||||
return result
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
def _explain(self, classification: Classification):
|
||||
"""装配候选画像并生成可解释评分(T-R1;任何异常不影响主链路)。"""
|
||||
try:
|
||||
from .explain import CandidateProfile, explain_routing
|
||||
from .experts import _cost_for
|
||||
|
||||
raw = classification.raw_scores or {}
|
||||
raw_max = max(raw.values()) if raw else 0.0
|
||||
candidates = []
|
||||
for dom, expert in self.experts.items():
|
||||
cap = (raw.get(dom, 0.0) / raw_max) if raw_max > 0 else 0.0
|
||||
reason = "无规则命中(capability=0)" if cap <= 0.0 else None
|
||||
candidates.append(CandidateProfile(
|
||||
domain=dom, model=getattr(expert, "model", expert.name),
|
||||
capability=cap,
|
||||
cost_per_mtok=_cost_for(getattr(expert, "model", "mock")),
|
||||
nominal_latency_ms=getattr(expert, "nominal_latency_ms", 100.0),
|
||||
reliability=self.stats.domain_reliability(dom),
|
||||
difficulty_score=classification.difficulty_score,
|
||||
hard_fail_reason=reason))
|
||||
return explain_routing(candidates)
|
||||
except Exception: # noqa: BLE001 解释层故障不影响路由(D-G4 同款纪律)
|
||||
return None
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
async def _fallback_result(self, query: str, classification: Classification,
|
||||
route: list, start: float,
|
||||
error: Optional[str] = None) -> RouterResult:
|
||||
error: Optional[str] = None,
|
||||
explanation=None) -> RouterResult:
|
||||
"""兜底路径的公共收尾:调用大模型回退 -> finalize -> 记录指标。
|
||||
|
||||
低置信度直连、专家异常两条路径共用,避免收尾逻辑三处重复。
|
||||
@@ -145,7 +180,7 @@ class Router:
|
||||
result = self._finalize(query, classification, fb, quality_score=0.0,
|
||||
upgraded=True, route=route, latency_ms=latency,
|
||||
model_used=fb.model_used, cost_est=fb.cost_est,
|
||||
error=error)
|
||||
error=error, explanation=explanation)
|
||||
self._record(result, latency)
|
||||
return result
|
||||
|
||||
@@ -164,7 +199,7 @@ class Router:
|
||||
def _finalize(query: str, classification: Classification, resp: ExpertResponse,
|
||||
quality_score: float, upgraded: bool, route: list,
|
||||
latency_ms: float, model_used: str, cost_est: float,
|
||||
error: Optional[str] = None) -> RouterResult:
|
||||
error: Optional[str] = None, explanation=None) -> RouterResult:
|
||||
return RouterResult(
|
||||
query=query,
|
||||
response=resp.text,
|
||||
@@ -179,6 +214,7 @@ class Router:
|
||||
cache_hit=False,
|
||||
cost_est=cost_est,
|
||||
error=error,
|
||||
route_explanation=explanation.to_dict() if explanation is not None else None,
|
||||
)
|
||||
|
||||
def _record(self, result: RouterResult, latency_ms: float):
|
||||
|
||||
Reference in New Issue
Block a user