Files
projectAIpopular/router_system/router.py
T

416 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""主路由器:两级路由(大领域组 → 组内路由模型 → 专业执行器)专家系统编排。
两级体系(对齐用户架构决策):
第一级:用户通过接口指定大领域组(domain_group: tech/professional/lifestyle/general),
或系统自动检测(8 领域分类 → 映射到组)
第二级:组内路由模型(RuleClassifier(domains=组内领域) + 组内知识/模板)识别具体
领域、子领域、拆解子任务 → 组内专业小模型/规则执行器
组内路由模型只认识本组领域:体积与匹配开销约为统一路由模型的 1/4,
且未来 L2 模型层可每组一个更小的路由模型,按需加载不常驻。
链路:缓存 → 组路由(分类/子领域/拆解) → 黑板+前向链 → DAG 执行 → 合并
→ Judge 校验 → (不达标)最后处理者升级 → 缓存/指标
L0 模式(默认):规则分类 + 规则拆解 + 规则执行器 + 规则 Judge —— 零模型参数、零 API。
L2 模式(可选):execution.expert_backend = hf/api 时,子任务改由专家池小模型执行
(≤8B,按需加载),其余流程不变。
"""
from __future__ import annotations
import uuid
from typing import Any, Dict, List, Optional
from .cache import RouterCache
from .classifier import BaseClassifier, RuleClassifier, build_classifier
from .config import load_config
from .executors import NodeExecutor, build_node_executor
from .experts import Expert, build_expert_pool
from .fallback import FallbackProvider, build_fallback
from .inference import InferenceEngine
from .judge import BaseJudge, build_judge
from .knowledge import KnowledgeBase
from .memory import TaskGraph, TaskNode, WorkingMemory
from .models import Classification, ExpertResponse, RouterResult, now_ms
from .planner import Planner
from .stats import Stats
from .trace import TraceStore
class Router:
def __init__(
self,
classifier: BaseClassifier,
experts: Dict[str, Expert],
judge: BaseJudge,
fallback: FallbackProvider,
cache: Optional[RouterCache] = None,
stats: Optional[Stats] = None,
config: Optional[Dict[str, Any]] = None,
kb: Optional[KnowledgeBase] = None,
planner: Optional[Planner] = None,
):
self.classifier = classifier
self.experts = experts
self.judge = judge
self.fallback = fallback
self.cache = cache or RouterCache()
self.stats = stats or Stats()
cfg = config or {}
rcfg = cfg.get("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)
# ---- 专家系统内核 ----
self.kb = kb or KnowledgeBase()
self.planner = planner or Planner(self.kb)
self.inference = InferenceEngine(self.kb)
ecfg = cfg.get("execution", {})
self.expert_backend = ecfg.get("expert_backend", "rule") # rule | hf | api
# 子任务执行后端(T1 抽象:NodeExecutor 工厂,新增后端无需改 Router)
self.node_executor: NodeExecutor = build_node_executor(
self.expert_backend, kb=self.kb, experts=experts)
# 推理链轨迹存储(T3:可解释性产品化)
self.trace_store = TraceStore()
# ---- 两级路由:大领域分组 + 组内路由模型(更小更专) ----
self.domain_groups: Dict[str, List[str]] = cfg.get("domain_groups", {}) or {}
if not self.domain_groups:
# 兜底:未配置时按单组(全部领域)处理,行为退化为一级路由
self.domain_groups = {"all": list(self.experts.keys())}
self._group_of_domain: Dict[str, str] = {}
for g, domains in self.domain_groups.items():
for d in domains:
self._group_of_domain[d] = g
# 组内路由模型:每组一个轻量分类器(只认识组内领域)
self._group_classifiers: Dict[str, RuleClassifier] = {
g: RuleClassifier(domains=domains)
for g, domains in self.domain_groups.items()
}
# ---------------------------------------------------------------
async def route(self, query: str, domain_group: Optional[str] = None) -> RouterResult:
"""两级路由入口。
domain_group 指定时:跳过 8 领域统一分类器,直接用组内路由模型
RuleClassifier(domains=组内领域))识别组内领域 —— 更小更专。
未指定时:统一分类器识别领域 → 自动映射到大领域组(向后兼容)。
"""
start = now_ms()
route: list = []
request_id = uuid.uuid4().hex[:12]
# ---- Step 1: 缓存 ----
if self.cache_enabled:
hit = self.cache.get(query)
if hit is not None:
level, cached = hit
latency = now_ms() - start
result = RouterResult(
query=query,
response=cached.get("response", ""),
domain=cached.get("domain", "general"),
difficulty=cached.get("difficulty", "medium"),
confidence=cached.get("confidence", 0.0),
upgraded=False,
quality_score=cached.get("quality_score", 0.0),
model_used=cached.get("model_used", ""),
route=["cache:" + level],
latency_ms=latency,
cache_hit=True,
cache_level=level,
cost_est=0.0,
subdomain=cached.get("subdomain"),
subdomain2=cached.get("subdomain2"),
domain_group=cached.get("domain_group"),
request_id=request_id,
)
self._store_trace(
request_id=request_id, query=query, group=cached.get("domain_group"),
domain=result.domain, difficulty=result.difficulty,
confidence=result.confidence, subdomain=result.subdomain,
subdomain2=result.subdomain2, route=route, quality=result.quality_score,
upgraded=False, model=result.model_used, latency=latency,
cache_hit=True, cache_level=level,
)
self.stats.record(latency, result.domain, result.difficulty, False, True, level, 0.0, result.model_used)
return result
route.append("cache:miss")
# ---- Step 2: 组路由(两级第一级)→ 组内分类(两级第二级) ----
classifier = self.classifier
group = domain_group
if group is not None:
# 用户指定大领域:校验 + 使用组内路由模型
if group not in self.domain_groups:
raise ValueError(
f"未知大领域组: {group}(可用: {sorted(self.domain_groups)}"
)
classifier = self._group_classifiers[group]
route.append(f"group:{group}@explicit")
classification = classifier.classify(query)
if group is None:
# 自动检测:8 领域分类 → 映射大领域组
group = self._group_of_domain.get(classification.domain, "general")
route.append(f"group:{group}@auto")
route.append(f"classify:{classification.domain}@{classification.confidence:.2f}/{classification.difficulty}")
subdomain, subdomain2 = self._detect_subdomain(query, classification.domain)
if subdomain:
route.append(f"subdomain:{subdomain}")
if subdomain2:
route.append(f"subdomain2:{subdomain2}")
# ---- Step 3: 低置信度 -> 直接走最后处理者 ----
if classifier.should_fallback(classification, self.low_confidence_threshold):
route.append("direct_fallback")
fb = await self._call_fallback(query)
latency = now_ms() - start
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,
subdomain=subdomain, subdomain2=subdomain2,
domain_group=group)
result.request_id = request_id
self._store_trace(
request_id=request_id, query=query, group=group,
domain=result.domain, difficulty=result.difficulty,
confidence=result.confidence, subdomain=subdomain,
subdomain2=subdomain2, route=route, quality=0.0,
upgraded=True, model=fb.model_used, latency=latency,
)
self._record(result, latency)
return result
# ---- Step 4: Planner 任务拆解(DAG ----
graph = self.planner.plan(query, classification)
route.extend(self.planner.explain_plan(graph))
# ---- Step 5: 黑板初始化 + 前向链(规则轨迹) ----
memory = WorkingMemory()
self.inference.initialize(
query, classification.domain, classification.difficulty,
classification.confidence, memory,
)
fired = self.inference.run(query, classification.domain, memory)
if fired:
route.append(f"rules:{','.join(fired[:5])}")
# ---- Step 6: DAG 顺序执行(拓扑序) ----
order = graph.topo_order()
last_model = f"rule:{classification.domain}"
for node in order:
last_model = await self._execute_node(graph, node, classification, memory, route) or last_model
# ---- Step 7: 黑板合并(节点输出 + 推理机规则产出) ----
response = memory.merge([n.id for n in order])
# 追加推理机规则产出的部分解(带 output 的知识规则,如 git/docker/常识条目)
node_ids = {n.id for n in order}
extra_sections = [sid for sid in memory.sections if sid not in node_ids]
extras = [memory.section(s) for s in extra_sections if memory.section(s)]
if extras:
extra_text = "\n\n".join(extras)
response = (response + "\n\n" + extra_text) if response.strip() else extra_text
if not response.strip():
response = "(规则执行器)未能生成有效回答:任务均未产出内容。"
route.append("merge:empty")
# ---- Step 8: Judge 校验 ----
try:
evaluation = await self.judge.evaluate(query, response, classification.domain)
except Exception:
evaluation = None
route.append("judge_error")
quality_score = evaluation.overall_score if evaluation else 0.0
route.append(f"judge:{quality_score:.2f}")
upgraded = False
if evaluation is not None and evaluation.needs_fallback:
route.append("upgrade")
fb = await self._call_fallback(query)
response = fb.text
last_model = fb.model_used
upgraded = True
latency = now_ms() - start
result = self._finalize(query, classification, ExpertResponse(
text=response, model_used=last_model, latency_ms=latency,
tokens=max(8, int(len(response) / 2.2)), cost_est=0.0,
), quality_score=quality_score, upgraded=upgraded, route=route,
latency_ms=latency, model_used=last_model, cost_est=0.0,
subdomain=subdomain, subdomain2=subdomain2, domain_group=group)
result.request_id = request_id
self._store_trace(
request_id=request_id, query=query, group=group,
domain=result.domain, difficulty=result.difficulty,
confidence=result.confidence, subdomain=subdomain,
subdomain2=subdomain2, route=route, quality=quality_score,
upgraded=upgraded, model=last_model, latency=latency,
)
self._record(result, latency)
# 未升级的结果写缓存
if self.cache_enabled and not upgraded and result.response:
self.cache.put(query, result.to_dict())
return result
# ---------------------------------------------------------------
def _store_trace(self, request_id: str, query: str, group: Optional[str],
domain: str, difficulty: str, confidence: float,
subdomain: Optional[str], subdomain2: Optional[str],
route: list, quality: float, upgraded: bool,
model: str, latency: float,
cache_hit: bool = False, cache_level: Optional[str] = None) -> None:
"""记录完整推理链到轨迹存储(T3:可解释性产品化)。"""
self.trace_store.put(request_id, {
"request_id": request_id,
"query": query,
"domain_group": group,
"domain": domain,
"difficulty": difficulty,
"confidence": round(confidence, 4),
"subdomain": subdomain,
"subdomain2": subdomain2,
"route": list(route),
"quality_score": round(quality, 4),
"upgraded": upgraded,
"model_used": model,
"latency_ms": round(latency, 2),
"cache_hit": cache_hit,
"cache_level": cache_level,
})
# ---------------------------------------------------------------
def _detect_subdomain(self, query: str, domain: str) -> tuple:
"""子领域识别:返回 (二级 subdomain, 三级 subdomain2)。
二级取领域内最高优先级带 subdomain 的命中规则;
三级取最高优先级带 subdomain2 的命中规则(可与二级来自不同规则)。
"""
hits = self.kb.match(query, domain=domain)
sub = None
sub2 = None
for h in hits:
if sub is None and h.subdomain:
sub = h.subdomain
if sub2 is None and h.subdomain2:
sub2 = h.subdomain2
if sub is not None and sub2 is not None:
break
return sub, sub2
# ---------------------------------------------------------------
async def _execute_node(self, graph: TaskGraph, node: TaskNode,
classification: Classification, memory: WorkingMemory,
route: list) -> Optional[str]:
"""执行一个子任务节点;返回使用的 model_used(失败返回 None)。"""
# 依赖检查:依赖失败/跳过 → 本节点跳过
for dep_id in node.deps:
dep = graph.get(dep_id)
if dep is not None and dep.status in ("failed", "skipped"):
node.status = "skipped"
route.append(f"{node.id}:{node.kind}:skip")
return None
node.status = "running"
try:
# NodeExecutor 后端执行(rule 零参数 / model 专家池 ≤8B)
resp = await self.node_executor.execute(
node, classification.domain, classification.difficulty, memory)
node.output = resp.text
node.status = "done"
memory.write_section(node.id, resp.text)
route.append(f"{node.id}:{node.kind}")
return resp.model_used
except Exception as e:
node.status = "failed"
node.error = str(e)
self.stats.record_error()
route.append(f"{node.id}:{node.kind}:error:{type(e).__name__}")
return None
# ---------------------------------------------------------------
async def _call_fallback(self, query: str) -> ExpertResponse:
try:
return await self.fallback.generate(query)
except Exception as e:
# 回退也失败:返回错误占位响应
return ExpertResponse(
text=f"[系统错误] 专家与最后处理者均失败:{type(e).__name__}: {e}",
model_used=f"error:{self.fallback.name}",
cost_est=0.0,
)
@staticmethod
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,
subdomain: Optional[str] = None,
subdomain2: Optional[str] = None,
domain_group: Optional[str] = None) -> RouterResult:
return RouterResult(
query=query,
response=resp.text,
domain=classification.domain,
difficulty=classification.difficulty,
confidence=classification.confidence,
upgraded=upgraded,
quality_score=quality_score,
model_used=model_used,
route=route,
latency_ms=latency_ms,
cache_hit=False,
cost_est=cost_est,
error=error,
subdomain=subdomain,
subdomain2=subdomain2,
domain_group=domain_group,
)
def _record(self, result: RouterResult, latency_ms: float):
self.stats.record(
latency_ms,
result.domain,
result.difficulty,
result.upgraded,
result.cache_hit,
result.cache_level,
result.cost_est,
result.model_used,
)
# ---------------------------------------------------------------
def health(self) -> Dict[str, Any]:
return {
"status": "ok",
"domains": list(self.experts.keys()),
"domain_groups": self.domain_groups,
"classifier": type(self.classifier).__name__,
"judge": type(self.judge).__name__,
"fallback": type(self.fallback).__name__,
"planner": type(self.planner).__name__,
"execution_mode": self.expert_backend,
"rules": self.kb.rules_count(),
}
def build_router(config_path: Optional[str] = None) -> Router:
"""从配置构建完整 Router(默认 L0 专家系统模式:零参数可跑)。"""
config = load_config(config_path)
kb = KnowledgeBase()
classifier = build_classifier(config.get("classifier", {}))
experts = build_expert_pool(config.get("experts", {}), config.get("domains", []))
judge = build_judge(config.get("judge", {}),
config.get("router", {}).get("judge_fallback_threshold", 0.70),
kb=kb)
fallback = build_fallback(config.get("fallback", {}))
cache_cfg = config.get("cache", {})
cache = RouterCache(
semantic_enabled=cache_cfg.get("semantic_enabled", True),
similarity_threshold=cache_cfg.get("similarity_threshold", 0.88),
promote_frequency=cache_cfg.get("promote_frequency", 5),
)
ecfg = config.get("execution", {})
planner = Planner(kb, max_depth=ecfg.get("max_plan_depth", 3))
stats = Stats()
return Router(classifier, experts, judge, fallback, cache, stats, config,
kb=kb, planner=planner)