15 KiB
| title | status | created | updated | origin |
|---|---|---|---|---|
| feat: 路由智能化优化 — 复杂度校准、意图消歧、质量门控增强 | active | 2026-06-15 | 2026-06-15 | test-results/e2e/capability_report.txt (真实LLM回测分析报告) |
Summary
基于真实 LLM 回测分析报告暴露的三个核心根因,优化 CostAwareRouter 的路由智能化水平:修复 HeuristicClassifier 复杂度评分偏差(执行模式准确率从 9.09% 提升至 >30%),解决 IntentRouter 首次匹配导致的技能混淆(技能路由 F1 从 66.67% 提升至 >80%),增强 QualityGate 的技能匹配验证拦截错误路由。
当前进度: U1 代码已实现,待补单元测试;U2/U3 待实现;U4 待验证。
Problem Frame
真实 LLM 回测(74个观测)揭示三个核心问题:
- 执行模式准确率 9.09% — HeuristicClassifier 倾向高估复杂度,将简单问答(如"你好"、"你是谁")判为需要 REACT 而非 DIRECT_CHAT。40个执行模式判断错误中仅1次低估复杂度。
- keyword_match 召回率 0% — 62个关键词匹配用例全部未路由到预期技能,真实 SkillRegistry 虽然加载了15个技能,但路由链路未能正确匹配。
- 意图歧义 — plan_exec_agent 与 goal_driven_agent 的关键词重叠("规划"、"报告"子串),IntentRouter 首次匹配策略导致混淆。
Requirements
- R1: HeuristicClassifier 复杂度评分校准 — 简单问答应得低分(<0.3),复杂任务应得高分(>0.7)
- R2: IntentRouter 多候选评分排序 — 匹配多个技能时按得分排序选择最佳,而非首次匹配
- R3: QualityGate 技能匹配验证 — 拦截路由结果与技能能力不一致的输出
- R4: 回测验证 — 改进后执行模式准确率 >30%,技能路由 F1 >80%
Key Technical Decisions
KTD1: HeuristicClassifier 评分重构 — 增加低复杂度信号
决策: 在现有高/中复杂度关键词之外,增加低复杂度关键词列表和否定信号机制。当输入包含低复杂度信号(问候、闲聊、简单定义)时,直接降低基础分数;当高复杂度词出现在否定上下文("不要X"、"无需X")时,不增加分数。
理由: 当前分类器只有正向累加逻辑(命中高复杂度词→加分),没有负向扣减逻辑。这导致任何包含"分析"、"搜索"等常见动词的输入都被判为高复杂度,即使实际是简单问答。
替代方案: 用 LLM 替代规则分类器 — 延迟高(~500ms)、成本高(~100 tokens),且当前 merged_llm_classify 已在 0.3-0.7 区间使用 LLM,规则层应保持零成本。
实现状态: 代码已完成。classify() 方法已重写,包含低复杂度信号优先检测、否定上下文排除、阈值调整(0.15→0.10, 0.45→0.35)、短疑问句扣减。
KTD2: IntentRouter 多候选评分排序
决策: 修改 _match_keywords() 从"首次匹配返回"改为"收集所有匹配候选,按匹配关键词数量×关键词长度排序,返回最佳匹配"。
理由: 首次匹配依赖 skills 列表遍历顺序,不可控且不公平。多候选评分让匹配更多、更精确关键词的技能胜出。例如输入"规划一个调研报告"同时匹配 plan_exec_agent("规划"、"报告")和 goal_driven_agent("规划"、"调研"),但 goal_driven_agent 还匹配"生成报告"的子串"报告",匹配数相同则按关键词长度排序,更长的关键词("调研报告" > "报告")权重更高。
替代方案: 在技能配置中添加互斥关键词 — 需要逐对配置,维护成本高,且无法覆盖所有重叠场景。
实现状态: 待实现。当前 _match_keywords() 仍为首次匹配逻辑(intent.py L89-98)。
KTD3: QualityGate 技能匹配验证 — 轻量级路由一致性检查
决策: 在 QualityGate.validate() 中增加可选的 skill_context 参数,当提供时检查输出内容是否与路由到的技能的能力范围一致。使用规则检查(关键词覆盖度)而非 LLM 语义检查,保持零额外成本。
理由: 当前 QualityGate 只检查输出格式(必填字段、字数、Schema),不检查输出内容是否与路由技能匹配。3个用例虽然 HTTP 成功但路由到了错误技能,质量门控未能拦截。
实现状态: 待实现。当前 validate() 仅有四维度检查(gate.py L37-114)。
Scope Boundaries
In Scope
- HeuristicClassifier 评分逻辑优化(代码已完成,待补测试)
- IntentRouter._match_keywords() 多候选评分排序
- QualityGate 增加技能匹配验证维度
- 更新回测基准数据集以反映新的评分逻辑
- 改进后重跑回测验证
Out of Scope
- LLM 分类器优化(merged_llm_classify 和 _classify_with_llm 已有实现,不在本次优化范围)
- SemanticRouter 优化(需要嵌入模型,属于独立优化方向)
- ExpertTeamRouter 在服务器启动时的注入(已实现但未接入 create_app,属于部署配置问题)
- 新增技能配置文件
Deferred to Follow-Up Work
- 训练专用意图分类模型替代规则匹配(长期方向)
- 构建复杂度校准数据集持续优化阈值
- 实现自动质量回归检测 CI 流水线
Implementation Units
U1. HeuristicClassifier 复杂度评分校准
Goal: 修复复杂度评分偏差,使简单问答得低分、复杂任务得高分,提升执行模式准确率
Requirements: R1, R4
Dependencies: None
Files:
src/agentkit/chat/skill_routing.py— HeuristicClassifier 类(代码已完成)tests/unit/chat/test_skill_routing.py— 新增复杂度校准测试(待编写)
Approach:
代码已实现以下改动:
-
增加低复杂度关键词列表
_LOW_COMPLEXITY_HINTS_CN(17个词)和_LOW_COMPLEXITY_HINTS_EN(14个词),命中时基础分数为 0.05,且不再累加高复杂度词分数。 -
增加否定上下文检测
_NEGATION_PATTERNS,匹配"不要/无需/不用/don't/no need/without"后跟的词,该词不计入高复杂度匹配。 -
调整基础分数阈值:无关键词命中时基础分 0.10(原 0.15),中等复杂度命中基础分 0.35(原 0.45)。
-
增加短疑问句检测
_SHORT_QUESTION_RE:以"?"或"?"结尾且长度 <30 字符时,额外 -0.10。
剩余工作: 编写单元测试验证分类器行为。
Patterns to follow: 现有 test_skill_routing.py 中的测试类结构(TestExpertTeamRouterCanHandle 等)
Test scenarios:
-
低复杂度信号优先检测
- "你好" → 复杂度 < 0.3(命中
_LOW_COMPLEXITY_HINTS_CN) - "Hello" → 复杂度 < 0.3(命中
_LOW_COMPLEXITY_HINTS_EN) - "嗨,早上好" → 复杂度 < 0.3(多个低复杂度词命中)
- "你好,请帮我分析一下这个数据" → 复杂度 < 0.15(低复杂度信号优先,不累加高复杂度词)
- "你好" → 复杂度 < 0.3(命中
-
身份查询
- "你是谁" → 复杂度 < 0.3
- "你叫什么" → 复杂度 < 0.3
-
否定上下文排除
- "不要搜索" → "搜索"不计入高复杂度匹配,复杂度 < 0.3
- "无需分析,直接告诉我答案" → "分析"被否定,复杂度 < 0.3
- "分析市场趋势,但不要搜索" → "搜索"被否定但"分析"未被否定,复杂度 > 0.5
-
阈值调整验证
- 无关键词的短消息("好的")→ 复杂度 ≤ 0.10
- 含中等复杂度词("如何使用Python?")→ 基础分 0.35 而非 0.45
-
短疑问句扣减
- "怎么用?" → 复杂度 < 0.3(短疑问句 -0.10)
- "如何设计一个高可用的微服务架构?" → 复杂度 > 0.5(长疑问句不扣减)
-
复杂任务高分
- "分析市场趋势并生成报告" → 复杂度 > 0.7(2个高复杂度词命中)
- "执行部署脚本并重启服务" → 复杂度 > 0.7
-
边界条件
- 空字符串 → 复杂度 0.0
- 纯空格 → 复杂度 0.0
- 超长低复杂度消息(>200字符的问候)→ 复杂度 ≤ 0.10
Verification: pytest tests/unit/chat/test_skill_routing.py -v,所有 HeuristicClassifier 测试通过
U2. IntentRouter 多候选评分排序
Goal: 解决首次匹配导致的技能混淆,使匹配更精确的技能胜出
Requirements: R2, R4
Dependencies: None
Files:
src/agentkit/router/intent.py— IntentRouter._match_keywords()tests/unit/router/test_intent.py— 新建多候选排序测试
Approach:
-
重写
_match_keywords()方法(当前为intent.pyL75-99):当前逻辑(首次匹配):
for skill in skills: for keyword in keywords: if keyword in combined_text: return RoutingResult(matched_skill=skill.name, ...) return None改为多候选评分:
candidates = [] for skill in skills: matched_kws = [kw for kw in skill.config.intent.keywords if kw.lower() in combined_text] if matched_kws: score = sum(len(kw) for kw in matched_kws) # 更长关键词权重更高 candidates.append((skill, matched_kws, score)) if not candidates: return None candidates.sort(key=lambda c: (-c[2], c[0].name)) # 得分降序,同名字母序 best_skill, best_kws, best_score = candidates[0] confidence = min(1.0, 0.5 + 0.1 * len(best_kws)) return RoutingResult(matched_skill=best_skill.name, method="keyword", confidence=confidence) -
保持
RoutingResult数据类接口不变,method仍为"keyword"。 -
向后兼容:单候选时行为与原来一致(只有一个 skill 匹配时,排序无影响)。
-
需要创建
tests/unit/router/目录和__init__.py。
Patterns to follow: 现有 RoutingResult 数据类结构;_extract_string_values() 的输入处理方式
Test scenarios:
- 单候选匹配 — 输入只匹配一个 skill 的关键词,行为与原来一致,confidence=1.0
- 多候选匹配 — 得分不同 — 输入同时匹配 skill_a(关键词"规划"2字)和 skill_b(关键词"调研报告"4字),skill_b 得分更高应胜出
- 多候选匹配 — 得分相同 — 两个 skill 得分相同时,按名称字母序稳定排序
- 无匹配 — 无任何关键词命中,返回 None
- 空关键词列表 — skill 的 intent.keywords 为空列表,不参与匹配
- 大小写不敏感 — 英文关键词 "Search" 应匹配 "search"
- 子串匹配行为 — 中文关键词"报告"应匹配包含"报告"的输入(保持现有子串匹配语义)
- confidence 计算 — 匹配1个关键词 confidence=0.6,匹配3个 confidence=0.8,上限 1.0
Verification: pytest tests/unit/router/test_intent.py -v,多候选排序测试通过
U3. QualityGate 技能匹配验证
Goal: 增加路由一致性检查,拦截技能匹配错误的低质量输出
Requirements: R3, R4
Dependencies: None
Files:
src/agentkit/quality/gate.py— QualityGate.validate()tests/unit/quality/test_gate.py— 新建技能匹配验证测试
Approach:
-
在
QualityGate.validate()签名中增加可选参数skill_context: dict | None = None:async def validate( self, output: dict[str, Any], skill: Skill, skill_context: dict | None = None, # 新增 ) -> QualityResult: -
skill_context结构:{"skill_name": str, "intent_keywords": list[str]} -
当
skill_context提供且intent_keywords非空时,增加第五维度检查"技能匹配验证":- 将 output 中所有字符串值拼接
- 检查拼接文本是否包含至少一个
intent_keywords中的关键词(子串匹配) - 如果 0 个关键词匹配 →
QualityCheck(name="skill_match", passed=True, message="Warning: output may not match routed skill")— 警告但不拦截 - 如果 ≥ 1 个关键词匹配 →
QualityCheck(name="skill_match", passed=True)— 静默通过
-
警告升级为失败的组合逻辑:当
skill_match警告存在且其他任何维度检查失败时,skill_match的passed也变为False,导致整体passed=False。 -
保持向后兼容:
skill_context为 None 或缺少intent_keywords时,行为与原来完全一致(四维度检查)。
Patterns to follow: 现有四维度检查模式(gate.py L50-114);QualityCheck 数据类
Test scenarios:
- 无 skill_context — 行为与原来一致,仅四维度检查
- skill_context=None — 等同于无 skill_context
- skill_context 缺少 intent_keywords — 等同于无 skill_context
- 有 skill_context 且输出包含关键词 — 通过,无警告消息
- 有 skill_context 且输出不包含任何关键词 — 通过但有警告消息
- 输出无关 + 其他维度失败 — skill_match passed=False,整体 passed=False
- 输出无关 + 其他维度全部通过 — skill_match passed=True(仅警告),整体 passed=True
- 空 intent_keywords 列表 — 跳过技能匹配检查
Verification: pytest tests/unit/quality/test_gate.py -v,技能匹配验证测试通过
U4. 回测验证与基准更新
Goal: 验证改进效果,更新基准数据集
Requirements: R4
Dependencies: U1, U2, U3
Files:
tests/e2e/test_capability_router_direct.py— 使用真实 LLM 回测tests/e2e/benchmark_dataset.py— 可能需要更新预期值test-results/e2e/capability_report.txt— 对比改进前后报告
Approach:
-
运行完整回测:
python3 -m pytest tests/e2e/test_capability_router_direct.py -v -
对比改进前后指标:
- 执行模式准确率:9.09% → 目标 >30%
- 技能路由 F1:66.67% → 目标 >80%
- 任务成功率:100% → 保持
-
如果基准数据集中的预期值因评分逻辑变化需要调整,更新
benchmark_dataset.py -
保存改进后报告为基线:
cp test-results/e2e/capability_report.json test-results/e2e/baseline_capability_report.json
Test scenarios:
- 回测全部通过
- 执行模式准确率 >30%
- 技能路由 F1 >80%
- 无回归(任务成功率不下降)
Verification: 运行回测并检查报告指标
Risks & Dependencies
| 风险 | 影响 | 缓解 |
|---|---|---|
| 复杂度评分调整可能过度修正,导致复杂任务被判为简单 | 高复杂度任务路由到 DIRECT_CHAT,无法使用工具 | 保留 merged_llm_classify 兜底机制,0.3-0.7 区间仍由 LLM 二次确认 |
| 多候选排序可能改变现有路由行为的兼容性 | 已有用户依赖的路由结果可能变化 | 排序逻辑仅在多候选时生效,单候选行为不变 |
| QualityGate 技能匹配验证的"相关词"判断可能误报 | 正常输出被标记为警告 | 使用 warning 级别而非 error,不单独拦截 |
| keyword_match 召回率 0% 的根因可能不仅是 IntentRouter | 即使修复多候选排序,仍可能因技能配置关键词不匹配而召回率低 | U4 回测后若仍低,需进一步分析技能配置与基准用例的对齐度 |
Open Questions
- 复杂度评分的具体阈值已在代码中设定初始值(0.05/0.10/0.35/0.65/0.80),需通过 U4 回测校准
- 否定上下文检测的正则模式覆盖度需在回测中验证,可能需要迭代补充
- keyword_match 召回率 0% 是否完全由 IntentRouter 首次匹配导致,还是技能配置关键词本身与基准用例不对齐 — 需 U2 实现后通过 U4 验证