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
This commit is contained in:
tzt
2026-09-05 08:28:25 +08:00
parent 747d85c3ba
commit ce0f6170d3
82 changed files with 74132 additions and 36 deletions
+235
View File
@@ -0,0 +1,235 @@
"""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()
+370
View File
@@ -0,0 +1,370 @@
"""执行器体系(L0 默认专家 + NodeExecutor 后端抽象)。
设计对齐"专家系统风格"(《可行性调研与落地实现路线报告》第八章):
- 输出 = 结构化模板填充(回显查询、知识库事实、领域结构),不追求自然语言流畅度
- 确定性:同输入 → 同输出(无采样随机)
- 最小参数:零模型参数;L2 模式下同一节点可改由本地小模型执行(Router 按配置切换)
kind(子任务动作类型)与模板对应:
analyze 需求/条件分析 | design 方案设计 | implement 代码实现 | solve 数学求解
diagnose 错误定位 | fix 修复方案 | retrieve 知识检索 | conclude 结论
advise 一般建议 | explain 展开解释 | disclaimer 免责/警示 | verify 自检
"""
from __future__ import annotations
from typing import Any, Dict, Optional
from .experts import Expert, extract_content_terms
from .knowledge import KnowledgeBase
from .memory import TaskNode, WorkingMemory
from .models import ExpertResponse
# 各领域"分析"步骤的目标描述
_GOALS = {
"code": "输出可运行的代码实现",
"math": "得到问题的解并给出推导",
"legal": "给出法律结论与依据",
"medical": "给出科普性建议",
"finance": "给出理财/金融建议与风险提示",
"life": "给出实用生活建议",
"education": "给出学习/行动方案",
"general": "给出结构化说明",
}
# 各领域"约束/边界"提示
_CONSTRAINTS = {
"code": "边界条件(空输入、极端值);复杂度目标",
"math": "定义域、无解/多解情况、特殊值",
"legal": "以现行有效法律为准,个案需咨询律师",
"medical": "个体差异;非诊断,请遵医嘱",
"finance": "市场有风险,投资需谨慎;不构成投资建议",
"life": "结合个人实际情况,安全第一",
"education": "结合个人基础与目标,循序渐进",
"general": "围绕核心问题,避免无关展开",
}
# 各领域"验证"清单
_VERIFY_CHECKS = {
"code": ["输入输出覆盖", "边界条件", "复杂度合理", "可运行性"],
"math": ["中间步骤正确", "结果代入验证", "边界/特殊值", "单位与符号"],
"legal": ["法条依据充分", "事实对应", "免责提示", "结论可执行"],
"medical": ["建议有依据", "警示信号明确", "免责提示", "不构成诊断"],
"finance": ["风险提示完整", "数据/规则准确", "免责提示", "建议可执行"],
"life": ["建议实用", "安全提示", "贴合场景"],
"education": ["方案可执行", "目标可衡量", "符合个人基础"],
"general": ["要点覆盖", "逻辑连贯", "无事实错误"],
}
def _kw(query: str, n: int = 6) -> str:
terms = extract_content_terms(query)
return "".join(terms[:n]) if terms else "该主题"
class RuleExecutor(Expert):
"""规则执行器:实现 Expert 接口;L0 模式的默认领域执行器。"""
name = "rule-executor"
def __init__(self, name: str = "rule-executor", domain: str = "general",
kb: Optional[KnowledgeBase] = None):
self.name = name
self.domain = domain
self.kb = kb
async def generate(self, query: str, difficulty: str,
memory: Optional[WorkingMemory] = None,
node: Optional[TaskNode] = None) -> ExpertResponse:
"""按节点 kind 生成确定性输出。兼容 Expert 基类签名(后两参可选)。"""
kind = node.kind if node is not None else "explain"
domain = node.domain if node is not None else self.domain
text = self._template(kind, domain, query, difficulty, memory)
tokens = max(8, int(len(text) / 2.2))
return ExpertResponse(
text=text,
model_used=f"rule:{domain}:{kind}",
latency_ms=0.0,
tokens=tokens,
cost_est=0.0, # 零参数执行器无推理成本
)
# ---------------------------------------------------------------
def _template(self, kind: str, domain: str, query: str, difficulty: str,
memory: Optional[WorkingMemory]) -> str:
facts: Dict[str, Any] = memory.facts if memory else {}
goal = _GOALS.get(domain, _GOALS["general"])
constraints = _CONSTRAINTS.get(domain, _CONSTRAINTS["general"])
kw = _kw(query)
if kind == "analyze":
return (
f"{domain} 分析】\n"
f"- 任务:{query}\n"
f"- 关键要素:{kw}\n"
f"- 目标:{goal}\n"
f"- 约束/边界:{constraints}\n"
f"- 难度评估:{difficulty}"
)
if kind == "design":
return (
f"{domain} 方案设计】\n"
f"针对「{query}」的设计思路:\n"
f"1. 明确核心目标与验收标准\n"
f"2. 选择合适的方法/数据结构(依据:{kw}\n"
f"3. 拆解实现步骤并标注复杂度\n"
f"4. 预留边界处理与异常路径\n"
f"5. 设计自测用例(正常/边界/异常)"
)
if kind == "implement":
return (
f"{domain} 实现】\n"
f"```python\n"
f"def solve() -> None:\n"
f" # 关键点:{kw}\n"
f" # 1. 校验输入与边界条件\n"
f" # 2. 核心逻辑(依据 design 步骤)\n"
f" # 3. 输出结果\n"
f" pass\n"
f"```\n"
f"要点:{kw};复杂度与边界说明见 design/verify 步骤。"
)
if kind == "solve":
return (
f"{domain} 求解】\n"
f"题目:{query}\n"
f"步骤:\n"
f"1. 提取已知条件({kw}\n"
f"2. 选择方法:代数变形/公式代入/逐步推导\n"
f"3. 求解并化简中间结果\n"
f"4. 检查特殊值与边界\n"
f"结论:在标准假设下可得到闭合形式解;完整推导见正式解答。"
)
if kind == "diagnose":
return (
f"{domain} 诊断】\n"
f"错误现象:{query}\n"
f"排查步骤:\n"
f"1. 复现并定位出错行\n"
f"2. 检查变量类型与取值(重点:{kw}\n"
f"3. 核对函数签名、作用域与返回值\n"
f"4. 打印中间变量验证假设\n"
f"5. 用最小样例隔离问题"
)
if kind == "fix":
return (
f"{domain} 修复方案】\n"
f"针对「{query}」:\n"
f"1. 根因:见 diagnose 步骤\n"
f"2. 修复:调整类型/增加空值判断/修正逻辑分支\n"
f"```python\n"
f"def fixed() -> None:\n"
f" # 修复点:{kw}\n"
f" pass\n"
f"```\n"
f"3. 回归:补充对应单测后重跑"
)
if kind == "retrieve":
return self._retrieve(domain, query, memory)
if kind == "conclude":
return (
f"{domain} 结论】\n"
f"综合「{query}」:\n"
f"1. 事实梳理:{kw}\n"
f"2. 适用规则/依据(见 retrieve 步骤)\n"
f"3. 结论:在所述前提下,按上述规则处理\n"
f"4. 注意事项:个案差异,必要时咨询专业人士"
)
if kind == "advise":
return (
f"{domain} 建议】\n"
f"关于「{query}」的一般性建议:\n"
f"1. 基础注意事项({kw}\n"
f"2. 可操作建议:分步执行并观察效果\n"
f"3. 警示信号:出现下列情况应及时就医(见 warning 步骤)"
)
if kind == "explain":
if domain == "code":
return (
f"【code 代码讲解】\n"
f"代码/片段:{query}\n"
f"讲解结构:\n"
f"1. 整体目的:这段代码要解决什么问题({kw}\n"
f"2. 执行流程:按行/按函数梳理数据流与调用链\n"
f"3. 关键点:数据结构、边界处理、异常路径\n"
f"4. 可改进点:命名/复杂度/可读性建议"
)
return (
f"{domain} 说明】\n"
f"主题:{query}\n"
f"1. 背景与定义\n"
f"2. 核心要点:{kw}\n"
f"3. 分类/维度/机制\n"
f"4. 实际应用与注意事项\n"
f"如需更深入分析,可补充上下文。"
)
if kind == "disclaimer":
if domain == "legal":
return (
"⚠️ 提示:以上为一般性法律分析,不构成正式法律意见;"
"个案请咨询执业律师。"
)
if domain == "medical":
return (
"⚠️ 提示:以上内容仅供健康科普,不能替代医生诊断;"
"如有不适请及时就医。"
)
if domain == "finance":
return (
"⚠️ 提示:以上为一般性金融科普,不构成投资建议;"
"投资有风险,决策前请结合自身情况并咨询专业人士。"
)
return ""
if kind == "verify":
checks = _VERIFY_CHECKS.get(domain, _VERIFY_CHECKS["general"])
items = "\n".join(f"- {c}" for c in checks)
return f"{domain} 自检】\n{items}"
if kind == "refactor":
return (
f"【code 重构方案】\n"
f"针对「{query}」:\n"
f"1. 现状问题:重复代码/长函数/命名不清/耦合({kw}\n"
f"2. 重构手法:提取函数、消除魔法数字、引入类或模块、统一命名\n"
f"3. 目标结构:单一职责、清晰分层、可测试性\n"
f"4. 验证:重构前后行为等价(跑通全部测试)"
)
if kind == "testcase":
return (
f"【code 测试用例】\n"
f"针对「{query}」设计测试:\n"
f"```python\n"
f"def test_xxx():\n"
f" # 正常路径:{kw}\n"
f" pass\n\n"
f"def test_edge():\n"
f" # 边界:空输入/极值/None\n"
f" pass\n\n"
f"def test_error():\n"
f" # 异常路径:非法参数\n"
f" pass\n"
f"```\n"
f"覆盖策略:正常 + 边界 + 异常三组,断言明确"
)
if kind == "complexity":
return (
f"【code 复杂度分析】\n"
f"针对「{query}」:\n"
f"1. 时间复杂度:核心循环/递归层数 → 平均与最坏情况({kw}\n"
f"2. 空间复杂度:辅助数据结构占用\n"
f"3. 优化建议:若可接受,给出降复杂度的替代思路"
)
if kind == "optimize":
return (
f"【math 最优化求解】\n"
f"问题:{query}\n"
f"步骤:\n"
f"1. 建立目标函数与约束({kw}\n"
f"2. 求导/配方/不等式法找候选极值点\n"
f"3. 比较候选值并与边界比较\n"
f"4. 结论:给出最大值/最小值及取到条件"
)
if kind == "draft":
return (
f"【写作初稿】\n"
f"主题:{query}\n"
f"结构:\n"
f"1. 开头:点明主题与背景({kw}\n"
f"2. 主体:分点展开,每点配一个例子或依据\n"
f"3. 结尾:总结观点 + 行动建议\n"
f"(初稿完成,待 polish 步骤润色)"
)
if kind == "polish":
return (
f"【写作润色】\n"
f"基于初稿检查:\n"
f"1. 语法与错别字\n"
f"2. 逻辑衔接与段落过渡\n"
f"3. 语气统一(正式/亲切)与受众匹配\n"
f"4. 长度控制与重点突出({kw}"
)
# 未知 kind 兜底
return f"(规则执行器)「{query}」:{kw}"
# ---------------------------------------------------------------
def _retrieve(self, domain: str, query: str,
memory: Optional[WorkingMemory]) -> str:
"""知识检索:从知识库事实表取命中的条目;无命中则给出查阅建议。"""
if self.kb is None:
return (
f"{domain} 知识检索】\n"
f"未配置知识库,建议查阅权威资料({_kw(query)})。"
)
facts = self.kb.facts(domain)
hits = [f for f in facts if any(k in query for k in f.get("keywords", []))]
if hits:
lines = [f"- {f['statement']}" for f in hits]
return f"{domain} 知识检索】\n" + "\n".join(lines)
return (
f"{domain} 知识检索】\n"
f"未命中知识库条目;建议以现行有效法规/最新指南为准,"
f"并结合个案情况分析({_kw(query)})。"
)
# ===============================================================
# NodeExecutor:子任务执行后端抽象(T1:整体项目部分拆解·先行实现)
#
# Router._execute_node 不再内联 if-else 分支,而是依赖 NodeExecutor 接口:
# - RuleNodeExecutor L0 规则执行器(零参数、确定性)
# - ModelNodeExecutorL2 专家池小模型(≤8B,按需加载)
# - 未来可加:多路采样执行器、API 执行器、组内模型执行器……
# 工厂按配置选择后端,新增后端无需改动 Router。
# ===============================================================
class NodeExecutor:
"""子任务执行后端抽象接口。"""
name: str = "node-executor"
async def execute(self, node: TaskNode, domain: str, difficulty: str,
memory: WorkingMemory) -> ExpertResponse:
raise NotImplementedError
class RuleNodeExecutor(NodeExecutor):
"""L0:规则执行器后端(零参数、确定性、零成本)。"""
name = "rule"
def __init__(self, kb: Optional[KnowledgeBase] = None):
self._rule = RuleExecutor("rule-executor", "general", kb=kb)
async def execute(self, node: TaskNode, domain: str, difficulty: str,
memory: WorkingMemory) -> ExpertResponse:
return await self._rule.generate(node.query, difficulty, memory, node)
class ModelNodeExecutor(NodeExecutor):
"""L2:专家池小模型后端(≤8B;组内模型按需加载,用完即卸载由推理服务管理)。"""
name = "model"
def __init__(self, experts: Dict[str, Expert]):
self._experts = experts
async def execute(self, node: TaskNode, domain: str, difficulty: str,
memory: WorkingMemory) -> ExpertResponse:
expert = self._experts.get(node.domain) or self._experts.get("general")
return await expert.generate(node.query, difficulty)
def build_node_executor(backend: str, kb: Optional[KnowledgeBase] = None,
experts: Optional[Dict[str, Expert]] = None) -> NodeExecutor:
"""按配置选择子任务执行后端。"""
if backend == "rule":
return RuleNodeExecutor(kb=kb)
if backend in ("hf", "api", "model"):
if not experts:
raise ValueError("ModelNodeExecutor 需要专家池(experts")
return ModelNodeExecutor(experts)
raise ValueError(f"未知执行后端: {backend}(支持 rule | hf | api | model")
+93
View File
@@ -0,0 +1,93 @@
"""前向链推理机:知识库规则驱动的工作记忆演化(专家系统推理核心,零依赖)。
流程(经典前向链 forward chaining):
1. 初始化黑板:写入领域/难度/置信度等事实
2. 循环:在领域内匹配规则(未触发过的)→ 按优先级执行
- 命中即记录轨迹 rule:<id>@<priority>
- 规则带 output 模板 → 渲染后写入黑板章节(部分解)
- 规则带 actions → 执行动作(写事实/写章节)
3. 终止:无新规则可触发 / 达到步数上限(防死循环)
确定性保证:规则匹配基于子串包含,无随机性;同输入 → 同轨迹。
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from .knowledge import KnowledgeBase, Rule
from .memory import WorkingMemory
def render_template(template: str, query: str, facts: Dict[str, Any]) -> str:
"""渲染输出模板:替换 {query}{facts.<key>} 占位符;缺失以 [未提供] 占位,不抛异常。"""
out = template.replace("{query}", query)
for key, value in facts.items():
out = out.replace(f"{{facts.{key}}}", str(value))
# 剩余占位符兜底
while "{" in out and "}" in out:
start = out.find("{")
end = out.find("}", start)
if end == -1:
break
out = out[:start] + "[未提供]" + out[end + 1:]
return out
class InferenceEngine:
"""前向链推理机。"""
def __init__(self, kb: KnowledgeBase, max_steps: int = 20):
self.kb = kb
self.max_steps = max_steps
# ---------------------------------------------------------------
def initialize(self, query: str, domain: str, difficulty: str,
confidence: float, memory: WorkingMemory) -> None:
"""把分类结果写入黑板(事实初始化)。"""
memory.write_fact("query", query)
memory.write_fact("domain", domain)
memory.write_fact("difficulty", difficulty)
memory.write_fact("confidence", round(confidence, 4))
memory.add_trace(f"init:domain={domain},difficulty={difficulty},conf={confidence:.2f}")
# ---------------------------------------------------------------
def run(self, query: str, domain: str, memory: WorkingMemory,
max_steps: Optional[int] = None) -> List[str]:
"""前向链主循环。返回触发规则 id 列表(按触发顺序)。"""
steps = max_steps or self.max_steps
fired: List[str] = []
for _ in range(steps):
rules = self.kb.match(query, domain=domain)
# 选第一个"未触发过"的规则
target: Optional[Rule] = None
for r in rules:
if r.id not in fired:
target = r
break
if target is None:
break # 无新规则可触发 → 终止
fired.append(target.id)
self._fire(target, query, memory)
return fired
# ---------------------------------------------------------------
def _fire(self, rule: Rule, query: str, memory: WorkingMemory) -> None:
"""执行一条规则:记录轨迹 + 写事实 + 产出章节。"""
memory.add_trace(f"rule:{rule.id}@{rule.priority}")
# 规则动作
for action in rule.actions:
self._apply_action(action, rule, query, memory)
# 规则输出模板 → 章节
if rule.output:
text = render_template(rule.output, query, memory.facts)
memory.write_section(rule.id, text)
def _apply_action(self, action: str, rule: Rule, query: str,
memory: WorkingMemory) -> None:
"""动作格式:write_fact:key=valuevalue 支持 {query} 占位)。"""
if action.startswith("write_fact:"):
kv = action[len("write_fact:"):]
key, _, value = kv.partition("=")
value = value.replace("{query}", query)
memory.write_fact(key.strip(), value.strip(), rule_id=rule.id)
# 其他动作类型暂不实现(保留扩展位)
+365
View File
@@ -0,0 +1,365 @@
"""知识库:专家系统风格的规则与知识表示(零依赖,纯标准库)。
设计原则(对齐《可行性调研与落地实现路线报告》第八章"专家系统内核"):
- 领域知识显式化:写在规则文件里(config/knowledge/<domain>.yaml),不藏在模型参数中
- 确定性:规则匹配 = 子串包含(大小写不敏感),同输入同输出
- 可解释:每次命中都记录规则 id,形成推理轨迹
- 最小参数:L0 模式零模型参数,规则即知识
规则文件格式(YAML;若 pyyaml 不可用,可提供同名 .json):
domain: code
rules:
- id: code-sort
priority: 90 # 越大越先触发
patterns: ["排序", "sort"] # 任一子串命中即触发
template: code-implement # 可选:Planner 任务模板 id
output: | # 可选:输出模板({query} 等占位符)
(规则输出)...
facts: # 领域事实表(Judge 校验 / retrieve 执行器用)
- id: legal-nc
keywords: ["竞业"]
statement: "竞业限制期限不得超过二年"
任务模板(config/knowledge/tasks.yaml):
task_templates:
code-implement:
steps:
- {id: analyze, kind: analyze, domain: code}
- {id: design, kind: design, domain: code, deps: [analyze]}
加载顺序:内置默认规则(代码内兜底)→ 文件规则按 id 合并覆盖。
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional
DEFAULT_RULES_DIR = Path(__file__).resolve().parent.parent / "config" / "knowledge"
@dataclass
class Rule:
"""一条领域规则。"""
id: str
domain: str
priority: int = 50
patterns: List[str] = field(default_factory=list)
template: Optional[str] = None # 引用的任务模板 id
output: Optional[str] = None # 输出模板
actions: List[str] = field(default_factory=list) # 保留字段:动作扩展
subdomain: Optional[str] = None # 二级子领域(如 investing/labor/calculus
subdomain2: Optional[str] = None # 三级子领域(如 fund/overtime/sorting
def matches(self, text: str) -> bool:
"""任一 pattern 是 text 的子串即命中(大小写不敏感)。"""
if not self.patterns:
return False
q = text.lower()
return any(p.lower() in q for p in self.patterns)
# ---------------------------------------------------------------
# 三级子领域映射(rule_id -> subdomain2
# 集中维护:新增规则时在此加一行即可完成三级细化标注
# ---------------------------------------------------------------
SUBDOMAIN2_MAP: Dict[str, str] = {
# ---- code ----
"code-sort": "sorting",
"code-debug": "error-analysis",
"code-algorithm": "algorithm-general",
"code-refactor": "code-quality",
"code-database": "sql",
"code-explain": "code-reading",
"code-test": "unit-test",
"code-web": "web-dev",
"code-implement-general": "implementation",
"code-git-knowledge": "git",
"code-docker-knowledge": "container",
"code-python-knowledge": "python-env",
# ---- math ----
"math-equation": "equation",
"math-calculus": "calculus",
"math-algebra": "algebra",
"math-geometry": "geometry",
"math-proof": "proof",
"math-probability": "probability",
"math-number-theory": "number-theory",
"math-trigonometry": "trigonometry",
"math-optimization": "optimization",
"math-general": "math-general",
# ---- legal ----
"legal-contract": "contract",
"legal-labor": "labor",
"legal-ip": "intellectual-property",
"legal-housing": "housing",
"legal-marriage": "family-law",
"legal-tax": "tax",
"legal-consumer": "consumer-rights",
"legal-litigation": "litigation",
"legal-compliance": "compliance",
"legal-general": "legal-general",
# ---- medical ----
"medical-hypertension": "hypertension",
"medical-drug": "medication",
"medical-common": "common-illness",
"medical-chronic": "chronic-disease",
"medical-digestive": "digestive",
"medical-nutrition": "nutrition",
"medical-mental": "mental-health",
"medical-firstaid": "first-aid",
"medical-pediatrics": "pediatrics",
"medical-general": "medical-general",
# ---- finance ----
"finance-investing": "investing",
"finance-saving": "saving",
"finance-loan": "loan",
"finance-insurance": "insurance",
"finance-credit-card": "credit",
"finance-personal-budget": "budgeting",
"finance-general": "finance-general",
# ---- life ----
"life-food": "cooking",
"life-travel": "travel",
"life-home": "home",
"life-pet": "pet",
"life-fitness": "fitness",
"life-weather": "weather",
"life-general": "life-general",
# ---- education ----
"edu-study-method": "study-method",
"edu-exam": "exam",
"edu-language": "language",
"edu-course": "course",
"edu-career": "career",
"edu-general": "education-general",
# ---- general ----
"general-explain": "explain",
"general-writing": "writing",
"general-compare": "compare",
"general-translate": "translate",
"general-knowledge": "explain",
}
# ---------------------------------------------------------------
# 内置默认规则(兜底:即使规则文件缺失/损坏,系统仍可运行)
# ---------------------------------------------------------------
BUILTIN_RULES: List[Dict[str, Any]] = [
# ---- code ----
{"id": "code-sort", "domain": "code", "priority": 90,
"patterns": ["排序", "快速排序", "排序算法", "sort", "quicksort"],
"template": "code-implement"},
{"id": "code-debug", "domain": "code", "priority": 85,
"patterns": ["报错", "错误", "调试", "bug", "debug", "typeerror", "异常", "报 TypeError"],
"template": "code-debug"},
{"id": "code-implement-general", "domain": "code", "priority": 50,
"patterns": ["实现", "编写", "写一个", "函数", "代码", "编程", "用 python", "用 java",
"用 javascript", "sql", "接口", "算法"],
"template": "code-implement"},
# ---- math ----
{"id": "math-equation", "domain": "math", "priority": 90,
"patterns": ["方程", "求解", "求根", "solve", "equation", "解方程"],
"template": "math-solve"},
{"id": "math-calculus", "domain": "math", "priority": 85,
"patterns": ["积分", "导数", "微积分", "求导", "integral", "derivative", ""],
"template": "math-solve"},
{"id": "math-general", "domain": "math", "priority": 50,
"patterns": ["数学", "证明", "定理", "概率", "统计", "计算", "等于", "math", "不等式"],
"template": "math-solve"},
# ---- legal ----
{"id": "legal-contract", "domain": "legal", "priority": 90,
"patterns": ["合同", "条款", "违约", "离职", "竞业", "劳动", "contract", "clause", "赔偿"],
"template": "legal-advice"},
{"id": "legal-ip", "domain": "legal", "priority": 85,
"patterns": ["专利", "版权", "商标", "知识产权", "patent", "copyright", "trademark"],
"template": "legal-advice"},
{"id": "legal-general", "domain": "legal", "priority": 50,
"patterns": ["法律", "合规", "诉讼", "仲裁", "法条", "law", "legal", "法规"],
"template": "legal-advice"},
# ---- medical ----
{"id": "medical-hypertension", "domain": "medical", "priority": 90,
"patterns": ["高血压", "hypertension", "血压"],
"template": "medical-advice"},
{"id": "medical-drug", "domain": "medical", "priority": 85,
"patterns": ["药物", "吃药", "剂量", "副作用", "退烧药", "降压药", "dosage", "prescription"],
"template": "medical-advice"},
{"id": "medical-general", "domain": "medical", "priority": 50,
"patterns": ["医疗", "症状", "诊断", "治疗", "感冒", "发烧", "糖尿病", "医生", "患者",
"体检", "疫苗", "medical", "symptom", "disease"],
"template": "medical-advice"},
# ---- general ----
{"id": "general-explain", "domain": "general", "priority": 30,
"patterns": ["总结", "介绍", "解释", "为什么", "优缺点", "是什么", "翻译", "邮件",
"summarize", "explain", "what is", "写一封"],
"template": "general-explain"},
]
# 内置默认任务模板(兜底)
BUILTIN_TASKS: Dict[str, Dict[str, Any]] = {
"code-implement": {"steps": [
{"id": "analyze", "kind": "analyze", "domain": "code", "desc": "需求与约束分析"},
{"id": "design", "kind": "design", "domain": "code", "deps": ["analyze"], "desc": "算法与数据结构设计"},
{"id": "implement", "kind": "implement", "domain": "code", "deps": ["design"], "desc": "实现代码"},
{"id": "verify", "kind": "verify", "domain": "code", "deps": ["implement"], "desc": "自测校验"},
]},
"code-debug": {"steps": [
{"id": "analyze", "kind": "analyze", "domain": "code", "desc": "错误现象与复现分析"},
{"id": "diagnose", "kind": "diagnose", "domain": "code", "deps": ["analyze"], "desc": "定位错误根因"},
{"id": "fix", "kind": "fix", "domain": "code", "deps": ["diagnose"], "desc": "给出修复方案"},
{"id": "verify", "kind": "verify", "domain": "code", "deps": ["fix"], "desc": "修复后验证"},
]},
"math-solve": {"steps": [
{"id": "conditions", "kind": "analyze", "domain": "math", "desc": "明确已知条件与目标"},
{"id": "solve", "kind": "solve", "domain": "math", "deps": ["conditions"], "desc": "选择方法并求解"},
{"id": "verify", "kind": "verify", "domain": "math", "deps": ["solve"], "desc": "检查边界与验证"},
]},
"legal-advice": {"steps": [
{"id": "facts", "kind": "analyze", "domain": "legal", "desc": "梳理事实与法律问题"},
{"id": "retrieve", "kind": "retrieve", "domain": "legal", "deps": ["facts"], "desc": "检索适用法规"},
{"id": "conclude", "kind": "conclude", "domain": "legal", "deps": ["retrieve"], "desc": "给出法律意见"},
{"id": "disclaimer", "kind": "disclaimer", "domain": "legal", "deps": ["conclude"], "desc": "免责提示"},
]},
"medical-advice": {"steps": [
{"id": "symptoms", "kind": "analyze", "domain": "medical", "desc": "梳理症状与背景"},
{"id": "advise", "kind": "advise", "domain": "medical", "deps": ["symptoms"], "desc": "给出一般建议"},
{"id": "warning", "kind": "disclaimer", "domain": "medical", "deps": ["advise"], "desc": "就医警示"},
]},
"general-explain": {"steps": [
{"id": "outline", "kind": "analyze", "domain": "general", "desc": "梳理主题要点"},
{"id": "explain", "kind": "explain", "domain": "general", "deps": ["outline"], "desc": "展开解释"},
{"id": "conclude", "kind": "conclude", "domain": "general", "deps": ["explain"], "desc": "总结"},
]},
}
# 内置默认事实表(兜底)
BUILTIN_FACTS: Dict[str, List[Dict[str, Any]]] = {
"legal": [
{"id": "legal-noncompete", "keywords": ["竞业", "离职", "同业"],
"statement": "竞业限制期限不得超过二年,且用人单位应在限制期内按月给予经济补偿"},
{"id": "legal-renew-compensation", "keywords": ["不续签", "经济补偿", "劳动合同"],
"statement": "劳动合同期满用人单位不续签的,通常应支付经济补偿(每满一年一个月工资)"},
],
"medical": [
{"id": "medical-hypertension-diet", "keywords": ["高血压", "饮食"],
"statement": "高血压患者应低盐低脂饮食、控制体重、规律运动、戒烟限酒,并在医生指导下用药"},
{"id": "medical-fever-drug", "keywords": ["发烧", "退烧"],
"statement": "体温超过 38.5℃ 可在药师指导下使用退烧药;持续发热或出现严重症状应及时就医"},
],
}
def _try_load_yaml(path: Path) -> Optional[Dict[str, Any]]:
try:
import yaml # type: ignore
except ImportError:
return None
try:
with open(path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return data if isinstance(data, dict) else None
except Exception:
return None
def _try_load_json(path: Path) -> Optional[Dict[str, Any]]:
json_path = path.with_suffix(".json")
if not json_path.exists():
return None
try:
with open(json_path, "r", encoding="utf-8") as f:
data = json.load(f)
return data if isinstance(data, dict) else None
except Exception:
return None
class KnowledgeBase:
"""知识库:加载规则文件,提供规则匹配、任务模板、事实表查询。"""
def __init__(self, rules_dir: Optional[str | Path] = None):
self.rules_dir = Path(rules_dir) if rules_dir else DEFAULT_RULES_DIR
self._rules: Dict[str, Rule] = {}
self._tasks: Dict[str, Dict[str, Any]] = {}
self._facts: Dict[str, List[Dict[str, Any]]] = {}
self.load()
# ---- 加载 ----
def load(self) -> None:
"""内置默认 + 规则文件合并(文件规则按 id 覆盖内置)。"""
self._rules = {}
self._tasks = dict(BUILTIN_TASKS)
for item in BUILTIN_RULES:
self._register_rule(item)
self._facts = {d: [dict(f) for f in facts] for d, facts in BUILTIN_FACTS.items()}
if self.rules_dir.is_dir():
for f in sorted(self.rules_dir.glob("*.yaml")):
data = _try_load_yaml(f)
if data is not None:
self._load_file_data(f, data)
for f in sorted(self.rules_dir.glob("*.json")):
if f.name not in {p.name for p in self.rules_dir.glob("*.yaml")}:
data = _try_load_json(f)
if data is not None:
self._load_file_data(f, data)
def _load_file_data(self, path: Path, data: Dict[str, Any]) -> None:
name = path.stem
if name == "tasks":
for tid, tpl in (data.get("task_templates") or {}).items():
if isinstance(tpl, dict) and isinstance(tpl.get("steps"), list):
self._tasks[tid] = tpl
return
domain = data.get("domain", name)
for item in data.get("rules") or []:
if isinstance(item, dict) and item.get("id"):
self._register_rule({**item, "domain": domain})
for fact in data.get("facts") or []:
if isinstance(fact, dict) and fact.get("id"):
self._facts.setdefault(domain, []).append(fact)
def _register_rule(self, item: Dict[str, Any]) -> None:
rule = Rule(
id=str(item["id"]),
domain=str(item.get("domain", "general")),
priority=int(item.get("priority", 50)),
patterns=[str(p) for p in item.get("patterns", [])],
template=item.get("template"),
output=item.get("output"),
actions=[str(a) for a in item.get("actions", [])],
subdomain=item.get("subdomain"),
subdomain2=item.get("subdomain2") or SUBDOMAIN2_MAP.get(str(item["id"])),
)
self._rules[rule.id] = rule
# ---- 查询 ----
def match(self, text: str, domain: Optional[str] = None) -> List[Rule]:
"""返回命中的规则,按优先级降序。domain 为空则全领域匹配。"""
hits = []
for rule in self._rules.values():
if domain is not None and rule.domain != domain:
continue
if rule.matches(text):
hits.append(rule)
hits.sort(key=lambda r: r.priority, reverse=True)
return hits
def rule(self, rule_id: str) -> Optional[Rule]:
return self._rules.get(rule_id)
def rules_count(self) -> int:
return len(self._rules)
def task_template(self, tid: str) -> Optional[Dict[str, Any]]:
return self._tasks.get(tid)
def task_ids(self) -> List[str]:
return sorted(self._tasks.keys())
def facts(self, domain: str) -> List[Dict[str, Any]]:
return self._facts.get(domain, [])
def domains(self) -> List[str]:
return sorted({r.domain for r in self._rules.values()})
+128
View File
@@ -0,0 +1,128 @@
"""黑板(Blackboard)/ 工作记忆:专家系统风格的共享工作区(零依赖)。
- TaskNode:子任务节点(DAG 顶点),由 Planner 创建、Router 按拓扑序执行
- TaskGraph:子任务 DAG,提供拓扑排序与状态查询
- WorkingMemory:黑板,各知识源(执行器/规则)写入部分解,最后合并为最终答案
对齐《可行性调研与落地实现路线报告》第八章:
"黑板协作:多知识源(领域专家/执行器)通过共享黑板协作,而不是一个模型全包"
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class TaskNode:
"""一个子任务节点。"""
id: str
kind: str # analyze | design | implement | solve | diagnose | fix
# | retrieve | conclude | advise | explain | disclaimer | verify
domain: str
query: str # 子任务输入(通常为原始查询)
status: str = "pending" # pending | running | done | failed | skipped
output: Optional[str] = None
rule_trace: List[str] = field(default_factory=list)
deps: List[str] = field(default_factory=list)
desc: str = ""
error: Optional[str] = None
class TaskGraph:
"""子任务 DAG:节点 + 依赖边。"""
def __init__(self):
self._nodes: Dict[str, TaskNode] = {}
def add_node(self, node: TaskNode) -> None:
if node.id in self._nodes:
raise ValueError(f"节点 id 重复: {node.id}")
self._nodes[node.id] = node
def get(self, node_id: str) -> Optional[TaskNode]:
return self._nodes.get(node_id)
def nodes(self) -> List[TaskNode]:
return list(self._nodes.values())
def topo_order(self) -> List[TaskNode]:
"""Kahn 拓扑排序:依赖在前。循环依赖时按插入序兜底(不崩溃)。"""
indeg: Dict[str, int] = {}
for n in self._nodes.values():
indeg[n.id] = 0
for n in self._nodes.values():
for d in n.deps:
if d in indeg:
indeg[n.id] += 1
ready = [n for n in self._nodes.values() if indeg[n.id] == 0]
ready.sort(key=lambda n: list(self._nodes.keys()).index(n.id))
order: List[TaskNode] = []
while ready:
n = ready.pop(0)
order.append(n)
for m in self._nodes.values():
if n.id in m.deps:
indeg[m.id] -= 1
if indeg[m.id] == 0 and m not in order:
ready.append(m)
if len(order) < len(self._nodes):
# 循环依赖兜底:剩余节点按插入序追加
for n in self._nodes.values():
if n not in order:
order.append(n)
return order
def all_done(self) -> bool:
return all(n.status == "done" for n in self._nodes.values())
def failed(self) -> List[TaskNode]:
return [n for n in self._nodes.values() if n.status == "failed"]
def __len__(self) -> int:
return len(self._nodes)
class WorkingMemory:
"""黑板:facts(槽位事实)+ sections(章节部分解)+ trace(推理轨迹)。"""
def __init__(self):
self.facts: Dict[str, Any] = {}
self.sections: Dict[str, str] = {}
self.trace: List[str] = []
# ---- 事实 ----
def write_fact(self, key: str, value: Any, rule_id: Optional[str] = None) -> None:
if key in self.facts:
self.trace.append(f"overwrite:{key}@{rule_id or '?'}")
self.facts[key] = value
if rule_id:
self.trace.append(f"fact:{key}={str(value)[:40]}@rule:{rule_id}")
def get_fact(self, key: str, default: Any = None) -> Any:
return self.facts.get(key, default)
# ---- 章节 ----
def write_section(self, sid: str, text: str) -> None:
"""写入章节;同 id 覆盖(记录 trace)。"""
if sid in self.sections:
self.trace.append(f"overwrite_section:{sid}")
self.sections[sid] = text
def section(self, sid: str) -> Optional[str]:
return self.sections.get(sid)
def merge(self, order: Optional[List[str]] = None) -> str:
"""按 order(章节顺序)合并为最终答案;order 为空则按写入顺序。"""
if order:
parts = [self.sections[s] for s in order if s in self.sections]
if parts:
return "\n\n".join(parts)
return "\n\n".join(self.sections.values())
# ---- 轨迹 ----
def add_trace(self, item: str) -> None:
self.trace.append(item)
def explain(self) -> List[str]:
return list(self.trace)
+103
View File
@@ -0,0 +1,103 @@
"""规则 Planner:把查询拆解为子任务 DAG(任务分解,专家系统风格,零参数)。
拆解逻辑(确定性规则):
1. 在分类领域内匹配知识规则
2. 取最高优先级且带 template 的命中规则 → 对应任务模板
3. 非 easy 难度且有模板 → 生成多节点 DAG(模板 steps 转 TaskNode,含依赖)
4. easy 难度或无模板命中 → 单节点直接求解(不拆,最小开销)
5. 拆解深度防护:节点不再递归拆解(当前为单层拆解,模板本身即最终粒度)
对齐架构目标:"路由模型把任务拆解后分步骤交给各个小模型"
L0 模式下各子任务由规则执行器完成(零参数),L2 模式可交给本地小模型。
"""
from __future__ import annotations
from typing import List, Optional
from .knowledge import KnowledgeBase
from .memory import TaskGraph, TaskNode
from .models import Classification
# 单节点求解时按领域选择默认动作 kind
_SINGLE_KIND = {
"code": "implement",
"math": "solve",
"legal": "conclude",
"medical": "advise",
"general": "explain",
"finance": "conclude",
"life": "advise",
"education": "design",
}
# 强制拆解领域:即使 easy 也走完整任务模板
# legal 需要 retrieve+disclaimermedical 需要 advise+warning
# finance 需要 retrieve+风险免责——均为领域硬要求)
FORCE_SPLIT_DOMAINS = {"legal", "medical", "finance"}
# 强制拆解模板:命中即拆(debug 流程必须 analyze→diagnose→fix→verify
FORCE_SPLIT_TEMPLATES = {"code-debug"}
class Planner:
"""规则 Planner:查询 → 子任务 DAG。"""
def __init__(self, kb: KnowledgeBase, max_depth: int = 3):
self.kb = kb
self.max_depth = max_depth
# ---------------------------------------------------------------
def plan(self, query: str, classification: Classification) -> TaskGraph:
domain = classification.domain
difficulty = classification.difficulty
# 1. 领域内匹配规则,取最高优先级带模板的规则
template_id: Optional[str] = None
hits = self.kb.match(query, domain=domain)
for h in hits:
if h.template:
template_id = h.template
break
graph = TaskGraph()
# 2. 非 easy / 强制拆解领域 / 强制拆解模板 → 多节点 DAG
if template_id and (difficulty != "easy"
or domain in FORCE_SPLIT_DOMAINS
or template_id in FORCE_SPLIT_TEMPLATES):
tpl = self.kb.task_template(template_id)
if tpl and tpl.get("steps"):
for step in tpl["steps"]:
node = TaskNode(
id=str(step["id"]),
kind=str(step.get("kind", "solve")),
domain=str(step.get("domain", domain)),
query=query,
deps=[str(d) for d in step.get("deps", [])],
desc=str(step.get("desc", "")),
)
graph.add_node(node)
return graph
# 3. easy / 无模板 → 单节点
kind = _SINGLE_KIND.get(domain, "explain")
graph.add_node(TaskNode(
id="solve",
kind=kind,
domain=domain,
query=query,
desc=f"单节点求解({domain}/{difficulty}",
))
return graph
# ---------------------------------------------------------------
def explain_plan(self, graph: TaskGraph) -> List[str]:
"""把 DAG 渲染为可读的拆解轨迹(用于 route 与 --trace)。"""
if len(graph) == 1:
n = graph.nodes()[0]
return [f"plan:single[{n.kind}]"]
parts = []
for n in graph.topo_order():
dep = f"<{','.join(n.deps)}" if n.deps else ""
parts.append(f"{n.id}:{n.kind}{dep}")
return [f"plan:multi[{len(graph)}]({' -> '.join(parts)})"]
+217
View File
@@ -0,0 +1,217 @@
"""Skill 技能体系(T12Agent-Skill 路由器·先行实现)。
把路由器独立为"技能注册表 + Agent 规划器"
- 用户只需提供需求,无需指定领域/模型
- Agent 路由器自行分析需求 → 规划 skill 调用序列(可组合、可依赖)→ 执行
Skill 抽象:name(唯一标识)+ description(能力描述,供规划器选择)
+ params(参数说明)+ execute(ctx)(执行,返回文本结果)。
内置技能(把专家系统内核能力封装为可调用单元):
- es.<kind> :规则执行器模板生成(analyze/design/implement/solve/... 共 18 种)
- kb.retrieve :知识库事实检索(法律/医疗/金融事实条目)
- kb.answer :规则 output 知识问答(git/docker/天气/翻译等常识条目)
- judge.evaluate:质量评分
- fallback.call :最后处理者(升级)
- cache.get / cache.put:两阶段缓存
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from .executors import RuleExecutor
from .knowledge import KnowledgeBase
from .memory import TaskNode, WorkingMemory
from .models import ExpertResponse
@dataclass
class SkillContext:
"""一次 skill 调用的执行上下文(黑板 + 输入参数)。"""
query: str
domain: str
difficulty: str
memory: WorkingMemory
kb: Optional[KnowledgeBase] = None
args: Dict[str, Any] = field(default_factory=dict)
class Skill:
"""技能抽象:可被 Agent 路由器调用的能力单元。"""
name: str = "skill"
description: str = ""
params: List[str] = field(default_factory=list)
async def execute(self, ctx: SkillContext) -> str:
raise NotImplementedError
def __repr__(self) -> str:
return f"<Skill {self.name}>"
class SkillRegistry:
"""技能注册表:注册 / 发现 / 执行。"""
def __init__(self):
self._skills: Dict[str, Skill] = {}
def register(self, skill: Skill) -> None:
if skill.name in self._skills:
raise ValueError(f"技能重复注册: {skill.name}")
self._skills[skill.name] = skill
def get(self, name: str) -> Optional[Skill]:
return self._skills.get(name)
def has(self, name: str) -> bool:
return name in self._skills
def list(self) -> List[str]:
return sorted(self._skills.keys())
def catalog(self) -> List[Dict[str, Any]]:
"""技能目录(供 Agent 规划器 / 用户发现使用)。"""
return [
{"name": s.name, "description": s.description, "params": list(s.params)}
for s in sorted(self._skills.values(), key=lambda s: s.name)
]
async def execute(self, name: str, ctx: SkillContext) -> str:
skill = self._skills.get(name)
if skill is None:
raise KeyError(f"未注册技能: {name}(可用: {self.list()}")
return await skill.execute(ctx)
# ===============================================================
# 内置技能实现
# ===============================================================
class TemplateSkill(Skill):
"""es.<kind>:规则执行器模板生成(确定性、零参数)。"""
def __init__(self, kind: str, executor: RuleExecutor):
self.kind = kind
self._executor = executor
self.name = f"es.{kind}"
self.description = f"规则模板生成({kind}):结构化确定性输出"
self.params = ["domain", "difficulty", "memory"]
async def execute(self, ctx: SkillContext) -> str:
node = TaskNode(
id="skill", kind=self.kind, domain=ctx.domain,
query=ctx.query, desc=f"skill:{self.name}",
)
resp: ExpertResponse = await self._executor.generate(
ctx.query, ctx.difficulty, ctx.memory, node)
return resp.text
class KBRetrieveSkill(Skill):
"""kb.retrieve:知识库事实检索(法律/医疗/金融事实条目)。"""
name = "kb.retrieve"
description = "从知识库事实表检索领域知识条目(法条/指南/理财常识等)"
params = ["domain", "query"]
def __init__(self, kb: Optional[KnowledgeBase] = None):
self._kb = kb
async def execute(self, ctx: SkillContext) -> str:
kb = self._kb or ctx.kb
if kb is None:
return "kb.retrieve)未配置知识库。"
facts = kb.facts(ctx.domain)
hits = [f for f in facts if any(k in ctx.query for k in f.get("keywords", []))]
if hits:
lines = [f"- {f['statement']}" for f in hits]
return f"{ctx.domain} 知识检索】\n" + "\n".join(lines)
return (f"{ctx.domain} 知识检索】\n未命中知识库条目;"
f"建议以现行有效法规/最新指南为准。")
class KBAnswerSkill(Skill):
"""kb.answer:规则 output 知识问答(git/docker/天气/翻译等常识条目)。"""
name = "kb.answer"
description = "知识库规则问答:命中 output 规则直接给出知识章节"
params = ["domain", "query"]
def __init__(self, kb: Optional[KnowledgeBase] = None):
self._kb = kb
async def execute(self, ctx: SkillContext) -> str:
kb = self._kb or ctx.kb
if kb is None:
return "kb.answer)未配置知识库。"
hits = kb.match(ctx.query, domain=ctx.domain)
for h in hits:
if h.output:
from .inference import render_template
return render_template(h.output, ctx.query, {})
return "kb.answer)未命中知识规则。"
class JudgeSkill(Skill):
"""judge.evaluate:质量评分(5 维:覆盖/长度/格式/安全/知识引用)。"""
name = "judge.evaluate"
description = "评估回答质量(0-1 分),低于阈值建议升级"
params = ["query", "response", "domain"]
def __init__(self, judge, fallback_threshold: float = 0.70):
self._judge = judge
self._threshold = fallback_threshold
async def execute(self, ctx: SkillContext) -> str:
response = ctx.args.get("response", "")
domain = ctx.args.get("domain", ctx.domain)
eval_result = await self._judge.evaluate(ctx.query, response, domain)
return (f"【质量评分】{eval_result.overall_score:.2f} "
f"{'(需升级)' if eval_result.needs_fallback else '(达标)'} "
f"{'; '.join(eval_result.reasons)}")
class FallbackSkill(Skill):
"""fallback.call:最后处理者(升级/降级兜底)。"""
name = "fallback.call"
description = "调用最后处理者(本地≤8B 模型 / 降级模板 / mock)"
params = ["query"]
def __init__(self, fallback):
self._fallback = fallback
async def execute(self, ctx: SkillContext) -> str:
resp = await self._fallback.generate(ctx.query)
return resp.text
def build_skill_registry(kb: Optional[KnowledgeBase] = None,
executor: Optional[RuleExecutor] = None,
judge=None, fallback=None,
fallback_threshold: float = 0.70) -> SkillRegistry:
"""构建内置技能注册表。
模板技能自动注册 18 种 kindanalyze/design/implement/solve/diagnose/fix/
retrieve/conclude/advise/explain/disclaimer/verify/refactor/testcase/
complexity/optimize/draft/polish);retrieve 由 kb.retrieve 接管(更专)。
"""
reg = SkillRegistry()
exec_ = executor or RuleExecutor("rule-executor", "general", kb=kb)
_EXCLUDED = {"retrieve"} # retrieve 用 kb.retrieve(知识库驱动)
for kind in ("analyze", "design", "implement", "solve", "diagnose", "fix",
"conclude", "advise", "explain", "disclaimer", "verify",
"refactor", "testcase", "complexity", "optimize", "draft",
"polish"):
if kind not in _EXCLUDED:
reg.register(TemplateSkill(kind, exec_))
reg.register(KBRetrieveSkill(kb=kb))
reg.register(KBAnswerSkill(kb=kb))
if judge is not None:
reg.register(JudgeSkill(judge, fallback_threshold=fallback_threshold))
if fallback is not None:
reg.register(FallbackSkill(fallback))
return reg
+49
View File
@@ -0,0 +1,49 @@
"""推理链轨迹存储(T3:整体项目部分拆解·先行实现)。
内存环形缓冲(零依赖):记录每次请求的完整推理链(两级路由决策、
三级子领域、规则触发、任务拆解、节点执行、质量评分),支持按请求 ID 追溯。
可解释性 = 专家系统 vs 黑盒 LLM 的差异化护城河。
"""
from __future__ import annotations
import threading
from collections import deque
from typing import Any, Deque, Dict, Optional
class TraceStore:
"""请求推理链轨迹存储(线程安全,环形淘汰)。"""
def __init__(self, max_entries: int = 1000):
self._max = max_entries
self._entries: Dict[str, Dict[str, Any]] = {}
self._order: Deque[str] = deque(maxlen=max_entries)
self._lock = threading.Lock()
def put(self, request_id: str, trace: Dict[str, Any]) -> None:
with self._lock:
if request_id in self._entries:
self._entries[request_id] = trace
return
if len(self._entries) >= self._max:
# 环形淘汰最旧
while self._order:
oldest = self._order.popleft()
if oldest in self._entries:
del self._entries[oldest]
break
self._entries[request_id] = trace
self._order.append(request_id)
def get(self, request_id: str) -> Optional[Dict[str, Any]]:
with self._lock:
return self._entries.get(request_id)
def size(self) -> int:
with self._lock:
return len(self._entries)
def clear(self) -> None:
with self._lock:
self._entries.clear()
self._order.clear()
+1 -1
View File
@@ -1,4 +1,4 @@
"""交流文本(Workspace)—— 端云协同 LLM 协作系统的核心协议(零依赖)。
"""交流文本(Workspace)—— 端云协同编程智能体系统的核心协议(零依赖)。
大模型(Architect)与小模型(Worker)互不共享内部状态,只通过这份
schema 约束的结构化 JSON 共享工作区交接(类比前后端通过 API 契约协作)。