Files
projectAIpopular/router_system/classifier.py
T
tzt b2fa8c3c81 feat(v2): 架构与算法优化——语义缓存 2.37x、拓扑排序 O(V+E)、分类器确定性决胜
算法:
- RouterCache:语义条目写入时预计算向量范数、语义查找单遍完成(消除命中后二次 O(N) 查找)、
  相似度=1.0 提前终止;微基准(3000 条目×200 查询):3986ms -> 1685ms,2.37x
- TaskGraph.topo_order:O(V²logV) 重排序/成员扫描 -> 邻接表+deque 的 O(V+E) Kahn,
  输出顺序契约不变(初始就绪层按插入序、循环依赖按插入序兜底、未知依赖忽略)
- RuleClassifier:同分决胜按领域名字典序(与规则表排列无关),次高分 O(n) 扫描

工程卫生:
- .mimosa/(扫描器工作目录)加入 .gitignore 并移出索引
- test_review 抽样测试改用内联确定性 LCG,消除 2 个低危(不安全随机数)

测试:新增 11 项(topo 契约 6 + 缓存回归 3 + 分类器 2)
pytest 230 passed(基线 219 全绿 + 11)
2026-09-18 08:35:36 +08:00

264 lines
13 KiB
Python
Raw 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.
"""意图分类器:识别查询领域(code/math/legal/medical/general)与难度。
- RuleClassifier:关键词/正则规则打分,纯标准库,零依赖,可离线运行。
- HuggingFaceClassifier:可选,基于 transformers 的分类模型(需安装 ML 依赖)。
置信度设计:每个领域有一组 (关键词, 权重)。命中权重求和得原始分 s,
confidence = 1 - exp(-s),保证 s=1 -> 0.63s=2 -> 0.86s=3 -> 0.95。
无领域命中(或最高分领域为 general)时置信度低,触发 should_fallback。
"""
from __future__ import annotations
import math
from typing import Dict, List, Optional, Tuple
from .difficulty import estimate_difficulty
from .models import Classification
# ---------------------------------------------------------------
# 领域关键词规则: (关键词, 权重)
# ---------------------------------------------------------------
DOMAIN_RULES: Dict[str, List[Tuple[str, float]]] = {
"code": [
# 中文
("python", 1.2), ("java", 1.2), ("javascript", 1.2), ("typescript", 1.2),
("代码", 1.2), ("编程", 1.2), ("函数", 0.9), ("接口", 0.8), ("报错", 0.9),
("调试", 0.9), ("部署", 0.8), ("算法", 0.8), ("数组", 0.8), ("排序", 0.9),
("正则", 0.8), ("数据库", 0.7), ("sql", 0.8), ("git", 0.7), ("api", 0.7),
("变量", 0.7), ("循环", 0.7), ("递归", 0.8), ("重构", 0.8), ("编译", 0.9),
("测试", 0.6), ("前端", 0.8), ("后端", 0.8), ("爬虫", 0.8), ("脚本", 0.7),
# 英文
("function", 0.9), ("class", 0.8), ("bug", 0.9), ("debug", 0.9),
("compile", 0.9), ("error", 0.6), ("code", 0.7), ("script", 0.7),
("algorithm", 0.8), ("sort", 0.7), ("array", 0.7), ("regex", 0.8),
("import", 0.7), ("loop", 0.7), ("recursion", 0.8), ("refactor", 0.8),
("deploy", 0.8), ("docker", 0.8), ("kubernetes", 0.8),
("async", 0.7), ("flask", 0.7), ("django", 0.7), ("api", 0.7),
("索引", 0.8), ("优化", 0.7), ("查询", 0.6),
],
"math": [
("数学", 1.2), ("方程", 1.0), ("求解", 0.8), ("导数", 1.0), ("积分", 1.0),
("矩阵", 0.9), ("概率", 0.9), ("统计", 0.8), ("证明", 0.8), ("定理", 0.9),
("微积分", 1.1), ("代数", 0.9), ("几何", 0.9), ("不等式", 0.9),
("equation", 1.0), ("derivative", 1.0), ("integral", 1.0), ("calculus", 1.1),
("matrix", 0.9), ("probability", 0.9), ("statistics", 0.8), ("proof", 0.8),
("theorem", 0.9), ("algebra", 0.9), ("geometry", 0.9), ("sqrt", 0.8),
("gcd", 0.8), ("lim", 0.8), ("polynomial", 0.9), ("summation", 0.7),
("math", 0.7), ("解", 0.6), ("计算", 0.8), ("等于", 0.6), ("求值", 0.7), ("函数", 0.6),
],
"legal": [
("法律", 1.2), ("合同", 1.0), ("法条", 1.0), ("合规", 1.0), ("诉讼", 1.0),
("知识产权", 1.1), ("版权", 0.9), ("专利", 0.9), ("违约", 0.9), ("赔偿", 0.8),
("仲裁", 0.9), ("劳动法", 1.0), ("刑法", 1.0), ("民法典", 1.0),
("法规", 0.8), ("条款", 0.7), ("律师", 0.8), ("起诉", 0.9), ("判决", 0.9),
# 劳动法
("加班", 0.9), ("加班费", 1.0), ("工资", 0.8), ("辞退", 0.9), ("裁员", 0.9),
("试用期", 0.9), ("社保", 0.8), ("公积金", 0.8), ("年假", 0.9), ("离职", 0.8),
("解除劳动合同", 1.1), ("经济补偿", 1.0), ("竞业", 1.0),
# 房产/婚姻/消费者
("租房", 0.9), ("买房", 0.9), ("购房", 0.9), ("押金", 0.8), ("房贷", 0.9),
("离婚", 1.0), ("继承", 0.9), ("遗产", 0.9), ("抚养权", 0.9), ("遗嘱", 0.9),
("退款", 0.9), ("退货", 0.8), ("消费者", 0.8), ("七天无理由", 1.0), ("维权", 0.8),
("law", 1.0), ("legal", 1.1), ("contract", 1.0), ("compliance", 1.0),
("litigation", 1.0), ("copyright", 0.9), ("patent", 0.9), ("trademark", 0.9),
("liability", 0.9), ("regulatory", 0.8), ("jurisdiction", 0.9),
("clause", 0.8), ("agreement", 0.7), ("申请", 0.6),
],
"medical": [
("医疗", 1.2), ("药物", 1.0), ("症状", 1.0), ("诊断", 1.0), ("治疗", 0.9),
("医生", 0.9), ("血压", 0.9), ("高血压", 1.0), ("糖尿病", 1.0), ("感冒", 0.9),
("剂量", 0.9), ("副作用", 0.9), ("手术", 0.9), ("患者", 0.9),
("吃药", 0.9), ("发烧", 1.0), ("疫苗", 0.9), ("感染", 0.9), ("体检", 0.7),
# 急救/消化/心理/营养/儿科
("烫伤", 1.0), ("烧伤", 1.0), ("止血", 0.9), ("扭伤", 0.9), ("中暑", 1.0),
("急救", 0.9), ("腹泻", 0.9), ("拉肚子", 0.9), ("便秘", 0.9), ("胃", 0.7),
("失眠", 0.9), ("焦虑", 0.9), ("抑郁", 0.9), ("压力", 0.6), ("睡眠", 0.7),
("减肥", 0.8), ("营养", 0.7), ("卡路里", 0.9), ("儿童", 0.8), ("婴儿", 0.9),
("宝宝", 0.8), ("抗生素", 0.9), ("止咳", 0.9),
("medical", 1.0), ("patient", 0.9), ("symptom", 1.0), ("disease", 0.9),
("diagnosis", 1.0), ("treatment", 0.8), ("prescription", 1.0),
("dosage", 0.9), ("side effect", 0.9), ("hypertension", 1.0),
("diabetes", 1.0), ("surgery", 0.8), ("clinic", 0.7), ("vaccine", 0.9),
("infection", 0.9), ("first aid", 0.9), ("insomnia", 0.9),
],
"general": [
("总结", 0.4), ("翻译", 0.4), ("介绍", 0.4), ("解释", 0.3),
("summarize", 0.4), ("translate", 0.4), ("explain", 0.3),
("introduce", 0.3), ("what is", 0.3), ("tell me", 0.3),
("write an essay", 0.4), ("邮件", 0.4), ("email", 0.3),
("推荐", 0.3), ("评价", 0.3),
],
"finance": [
("理财", 1.0), ("投资", 1.0), ("基金", 1.0), ("股票", 1.0), ("债券", 0.9),
("存款", 0.9), ("储蓄", 0.8), ("利率", 0.8), ("利息", 0.8), ("贷款", 1.0),
("房贷", 1.0), ("月供", 0.9), ("保险", 0.9), ("理赔", 0.9), ("保费", 0.8),
("信用卡", 1.0), ("征信", 0.9), ("逾期", 0.9), ("分期", 0.8), ("记账", 0.7),
("预算", 0.7), ("理财规划", 1.0), ("收益率", 0.9), ("定投", 0.9),
("invest", 0.8), ("fund", 0.8), ("stock", 0.9), ("loan", 0.9),
("mortgage", 0.9), ("insurance", 0.9), ("credit card", 0.9),
("finance", 0.8), ("money", 0.6), ("lpr", 0.9), ("投资理财", 1.1),
],
"life": [
("菜谱", 0.9), ("做饭", 0.8), ("烹饪", 0.9), ("美食", 0.8), ("做法", 0.7),
("旅行", 0.9), ("旅游", 0.9), ("攻略", 0.8), ("机票", 0.8), ("酒店", 0.7),
("签证", 0.9), ("景点", 0.8), ("自驾", 0.8),
("装修", 0.9), ("收纳", 0.8), ("家居", 0.7), ("清洁", 0.7), ("打扫", 0.7),
("宠物", 0.9), ("猫", 0.7), ("狗", 0.7), ("猫粮", 0.9), ("驱虫", 0.9),
("健身", 0.9), ("锻炼", 0.8), ("跑步", 0.8), ("增肌", 0.9), ("减脂", 0.9),
("瑜伽", 0.8), ("天气", 0.7), ("气温", 0.7),
("recipe", 0.8), ("travel", 0.9), ("trip", 0.8), ("pet", 0.8),
("workout", 0.9), ("gym", 0.8), ("weather", 0.7), ("cook", 0.8),
],
"education": [
("学习方法", 1.0), ("怎么学", 0.7), ("高效学习", 1.0), ("记忆", 0.6), ("复习", 0.7),
("预习", 0.7), ("笔记", 0.6), ("专注", 0.6), ("拖延", 0.7), ("学习效率", 0.9),
("考试", 0.9), ("备考", 1.0), ("刷题", 0.9), ("模拟考", 0.9), ("中考", 0.9),
("高考", 0.9), ("考研", 0.9), ("考前", 0.7),
("英语", 0.8), ("单词", 0.7), ("口语", 0.8), ("听力", 0.7), ("雅思", 1.0),
("托福", 1.0), ("四级", 0.9), ("六级", 0.9), ("背单词", 0.9),
("选课", 0.9), ("课程", 0.6), ("专业选择", 0.9), ("报班", 0.8), ("网课", 0.7),
("自学", 0.7), ("职业规划", 1.0), ("求职", 0.9), ("面试", 0.8), ("简历", 0.8),
("实习", 0.7), ("跳槽", 0.8), ("转行", 0.9),
("study", 0.8), ("exam", 0.9), ("language", 0.7), ("career", 0.8),
("interview", 0.8), ("education", 0.7), ("learn", 0.6),
],
}
_STOPWORDS = {
"的", "了", "吗", "呢", "啊", "是", "在", "有", "和", "与", "或", "及", "一个", "如何",
"the", "a", "an", "is", "are", "to", "of", "in", "on", "for", "with", "and",
"or", "do", "does", "can", "could", "would", "should", "please", "me", "my",
}
class BaseClassifier:
def classify(self, query: str) -> Classification:
raise NotImplementedError
def should_fallback(self, classification: Classification, threshold: float) -> bool:
return classification.confidence < threshold
class RuleClassifier(BaseClassifier):
"""基于关键词规则的分类器(零依赖)。
domains 参数(可选):限定只对部分领域打分 —— 两级路由中,
每个大领域的组内路由模型用 RuleClassifier(domains=组内领域)
只认识本组领域,体积与匹配开销约为统一分类器的 1/4。
"""
def __init__(self, confidence_floor: float = 0.55,
domains: Optional[List[str]] = None):
self.confidence_floor = confidence_floor
if domains is None:
self.rules = DOMAIN_RULES
else:
self.rules = {d: DOMAIN_RULES[d] for d in domains if d in DOMAIN_RULES}
self.domains = list(self.rules.keys())
def _score(self, query: str) -> Tuple[Dict[str, float], Dict[str, List[str]]]:
q = query.lower()
scores: Dict[str, float] = {}
matched: Dict[str, List[str]] = {}
for domain, rules in self.rules.items():
s = 0.0
hits = []
for kw, w in rules:
if kw in q:
s += w
hits.append(kw)
if s > 0:
scores[domain] = s
matched[domain] = hits
return scores, matched
def classify(self, query: str) -> Classification:
raw, matched = self._score(query)
if not raw:
# 完全无命中 -> general,低置信度
diff, ds = estimate_difficulty(query)
return Classification(
domain="general",
confidence=0.50,
difficulty=diff,
difficulty_score=ds,
raw_scores={},
matched_rules=[],
)
# 同分决胜:按领域名字典序,保证与规则表排列顺序无关的确定性
best_domain = max(sorted(raw), key=lambda d: raw[d])
best_score = raw[best_domain]
confidence = 1.0 - math.exp(-best_score)
# general 领域天然置信度压低
if best_domain == "general":
confidence = min(confidence, self.confidence_floor + 0.05)
# 与次高分的差距影响置信度(区分度)
if len(raw) > 1:
second = max(v for d, v in raw.items() if d != best_domain)
if second > 0.7 * best_score:
confidence *= 0.85
diff, ds = estimate_difficulty(query)
return Classification(
domain=best_domain,
confidence=round(min(0.99, confidence), 4),
difficulty=diff,
difficulty_score=ds,
raw_scores={k: round(v, 3) for k, v in raw.items()},
matched_rules=matched.get(best_domain, []),
)
class HuggingFaceClassifier(BaseClassifier):
"""可选:基于 transformers 的序列分类模型。
仅当安装 torch+transformers 且模型可加载时可用;否则抛错提示。
"""
def __init__(self, model_name: str, num_labels: int = 8, confidence_floor: float = 0.55):
try:
from transformers import AutoModelForSequenceClassification, AutoTokenizer
except ImportError as e:
raise RuntimeError(
"HuggingFaceClassifier 需要安装 ML 依赖:pip install -r requirements-ml.txt"
) from e
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForSequenceClassification.from_pretrained(
model_name, num_labels=num_labels
)
self.labels = ["code", "math", "legal", "medical", "general",
"finance", "life", "education"]
self.confidence_floor = confidence_floor
def classify(self, query: str) -> Classification:
import torch # type: ignore
inputs = self.tokenizer(query, return_tensors="pt", truncation=True, max_length=256)
with torch.no_grad():
logits = self.model(**inputs).logits
probs = torch.softmax(logits, dim=-1)[0]
idx = int(probs.argmax())
diff, ds = estimate_difficulty(query)
return Classification(
domain=self.labels[idx],
confidence=round(float(probs[idx]), 4),
difficulty=diff,
difficulty_score=ds,
raw_scores={self.labels[i]: round(float(probs[i]), 3) for i in range(len(self.labels))},
)
def build_classifier(cfg: Dict) -> BaseClassifier:
"""根据配置构建分类器。cfg 为 classifier 段配置。"""
ctype = cfg.get("type", "rule")
floor = cfg.get("confidence_floor", 0.55)
if ctype == "rule":
return RuleClassifier(confidence_floor=floor)
if ctype == "hf":
return HuggingFaceClassifier(cfg.get("model", "Qwen/Qwen3-0.6B"), confidence_floor=floor)
raise ValueError(f"未知分类器类型: {ctype}(支持 rule | hf")