"""RouteAgent:Agent-Skill 路由器(T12:先行实现)。 核心思想(对齐用户架构决策): - 用户只提供需求,不需要指定领域/模型/技能 - Agent 自行分析需求(两级路由:自动组检测 → 领域/难度/三级子领域) - 规划 skill 调用计划(复用 Planner 任务模板 → 每个子任务映射到技能) - 按拓扑序执行技能调用,黑板协作,合并输出 - 质量校验(judge skill)→ 不达标升级(fallback skill) 与 Router 的关系:Router.route() 是"编排管线",RouteAgent.route() 是 "技能调用式"同构实现——执行阶段通过 SkillRegistry 按技能名调用, 推理链轨迹记录每次 skill 调用(可解释性)。 """ from __future__ import annotations import uuid from typing import Any, Dict, Optional from .classifier import RuleClassifier from .executors import NodeExecutor from .fallback import FallbackProvider from .inference import InferenceEngine from .judge import BaseJudge from .knowledge import KnowledgeBase from .memory import TaskGraph, WorkingMemory from .models import Classification, ExpertResponse, RouterResult, now_ms from .planner import Planner from .skills import SkillContext, SkillRegistry, build_skill_registry from .trace import TraceStore # 子任务 kind → 技能名映射(retrieve 走知识库检索,其余走模板技能) _KIND_SKILL = { "analyze": "es.analyze", "design": "es.design", "implement": "es.implement", "solve": "es.solve", "diagnose": "es.diagnose", "fix": "es.fix", "retrieve": "kb.retrieve", "conclude": "es.conclude", "advise": "es.advise", "explain": "es.explain", "disclaimer": "es.disclaimer", "verify": "es.verify", "refactor": "es.refactor", "testcase": "es.testcase", "complexity": "es.complexity", "optimize": "es.optimize", "draft": "es.draft", "polish": "es.polish", } class RouteAgent: """Agent-Skill 路由器:需求分析 → 技能规划 → 技能执行 → 校验升级。""" def __init__( self, classifier: RuleClassifier, planner: Planner, kb: KnowledgeBase, judge: BaseJudge, fallback: FallbackProvider, node_executor: Optional[NodeExecutor] = None, registry: Optional[SkillRegistry] = None, low_confidence_threshold: float = 0.60, judge_fallback_threshold: float = 0.70, ): self.classifier = classifier self.planner = planner self.kb = kb self.judge = judge self.fallback = fallback self.node_executor = node_executor self.inference = InferenceEngine(kb) self.low_confidence_threshold = low_confidence_threshold self.judge_fallback_threshold = judge_fallback_threshold self.registry = registry or build_skill_registry( kb=kb, judge=judge, fallback=fallback, fallback_threshold=judge_fallback_threshold, ) self.trace_store = TraceStore() # --------------------------------------------------------------- async def route(self, query: str) -> RouterResult: start = now_ms() route: list = [] request_id = uuid.uuid4().hex[:12] # ---- Step 1: 需求分析(Agent 自行分析,无需用户指定) ---- classification = self.classifier.classify(query) 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 2: 低置信 → fallback 技能(Agent 自主兜底) ---- if classification.confidence < self.low_confidence_threshold: route.append("direct_fallback") resp = await self.registry.execute("fallback.call", SkillContext( query=query, domain=classification.domain, difficulty=classification.difficulty, memory=WorkingMemory(), kb=self.kb)) latency = now_ms() - start result = RouterResult( query=query, response=resp, domain=classification.domain, difficulty=classification.difficulty, confidence=classification.confidence, upgraded=True, quality_score=0.0, model_used=self.fallback.name, route=route, latency_ms=latency, cost_est=0.0, subdomain=subdomain, subdomain2=subdomain2, request_id=request_id, ) self._store_trace(result, route, request_id, query, latency) return result # ---- Step 3: 技能规划(Planner 任务模板 → skill 调用计划) ---- graph: TaskGraph = self.planner.plan(query, classification) route.extend(self.planner.explain_plan(graph)) # ---- Step 4: 黑板初始化 + 前向链 ---- 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 5: 按拓扑序执行技能调用 ---- order = graph.topo_order() last_model = f"rule:{classification.domain}" for node in order: model = await self._execute_skill(node, classification, memory, route) if model: last_model = model # ---- Step 6: 合并 + 质量校验(judge 技能) ---- response = memory.merge([n.id for n in order]) node_ids = {n.id for n in order} extras = [memory.section(s) for s in memory.sections if s not in node_ids and memory.section(s)] if extras: response = (response + "\n\n" + "\n\n".join(extras)) if response.strip() else "\n\n".join(extras) if not response.strip(): response = "(RouteAgent)未能生成有效回答。" route.append("merge:empty") 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") response = await self.registry.execute("fallback.call", SkillContext( query=query, domain=classification.domain, difficulty=classification.difficulty, memory=memory, kb=self.kb)) last_model = self.fallback.name upgraded = True latency = now_ms() - start result = RouterResult( query=query, response=response, domain=classification.domain, difficulty=classification.difficulty, confidence=classification.confidence, upgraded=upgraded, quality_score=quality_score, model_used=last_model, route=route, latency_ms=latency, cost_est=0.0, subdomain=subdomain, subdomain2=subdomain2, request_id=request_id, ) self._store_trace(result, route, request_id, query, latency) return result # --------------------------------------------------------------- async def _execute_skill(self, node, classification: Classification, memory: WorkingMemory, route: list) -> Optional[str]: """按节点 kind 调用技能;返回 model_used(失败 None)。""" for dep_id in node.deps: pass # 拓扑序已保证依赖先行;状态由节点自身管理 node.status = "running" skill_name = _KIND_SKILL.get(node.kind, f"es.{node.kind}") try: if self.node_executor is not None and node.kind not in ("retrieve",): # L2 模式:NodeExecutor 后端(组内小模型)执行 resp = await self.node_executor.execute( node, classification.domain, classification.difficulty, memory) text = resp.text model = resp.model_used else: ctx = SkillContext( query=node.query, domain=node.domain or classification.domain, difficulty=classification.difficulty, memory=memory, kb=self.kb, ) text = await self.registry.execute(skill_name, ctx) model = skill_name node.output = text node.status = "done" memory.write_section(node.id, text) route.append(f"skill:{skill_name}@{node.id}") return model except Exception as e: node.status = "failed" node.error = str(e) route.append(f"skill:{skill_name}@{node.id}:error:{type(e).__name__}") return None # --------------------------------------------------------------- def _detect_subdomain(self, query: str, domain: str) -> tuple: hits = self.kb.match(query, domain=domain) sub = 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 def _store_trace(self, result: RouterResult, route: list, request_id: str, query: str, latency: float) -> None: self.trace_store.put(request_id, { "request_id": request_id, "query": query, "domain_group": None, # Agent 模式:无用户指定,完全自主 "domain": result.domain, "difficulty": result.difficulty, "confidence": result.confidence, "subdomain": result.subdomain, "subdomain2": result.subdomain2, "route": list(route), "quality_score": result.quality_score, "upgraded": result.upgraded, "model_used": result.model_used, "latency_ms": round(latency, 2), "cache_hit": False, "cache_level": None, }) # --------------------------------------------------------------- def skills_catalog(self) -> list: """暴露技能目录(Agent 能力清单)。""" return self.registry.catalog()