fischer-agentkit/docs/plans/2026-06-15-003-feat-router-...

328 lines
16 KiB
Markdown
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.

---
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.72个高复杂度词命中
- "执行部署脚本并重启服务" → 复杂度 > 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%
- 技能路由 F166.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 验证