feat(v3): Web 应用化基线(异步任务/SSE/llama-server 管理/Vue SPA 四页 + 设置页整页滚动修复)
This commit is contained in:
+415
-217
@@ -1,217 +1,415 @@
|
||||
"""主路由器:协调 缓存 -> 分类 -> 专家 -> Judge -> 大模型回退 的完整链路。
|
||||
|
||||
流程(对齐实现方案):
|
||||
1. 检查缓存(L1 精确 / L2 语义)
|
||||
2. 低置信度查询直接走大模型(should_fallback)
|
||||
3. 分类器输出领域 + 难度
|
||||
4. 选择专家模型生成
|
||||
5. Judge 评估质量
|
||||
6. 质量不达标 -> 升级大模型
|
||||
7. 记录指标、写缓存、返回结果
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .cache import RouterCache
|
||||
from .classifier import BaseClassifier, build_classifier
|
||||
from .config import load_config
|
||||
from .experts import Expert, build_expert_pool
|
||||
from .fallback import FallbackProvider, build_fallback
|
||||
from .judge import BaseJudge, build_judge
|
||||
from .models import Classification, ExpertResponse, RouterResult, now_ms
|
||||
from .stats import Stats
|
||||
|
||||
|
||||
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,
|
||||
):
|
||||
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)
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
async def route(self, query: str) -> RouterResult:
|
||||
start = now_ms()
|
||||
route: list = []
|
||||
|
||||
# ---- 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,
|
||||
)
|
||||
self.stats.record(latency, result.domain, result.difficulty, False, True, level, 0.0, result.model_used)
|
||||
return result
|
||||
route.append("cache:miss")
|
||||
|
||||
# ---- Step 2: 分类 ----
|
||||
classification = self.classifier.classify(query)
|
||||
route.append(f"classify:{classification.domain}@{classification.confidence:.2f}/{classification.difficulty}")
|
||||
|
||||
# 低置信度 -> 直接走大模型
|
||||
if self.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)
|
||||
self._record(result, latency)
|
||||
return result
|
||||
|
||||
# ---- Step 3: 选择专家 ----
|
||||
domain = classification.domain
|
||||
expert = self.experts.get(domain)
|
||||
if expert is None:
|
||||
expert = self.experts.get("general")
|
||||
route.append("expert:fallback-to-general")
|
||||
else:
|
||||
route.append(f"expert:{expert.name}")
|
||||
|
||||
# ---- Step 4: 生成 ----
|
||||
try:
|
||||
expert_resp = await expert.generate(query, classification.difficulty)
|
||||
except Exception as e:
|
||||
self.stats.record_error()
|
||||
route.append(f"expert_error:{type(e).__name__}")
|
||||
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,
|
||||
error=str(e))
|
||||
self._record(result, latency)
|
||||
return result
|
||||
|
||||
# ---- Step 5: Judge 评估 ----
|
||||
try:
|
||||
evaluation = await self.judge.evaluate(query, expert_resp.text, 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
|
||||
final_resp = expert_resp
|
||||
if evaluation is not None and evaluation.needs_fallback:
|
||||
route.append("upgrade")
|
||||
final_resp = await self._call_fallback(query)
|
||||
upgraded = True
|
||||
|
||||
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)
|
||||
self._record(result, latency)
|
||||
|
||||
# 未升级的结果写缓存
|
||||
if self.cache_enabled and not upgraded and result.response:
|
||||
self.cache.put(query, result.to_dict())
|
||||
|
||||
return result
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
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) -> 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,
|
||||
)
|
||||
|
||||
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()),
|
||||
"classifier": type(self.classifier).__name__,
|
||||
"judge": type(self.judge).__name__,
|
||||
"fallback": type(self.fallback).__name__,
|
||||
}
|
||||
|
||||
|
||||
def build_router(config_path: Optional[str] = None) -> Router:
|
||||
"""从配置构建完整 Router(默认 mock 全链路,零依赖可跑)。"""
|
||||
config = load_config(config_path)
|
||||
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))
|
||||
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),
|
||||
)
|
||||
stats = Stats()
|
||||
return Router(classifier, experts, judge, fallback, cache, stats, config)␍
|
||||
"""主路由器:两级路由(大领域组 → 组内路由模型 → 专业执行器)专家系统编排。
|
||||
|
||||
两级体系(对齐用户架构决策):
|
||||
第一级:用户通过接口指定大领域组(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)
|
||||
|
||||
Reference in New Issue
Block a user