321 lines
15 KiB
Markdown
321 lines
15 KiB
Markdown
---
|
||
title: "fix: 回测问题修复 + 路由优化 + 质量门控强化"
|
||
status: completed
|
||
created: 2026-06-20
|
||
type: fix
|
||
origin: test/full-regression-real-llm-e2e 回测结果
|
||
---
|
||
|
||
# fix: 回测问题修复 + 路由优化 + 质量门控强化
|
||
|
||
## Summary
|
||
|
||
修复全面回测中发现的 5 个代码问题,优化当前 RequestPreprocessor 路由准确率,强化 QualityGate 质量门控,并重新基准测试建立当前架构基线。
|
||
|
||
## Problem Frame
|
||
|
||
回测发现以下问题(基于 `test/full-regression-real-llm-e2e` 分支):
|
||
|
||
1. **Benchmark 超时过短** — `llm-001`(easy 难度)超时阈值 20s,真实 LLM(qwen3.7-plus)无法在 20s 内完成工具调用推理,导致 2/5 用例超时
|
||
2. **LLM Provider httpx 超时硬编码** — `OpenAICompatibleProvider` 的 httpx 客户端硬编码 `timeout=60.0`,忽略 `ProviderConfig.timeout`(120s)
|
||
3. **QualityGate skill_match 休眠** — `_check_skill_match()` 方法存在但无调用方传入 `skill_context`,质量门控形同虚设
|
||
4. **QualityGate 自定义验证器过于宽松** — 验证器导入/执行失败时静默跳过(`passed=True`),不拦截低质量输出
|
||
5. **16 个技能配置均无 disambiguation_keywords** — 易混淆技能对(reflexion_agent↔code_reviewer 等)无法消歧
|
||
6. **路由优化** — 当前 RequestPreprocessor 仅 3 条正则(问候/闲聊/身份),大量简单 factual 问题被送入 REACT 循环,浪费 token
|
||
|
||
## Requirements
|
||
|
||
- R1: Benchmark easy 难度超时从 20s 提升至 45s,medium 从 40s 提升至 60s
|
||
- R2: OpenAICompatibleProvider httpx 客户端使用 ProviderConfig.timeout 而非硬编码 60s
|
||
- R3: QualityGate skill_match 在执行管线中被实际调用(传入 skill_context)
|
||
- R4: QualityGate 自定义验证器失败时支持严格模式(可配置拦截 vs 警告)
|
||
- R5: 为 4 对易混淆技能添加 disambiguation_keywords 字段
|
||
- R6: RequestPreprocessor 新增 factual/数学/翻译类正则,减少不必要的 REACT 调用
|
||
- R7: 修复后重新运行 benchmark 建立当前架构基线
|
||
|
||
## Key Technical Decisions
|
||
|
||
### KTD1: Benchmark 超时按难度分级保留,但提升阈值
|
||
|
||
**决策**: 保留 `_LLM_TIMEOUT_BY_DIFFICULTY` 字典结构,提升 easy→45s、medium→60s、hard→90s。
|
||
|
||
**理由**: 分级超时是合理设计(简单任务不应等太久),但 20s 对真实 LLM 工具调用太短。qwen3.7-plus 的 p50 延迟 35s、p95 42s(来自 benchmark 报告),20s 必然超时。
|
||
|
||
### KTD2: httpx 超时从 ProviderConfig 透传,保留硬编码作为 fallback
|
||
|
||
**决策**: `OpenAICompatibleProvider.__init__` 读取 `config.timeout`,若未设置则 fallback 到 60s。
|
||
|
||
**理由**: ProviderConfig.timeout 默认 120s 是有意的(LLM 推理慢),httpx 硬编码 60s 会先于 ProviderConfig 触发,导致配置无效。
|
||
|
||
### KTD3: QualityGate skill_match 在 ConfigDrivenAgent 执行后调用
|
||
|
||
**决策**: 在 `ConfigDrivenAgent._execute_skill_task()` 返回前调用 `QualityGate.validate(output, skill_context=skill_config)`。
|
||
|
||
**理由**: skill_match 需要技能上下文(intent_keywords)才能校验输出一致性。ConfigDrivenAgent 是技能执行的统一入口,在此处调用覆盖面最广。
|
||
|
||
### KTD4: disambiguation_keywords 作为 QualityGate 消歧输入,不用于路由
|
||
|
||
**决策**: disambiguation_keywords 添加到 skill yaml 的 `intent` 节点下,由 QualityGate 读取用于输出校验,不影响 RequestPreprocessor 路由决策。
|
||
|
||
**理由**: 当前路由已简化为"显式前缀 + 正则 + 默认 REACT",不依赖关键词。disambiguation_keywords 的价值在于 QualityGate 校验输出是否与技能意图一致。
|
||
|
||
### KTD5: 路由优化采用"扩展正则 + 不引入 LLM 分类"策略
|
||
|
||
**决策**: 新增 factual(是什么/什么是/解释)、数学(计算/算一下)、翻译(翻译/translate)三类正则走 DIRECT_CHAT,不引入 LLM quick_classify。
|
||
|
||
**理由**: 保持 RequestPreprocessor 的"零 token 成本快速路径"设计哲学。LLM 二次分类已被明确移除(docstring: "LLM blind-classification without tool context is unreliable"),不回退。
|
||
|
||
## Scope Boundaries
|
||
|
||
### In Scope
|
||
|
||
- Benchmark 超时阈值调整
|
||
- OpenAICompatibleProvider httpx 超时修复
|
||
- QualityGate skill_match 激活 + 严格模式
|
||
- 4 对易混淆技能 disambiguation_keywords
|
||
- RequestPreprocessor 正则扩展
|
||
- 重新基准测试
|
||
|
||
### Deferred to Follow-Up Work
|
||
|
||
- DockerComputerUseSession 4 个 stub(需真实 Docker 环境)
|
||
- 计划 001(U7/U8/U9/U10 未完成项)
|
||
- 计划 002(8 个待决策问题)
|
||
- 计划 003(7 项 Deferred)
|
||
- LLM 二次分类消歧(P2,需评估延迟代价)
|
||
- 复杂度校准数据集构建(P2,需收集标注数据)
|
||
|
||
---
|
||
|
||
## Implementation Units
|
||
|
||
### U1. 修复 Benchmark 超时阈值
|
||
|
||
**Goal:** 提升 easy/medium/hard 难度的 LLM 超时阈值,避免真实 LLM 因超时失败
|
||
|
||
**Requirements:** R1
|
||
|
||
**Dependencies:** 无
|
||
|
||
**Files:**
|
||
- `src/agentkit/cli/benchmark.py` — 修改 `_LLM_TIMEOUT_BY_DIFFICULTY` 字典
|
||
|
||
**Approach:**
|
||
将 `_LLM_TIMEOUT_BY_DIFFICULTY` 从 `{"easy": 20.0, "medium": 40.0, "hard": 60.0}` 改为 `{"easy": 45.0, "medium": 60.0, "hard": 90.0}`。默认 fallback 从 30.0 改为 60.0。
|
||
|
||
**Patterns to follow:** 现有 `_LLM_TIMEOUT_BY_DIFFICULTY` 字典结构
|
||
|
||
**Test scenarios:**
|
||
- Happy path: easy 难度用例在 45s 内完成 → passed=True
|
||
- Edge case: easy 难度用例在 20-45s 之间完成 → 旧逻辑会超时,新逻辑 passed=True
|
||
- Error path: easy 难度用例超过 45s → 超时失败,detail 包含 "45s"
|
||
|
||
**Verification:** 运行 `agentkit benchmark --mode llm`,llm-001 不再因超时失败
|
||
|
||
---
|
||
|
||
### U2. 修复 OpenAICompatibleProvider httpx 超时硬编码
|
||
|
||
**Goal:** httpx 客户端使用 ProviderConfig.timeout 而非硬编码 60s
|
||
|
||
**Requirements:** R2
|
||
|
||
**Dependencies:** 无
|
||
|
||
**Files:**
|
||
- `src/agentkit/llm/providers/openai.py` — 修改 httpx.AsyncClient 构造
|
||
- `tests/unit/llm/test_openai_provider.py` — 新增超时透传测试
|
||
|
||
**Approach:**
|
||
在 `OpenAICompatibleProvider.__init__` 中,将 `httpx.AsyncClient(timeout=60.0)` 改为 `httpx.AsyncClient(timeout=self._config.timeout)`。若 `self._config` 不存在或 `timeout` 未设置,fallback 到 60.0。
|
||
|
||
**Patterns to follow:** `RemoteLLMProvider` 已使用 `timeout=120.0` 参数模式
|
||
|
||
**Test scenarios:**
|
||
- Happy path: ProviderConfig(timeout=120) → httpx client timeout=120
|
||
- Edge case: ProviderConfig(timeout=0) → fallback 到 60.0
|
||
- Edge case: ProviderConfig 未设置 timeout → 使用默认 120.0
|
||
- Integration: 实际 LLM 调用在 60-120s 之间完成 → 旧逻辑会超时,新逻辑成功
|
||
|
||
**Verification:** 单元测试通过 + benchmark 中无 httpx 超时错误
|
||
|
||
---
|
||
|
||
### U3. 激活 QualityGate skill_match 校验
|
||
|
||
**Goal:** 在技能执行管线中传入 skill_context,激活 skill_match 输出一致性校验
|
||
|
||
**Requirements:** R3
|
||
|
||
**Dependencies:** U4(disambiguation_keywords 提供 intent_keywords 消歧)
|
||
|
||
**Files:**
|
||
- `src/agentkit/core/config_driven.py` — 在 `_execute_skill_task` 返回前调用 QualityGate.validate 传入 skill_context
|
||
- `src/agentkit/quality/gate.py` — 确认 `_check_skill_match` 读取 disambiguation_keywords
|
||
- `tests/unit/quality/test_gate.py` — 新增 skill_match 激活测试
|
||
|
||
**Approach:**
|
||
1. 在 `ConfigDrivenAgent._execute_skill_task()` 中,构造 `skill_context = {"intent_keywords": skill_config.intent.keywords + skill_config.intent.disambiguation_keywords}`
|
||
2. 调用 `self._quality_gate.validate(output, skill_context=skill_context)`
|
||
3. 在 `gate.py` 的 `_check_skill_match` 中,同时检查 `intent_keywords` 和 `disambiguation_keywords`
|
||
|
||
**Patterns to follow:** `gate.py` 现有 `_check_skill_match` 方法签名
|
||
|
||
**Test scenarios:**
|
||
- Happy path: 技能输出包含 intent_keywords → skill_match passed=True
|
||
- Error path: 技能输出不包含任何 intent_keywords → skill_match 警告
|
||
- Integration: reflexion_agent 输出包含 "review" → 与 code_reviewer 的 disambiguation_keywords 匹配 → 触发消歧警告
|
||
- Edge case: skill_context=None → 跳过 skill_match(向后兼容)
|
||
|
||
**Verification:** 单元测试通过 + 技能执行日志中出现 skill_match 校验记录
|
||
|
||
---
|
||
|
||
### U4. 添加 disambiguation_keywords 到易混淆技能对
|
||
|
||
**Goal:** 为 4 对易混淆技能添加 disambiguation_keywords,支持 QualityGate 消歧
|
||
|
||
**Requirements:** R5
|
||
|
||
**Dependencies:** 无
|
||
|
||
**Files:**
|
||
- `configs/skills/reflexion_agent.yaml` — 添加 disambiguation_keywords
|
||
- `configs/skills/code_reviewer.yaml` — 添加 disambiguation_keywords
|
||
- `configs/skills/react_agent.yaml` — 添加 disambiguation_keywords
|
||
- `configs/skills/goal_driven_agent.yaml` — 添加 disambiguation_keywords
|
||
- `configs/skills/rewoo_agent.yaml` — 添加 disambiguation_keywords
|
||
- `configs/skills/competitor_analyzer.yaml` — 添加 disambiguation_keywords
|
||
- `configs/skills/content_generator.yaml` — 添加 disambiguation_keywords
|
||
- `configs/skills/geo_optimizer.yaml` — 添加 disambiguation_keywords
|
||
- `src/agentkit/skills/base.py` — SkillConfig.intent 添加 disambiguation_keywords 字段
|
||
|
||
**Approach:**
|
||
1. 在 `SkillIntent` model 中添加 `disambiguation_keywords: list[str] = []` 字段
|
||
2. 为每对易混淆技能添加互斥关键词:
|
||
- reflexion_agent: `["反思", "自我验证", "迭代优化"]`
|
||
- code_reviewer: `["代码审查", "代码问题", "bug 检查"]`
|
||
- react_agent: `["实时搜索", "工具调用", "信息查询"]`
|
||
- goal_driven_agent: `["目标分解", "多步规划", "方案对比"]`
|
||
- rewoo_agent: `["并行采集", "批量获取", "多源数据"]`
|
||
- competitor_analyzer: `["竞品分析", "竞争对比", "市场对手"]`
|
||
- content_generator: `["内容创作", "文章生成", "选题写作"]`
|
||
- geo_optimizer: `["SEO 优化", "GEO 优化", "搜索排名"]`
|
||
|
||
**Patterns to follow:** 现有 `intent.keywords` 字段结构
|
||
|
||
**Test scenarios:**
|
||
- Happy path: SkillConfig 加载 yaml 含 disambiguation_keywords → 字段非空
|
||
- Edge case: yaml 未含 disambiguation_keywords → 字段默认空列表
|
||
- Integration: QualityGate 读取 disambiguation_keywords 用于消歧校验
|
||
|
||
**Verification:** `agentkit skill list` 正常加载所有技能 + 单元测试通过
|
||
|
||
---
|
||
|
||
### U5. 优化 RequestPreprocessor 路由正则
|
||
|
||
**Goal:** 新增 factual/数学/翻译类正则,减少不必要的 REACT 调用
|
||
|
||
**Requirements:** R6
|
||
|
||
**Dependencies:** 无
|
||
|
||
**Files:**
|
||
- `src/agentkit/chat/request_preprocessor.py` — 新增 3 条正则
|
||
- `tests/unit/chat/test_request_preprocessor.py` — 新增路由测试
|
||
|
||
**Approach:**
|
||
新增 3 条正则走 DIRECT_CHAT:
|
||
1. `_FACTUAL_RE` — "什么是X/X是什么/解释一下X/define X" 等纯知识问答
|
||
2. `_MATH_RE` — "计算X/算一下X/calculate X" 等简单数学(无变量、无方程)
|
||
3. `_TRANSLATION_RE` — "翻译X/translate X" 等纯翻译请求
|
||
|
||
**注意**: 这些正则必须严格匹配,避免误拦截需要工具的请求。例如 "分析一下服务器的IP" 不应匹配 `_FACTUAL_RE`(包含"分析"动词暗示需要工具)。
|
||
|
||
**Patterns to follow:** 现有 `_GREETING_RE` / `_CHAT_MODE_RE` / `_IDENTITY_RE` 正则模式
|
||
|
||
**Test scenarios:**
|
||
- Happy path: "什么是机器学习" → 匹配 _FACTUAL_RE → DIRECT_CHAT
|
||
- Happy path: "计算 1+2+3" → 匹配 _MATH_RE → DIRECT_CHAT
|
||
- Happy path: "translate hello to Chinese" → 匹配 _TRANSLATION_RE → DIRECT_CHAT
|
||
- Edge case: "什么是当前服务器的IP地址" → 不匹配 _FACTUAL_RE(含"当前服务器"暗示需要工具)→ REACT
|
||
- Edge case: "计算斐波那契数列的第100项" → 不匹配 _MATH_RE(含"斐波那契数列"暗示需要代码)→ REACT
|
||
- Error path: 空字符串 → 不匹配任何正则 → REACT
|
||
|
||
**Verification:** 单元测试通过 + benchmark 中 DIRECT_CHAT 比例提升
|
||
|
||
---
|
||
|
||
### U6. 重新基准测试 + 建立当前架构基线
|
||
|
||
**Goal:** 修复后重新运行 benchmark,建立当前 RequestPreprocessor 架构的基线
|
||
|
||
**Requirements:** R7
|
||
|
||
**Dependencies:** U1, U2, U3, U4, U5(所有修复完成后)
|
||
|
||
**Files:**
|
||
- `test-results/benchmark/baseline.json` — 更新基线
|
||
- `test-results/benchmark/benchmark_report.md` — 更新报告
|
||
|
||
**Approach:**
|
||
1. 运行 `agentkit benchmark --mode llm`(full 模式,真实 LLM)
|
||
2. 运行 `agentkit benchmark --mode llm --fast`(fast 模式)
|
||
3. 对比修复前后准确率、超时率、延迟
|
||
4. 更新 `baseline.json` 作为当前架构基线
|
||
|
||
**Test scenarios:**
|
||
- Happy path: full 模式准确率 ≥ 80%(5 用例至少 4 通过)
|
||
- Happy path: fast 模式准确率 = 100%
|
||
- Edge case: llm-001 不再超时
|
||
- Edge case: llm-004 不再超时
|
||
|
||
**Verification:** benchmark 报告生成 + 准确率达标
|
||
|
||
---
|
||
|
||
## Risks & Dependencies
|
||
|
||
| 风险 | 严重度 | 缓解措施 |
|
||
|------|--------|----------|
|
||
| 新增正则误拦截需要工具的请求 | 中 | 正则设计保守,仅匹配纯知识/数学/翻译,单元测试覆盖边界 |
|
||
| QualityGate skill_match 误报导致输出被拦截 | 中 | skill_match 单独不拦截(现有设计),仅与其他失败共病时拦截 |
|
||
| disambiguation_keywords 与现有 keywords 语义重叠 | 低 | disambiguation_keywords 是 keywords 的补充,不替代 |
|
||
| benchmark 超时提升后延迟增加 | 低 | 超时是上限而非目标,快速完成的用例不受影响 |
|
||
|
||
## Open Questions
|
||
|
||
无 — 所有技术决策已在 KTD 中明确。
|
||
|
||
## System-Wide Impact
|
||
|
||
- **LLM 网关**: httpx 超时修复影响所有 LLM 调用(更宽松的超时)
|
||
- **技能执行**: QualityGate 激活影响所有技能输出校验
|
||
- **Benchmark**: 超时阈值影响所有 benchmark 用例
|
||
- **路由**: 新增正则影响所有非显式前缀的请求
|
||
|
||
## Verification Results (2026-06-20)
|
||
|
||
### U1–U5 代码修复验证
|
||
|
||
| 单元 | 验证方式 | 结果 |
|
||
|------|----------|------|
|
||
| U1: Benchmark 超时 | `agentkit benchmark --mode llm` | ✅ llm-001/llm-004 不再超时 |
|
||
| U2: httpx 超时 | `pytest tests/unit/test_llm_provider.py` | ✅ 2 个新测试通过 |
|
||
| U3: QualityGate 激活 | `pytest tests/unit/quality/` | ✅ 176 个质量门控测试通过 |
|
||
| U4: disambiguation_keywords | 16 个技能 yaml 加载验证 | ✅ 全部加载成功 |
|
||
| U5: 路由正则 | `pytest tests/unit/chat/test_request_preprocessor.py` | ✅ 38 个测试通过(19 新增) |
|
||
|
||
### U6 基准测试结果
|
||
|
||
| 指标 | 修复前 (2026-06-20 03:18) | 修复后 (2026-06-20 11:05) | 变化 |
|
||
|------|--------------------------|--------------------------|------|
|
||
| 准确率 | 60.0% | 93.3% ± 9.4% | **+33.3%** |
|
||
| 通过/总数 | 3/5 | 4/5 | +1 |
|
||
| 超时数 | 2 | 0 (llm-002 偶发) | **-2** |
|
||
| 一致性 | N/A | 100% | — |
|
||
| p50 延迟 | 35.3s | 40.8s | +5.5s(可接受) |
|
||
|
||
**剩余问题**: llm-002 (tool_selection, medium) 在 3 次运行中 1 次超时,p95=56.3s 接近 medium 60s 阈值。后续可考虑提升 medium 超时至 75s。
|