Files
tzt ce0f6170d3 chore: T-P-1 工作区收敛——并行会话成果与历史未入库文件整理入库
- 入库历史遗漏源码/测试: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
2026-09-05 08:28:25 +08:00

236 lines
10 KiB
Python
Raw Permalink 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.
"""RouteAgentAgent-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()