328 lines
16 KiB
Markdown
328 lines
16 KiB
Markdown
---
|
||
title: "feat: 路由智能化优化 — 复杂度校准、意图消歧、质量门控增强"
|
||
status: superseded
|
||
superseded_by: "2026-06-16-005-refactor-routing-architecture-plan"
|
||
superseded_reason: "SimpleRouter 已替代 CostAwareRouter 的 4 层路由架构。IntentRouter 多候选评分(U2)和 QualityGate 技能匹配验证(U3)属于被删除的旧路由层组件,不再需要实现。U1 HeuristicClassifier 测试仅对向后兼容有价值。"
|
||
closed: 2026-06-16
|
||
origin: 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个观测)揭示三个核心问题:
|
||
|
||
1. **执行模式准确率 9.09%** — HeuristicClassifier 倾向高估复杂度,将简单问答(如"你好"、"你是谁")判为需要 REACT 而非 DIRECT_CHAT。40个执行模式判断错误中仅1次低估复杂度。
|
||
2. **keyword_match 召回率 0%** — 62个关键词匹配用例全部未路由到预期技能,真实 SkillRegistry 虽然加载了15个技能,但路由链路未能正确匹配。
|
||
3. **意图歧义** — 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:**
|
||
|
||
代码已实现以下改动:
|
||
|
||
1. 增加低复杂度关键词列表 `_LOW_COMPLEXITY_HINTS_CN`(17个词)和 `_LOW_COMPLEXITY_HINTS_EN`(14个词),命中时基础分数为 0.05,且不再累加高复杂度词分数。
|
||
|
||
2. 增加否定上下文检测 `_NEGATION_PATTERNS`,匹配"不要/无需/不用/don't/no need/without"后跟的词,该词不计入高复杂度匹配。
|
||
|
||
3. 调整基础分数阈值:无关键词命中时基础分 0.10(原 0.15),中等复杂度命中基础分 0.35(原 0.45)。
|
||
|
||
4. 增加短疑问句检测 `_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.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:**
|
||
|
||
1. 重写 `_match_keywords()` 方法(当前为 `intent.py` L75-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)
|
||
```
|
||
|
||
2. 保持 `RoutingResult` 数据类接口不变,`method` 仍为 `"keyword"`。
|
||
|
||
3. 向后兼容:单候选时行为与原来一致(只有一个 skill 匹配时,排序无影响)。
|
||
|
||
4. 需要创建 `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:**
|
||
|
||
1. 在 `QualityGate.validate()` 签名中增加可选参数 `skill_context: dict | None = None`:
|
||
```python
|
||
async def validate(
|
||
self,
|
||
output: dict[str, Any],
|
||
skill: Skill,
|
||
skill_context: dict | None = None, # 新增
|
||
) -> QualityResult:
|
||
```
|
||
|
||
2. `skill_context` 结构:`{"skill_name": str, "intent_keywords": list[str]}`
|
||
|
||
3. 当 `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)` — 静默通过
|
||
|
||
4. 警告升级为失败的组合逻辑:当 `skill_match` 警告存在且其他任何维度检查失败时,`skill_match` 的 `passed` 也变为 `False`,导致整体 `passed=False`。
|
||
|
||
5. 保持向后兼容:`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:**
|
||
|
||
1. 运行完整回测:`python3 -m pytest tests/e2e/test_capability_router_direct.py -v`
|
||
|
||
2. 对比改进前后指标:
|
||
- 执行模式准确率:9.09% → 目标 >30%
|
||
- 技能路由 F1:66.67% → 目标 >80%
|
||
- 任务成功率:100% → 保持
|
||
|
||
3. 如果基准数据集中的预期值因评分逻辑变化需要调整,更新 `benchmark_dataset.py`
|
||
|
||
4. 保存改进后报告为基线:`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 验证
|