feat(v1): 架构与算法优化二轮——共享 LLM 客户端去重、语义缓存 O(1) 提升/淘汰、分类器热路径清理

- 新增 router_system/llm_client.py:OpenAICompatClient 统一 experts/judge/fallback
  三处复制的懒建 AsyncClient + /chat/completions + choices/usage 解析(~60 行去重);
  密钥解析统一走 config.get_api_key(激活原死代码,顺带消除 experts 默认环境名不一致)
- 语义缓存 L2:条目容器 list→OrderedDict(提升/淘汰 O(n)→O(1)),按 query 天然去重;
  n-gram 向量 lru_cache 复用(同一次 miss 的 get/put 免重复分词);
  A/B:淘汰路径 0.040→0.034s,miss→put 往返 9.41→8.57s(-9%)
- RuleJudge 覆盖度:response.lower() 提出逐词循环(原 O(terms×len) 重复复制)
- extract_content_terms 纯函数 lru_cache 化(专家与 Judge 对同一查询免重复分词),返回 tuple
- RuleClassifier:_score 去掉败者领域白建的命中词 list(胜出后单独收集);
  修复 code 规则 ("api",0.7) 重复登记(原命中计 1.4 分)
- difficulty:正则模块级预编译
- tests:恢复上一轮引入的乱码中文 docstring;网关测试输入串恢复为可判 code 的中文查询
This commit is contained in:
tzt
2026-09-18 23:27:26 +08:00
parent 81b0bc9b9b
commit c072a1a237
10 changed files with 696 additions and 631 deletions
+56 -52
View File
@@ -1,52 +1,56 @@
"""查询难度估计器(启发式,纯标准库)。
依据:RouterArena 用 Bloom 分类法把问题分为 easy/medium/hard。
这里用查询长度、指令动词、数学/推理标记做轻量估计。
"""
from __future__ import annotations
import re
from typing import Tuple
# 触发 hard 的指令动词 / 推理标记
_HARD_MARKERS = [
"证明", "推导", "为什么", "如何", "对比", "比较", "分析", "评估", "设计", "优化",
"复杂度", "时间复杂度", "空间复杂度", "原理", "机制", "优缺点", "区别", "推论", "定理",
"proof", "prove", "derive", "explain why", "why", "how", "compare", "contrast",
"analy", "evaluate", "design", "optimize", "refactor", "architect",
"implement", "debug", "review", "plan", "synthesize",
"int", "sum", "sqrt", "lim", "log", "derivative", "integral",
]
# 触发 medium 的标记
_MEDIUM_MARKERS = [
"", "", "计算", "求解", "生成", "翻译", "总结", "解释",
"注意", "建议", "是否", "实现", "步骤",
"write", "code", "function", "script", "calculate", "solve", "summarize",
"translate", "fix", "explain", "describe", "list",
]
def estimate_difficulty(query: str) -> Tuple[str, float]:
"""返回 (difficulty, score)score 属于 [0,1]。"""
q = query.lower()
hard_hits = sum(1 for m in _HARD_MARKERS if m in q)
medium_hits = sum(1 for m in _MEDIUM_MARKERS if m in q)
length = len(query)
score = 0.0
score += min(0.30, length / 600.0) # 长度贡献
score += min(0.55, hard_hits * 0.25) # 推理标记贡献
score += min(0.30, medium_hits * 0.08) # 一般指令贡献
# 额外:代码 / 数学表达式(多步骤信号)
if "```" in query or re.search(r"\b(def|class|function|import)\b", q):
score += 0.15
if re.search(r"[0-9]+\s*[+\-*/^=]\s*[0-9xya-z]", q):
score += 0.15
score = max(0.0, min(1.0, score))
if score >= 0.55:
return "hard", score
if score >= 0.25:
return "medium", score
return "easy", score
"""查询难度估计器(启发式,纯标准库)。
依据:RouterArena 用 Bloom 分类法把问题分为 easy/medium/hard。
这里用查询长度、指令动词、数学/推理标记做轻量估计。
"""
from __future__ import annotations
import re
from typing import Tuple
# 触发 hard 的指令动词 / 推理标记
_HARD_MARKERS = [
"证明", "推导", "为什么", "如何", "对比", "比较", "分析", "评估", "设计", "优化",
"复杂度", "时间复杂度", "空间复杂度", "原理", "机制", "优缺点", "区别", "推论", "定理",
"proof", "prove", "derive", "explain why", "why", "how", "compare", "contrast",
"analy", "evaluate", "design", "optimize", "refactor", "architect",
"implement", "debug", "review", "plan", "synthesize",
"int", "sum", "sqrt", "lim", "log", "derivative", "integral",
]
# 触发 medium 的标记
_MEDIUM_MARKERS = [
"", "", "计算", "求解", "生成", "翻译", "总结", "解释",
"注意", "建议", "是否", "实现", "步骤",
"write", "code", "function", "script", "calculate", "solve", "summarize",
"translate", "fix", "explain", "describe", "list",
]
# 预编译正则(模块级一次,避免每次调用走 re 内部缓存查找)
_RE_CODE_EXPR = re.compile(r"\b(def|class|function|import)\b")
_RE_ARITH_EXPR = re.compile(r"[0-9]+\s*[+\-*/^=]\s*[0-9xya-z]")
def estimate_difficulty(query: str) -> Tuple[str, float]:
"""返回 (difficulty, score)score 属于 [0,1]。"""
q = query.lower()
hard_hits = sum(1 for m in _HARD_MARKERS if m in q)
medium_hits = sum(1 for m in _MEDIUM_MARKERS if m in q)
length = len(query)
score = 0.0
score += min(0.30, length / 600.0) # 长度贡献
score += min(0.55, hard_hits * 0.25) # 推理标记贡献
score += min(0.30, medium_hits * 0.08) # 一般指令贡献
# 额外:代码 / 数学表达式(多步骤信号)
if "```" in query or _RE_CODE_EXPR.search(q):
score += 0.15
if _RE_ARITH_EXPR.search(q):
score += 0.15
score = max(0.0, min(1.0, score))
if score >= 0.55:
return "hard", score
if score >= 0.25:
return "medium", score
return "easy", score