- 入库历史遗漏源码/测试:router_system 9 模块(agent/executors/inference/knowledge/ memory/planner/skills/trace)、tests 11 个测试文件、config/knowledge 领域知识 - 入库根目录方案文档(v2/v3/可行性×2)、references 文献(arxiv 14-18/cnki_open/ 参考文献清单)、research 论文素材(routerarena/paper/中文文献 PDF) - 前端构建产物刷新(新 hash);webapp 误写文档删除 - gitignore 增补:deepseek-harness、research/_refs、.mimosa/.zcode、网关日志/pid、 临时调试脚本、tests/e2e/node_modules、AI代理功能开发/prefix - 基线确认:318 passed
236 lines
10 KiB
Python
236 lines
10 KiB
Python
"""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()
|