- .scratch/v111-architecture-scan.md: mark §6.1, §6.2, §6.4 as RESOLVED (commits38e3817,f85eca1,6d32653); add §6.5 for T8 decision-log dedupe (commit4bcab06). - docs/release-notes-v1.1.0.md: add section ten listing the four post-release fixes shipped to main after v1.1.1 was deployed, so the release page documents what v1.1.1 production actually contains (and what the v1.1.1 image does NOT contain).
15 KiB
v1.1.1 智能评估模块架构扫描
生成:2026-08-13 | 用途:v1.1.1(收尾 + 测试加固 + 架构深化)的输入文档,驱动 Gitea tickets 拆解 范围:
backend/agenteval/intelligent_eval/为核心 + 外部边界(web routers、openclaw_client、前端、Worker Skill) 依据:grilling 共识(测试先行为锁定 → 分阶段深化 → 统一发布 v1.1.1)
1. 模块现状
intelligent_eval/ 共 13 文件 / 2883 行(v1.1.0 新增功能模块,生产单进程 FastAPI + SQLite):
| 文件 | 行数 | 职责 |
|---|---|---|
| repository.py | 459 | 3 个 Repository 类(Eval/Session/Message)+ CAS 条件写 |
| lifecycle.py | 354 | 智能评估生命周期(create/plan/approve/reject/cancel/report + session 会话) |
| cron_pool.py | 310 | Cron 池管理(initialize/scale/auto_scale/sync/stuck 检测) |
| decision.py | 276 | 自主决策(时段/欠账/严重度 → execute/wait/analyze) |
| task_queue.py | 247 | 任务队列(扫描入队/优先级/next/assign/complete) |
| alerts.py | 216 | 告警规则与历史 |
| fault_tolerance.py | 204 | 故障恢复(stuck 处理/对账/双端重启恢复) |
| read_model.py | 196 | 读模型投影(list/detail/report) |
| openclaw_client.py | 177 | OpenClaw 容器 CLI 客户端(docker exec) |
| report.py | 130 | 报告渲染 |
| metrics.py | 117 | 指标聚合 |
| config_snapshot.py | 109 | 配置快照 |
| models.py | 87 | 领域模型 |
依赖关系要点:lifecycle→repository;read_model→repository;cron_pool→openclaw_client + task_queue(267 行运行时 import complete_task);fault_tolerance→cron_pool+task_queue+openclaw_client;decision/task_queue 直接 session.exec 查库。
外部边界:
- Web:
web/routers/intelligent_evals.py(前缀/api/intelligent-evals)+web/routers/openclaw_cron_pool.py(前缀/api/openclaw) - OpenClaw 客户端:docker exec
openclawCLI 管理容器内 cron;硬编码gateway_token="agenteval-openclaw-token-2026" - Worker Skill:OpenClaw 容器内 cron worker 经 docker 网络直连后端
tasks/next|assign|complete+heartbeat,不经前端 - 前端:
IntelligentEvals.tsx(读模型useIntelligentEvalRead,统一轮询 + stale 过滤);CronPoolMonitor.tsx(自带 setInterval 5s,无统一 hook)
2. Seam 泄漏清单(深化候选)
| # | 位置 | 泄漏 | 影响 | 深化方向 |
|---|---|---|---|---|
| S1 | task_queue.py(_is_slot_due/_calculate_session_deficit/_calculate_priority)vs decision.py(_parse_time_slot/_get_current_slot/_count_sessions_in_slot/_has_high_severity_issues) |
时段/欠账/严重度领域知识重复:"8-10h" 解析两份、时段内 session 统计两份、优先级/欠账各自实现 | 改口径需改两处,易漂移 | 收敛为 shared 领域模块,两模块消费同一实现 |
| S2 | web/routers/intelligent_evals.py 后段(tasks/next 内联拼装、assign/complete、decision-logs POST/GET 直接 ORM at L332/360)+ openclaw_cron_pool.py(scale 决策混入、heartbeat ORM at L86/87) |
HTTP 层直接拼装 ORM/领域逻辑;前段干净 HTTP→lifecycle,后段落差 | decision-logs(Worker 审计面)回归风险高 | 收进 service/repository,router 只做参数校验+委托 |
| S3 | decision.py、task_queue.py 直接 session.exec 查 IntelligentEvalSessionDB |
纯决策/队列逻辑与 SQLite 直接耦合,绕过 repository/read_model | 可测试性受限(需真实 DB);逻辑难单测 | 注入读模型/查询接口,逻辑层纯化 |
| S4 | cron_pool.py:267 handle_stuck_cron 内 from ... import complete_task |
stuck→任务结算横跨两模块(运行时 import) | 卡死恢复的结算顺序分散 | 把 stuck→requeue/complete 结算收进 task_queue/统一作业模块 |
| S5 | openclaw_client.py 硬编码 gateway_token/container_name + subprocess |
外部系统边界配置硬编码;token 明文进进程列表;无独立测试(全 mock) | 换 token/环境需改代码;边界无守护 | token/容器走 settings 注入;subprocess 参数/解析加契约测试 |
| S6 | pages/CronPoolMonitor.tsx 自带 setInterval |
前端轮询不一致:Cron 池不走统一 usePolling,无 stale 过滤;无测试 | 与智能评估读模型行为漂移;卸载/不可见不清理 | 抽统一轮询 hook + 页面测试 |
| S7 | Worker Skill 回路(tasks/next/assign/complete + heartbeat) | 关键链路无端到端守护(heartbeat→busy→kill→requeue 故障链路缺失) | 深化动代码时最怕破坏的回环 | 补 e2e 测试 + router 下沉后契约守卫 |
非候选:
repository.py单文件多类——v2 方法论明确"不因文件长度拆分 Repository,文件移动不算 deepening",不处理。
3. 测试覆盖矩阵
现有覆盖(智能评估相关:10 单元 + 9 集成 + 1 前端)
| 主题 | 单元 | 集成 | 前端 | 评价 |
|---|---|---|---|---|
| 生命周期状态机 | ✅ model 测试 | ✅ evals_api | — | 完善 |
| 决策逻辑 | ✅ decision 测试 | — | — | 分支覆盖好 |
| 任务队列 | ✅ task_queue 测试 | ✅ task_queue_api | — | happy path 完整 |
| Cron 池管理 | ✅ cron_pool 测试 | ✅ cron_pool_api | — | 扩缩容路径完整 |
| 故障恢复 | ✅ fault_tolerance | ✅ fault_tolerance_e2e | — | 双端恢复均有 |
| 告警 | ✅ alerts 测试 | ✅ metrics_alerts_api | — | 规则覆盖好 |
| 快照 | ✅ config_snapshot | ✅ config_snapshot_api | — | 完善 |
| 读模型 | ✅ read_model 测试 | — | ✅ intelligentEval.test | 有 stale 过滤 |
| Worker Skill | — | ✅ worker_skill_api | ❌ | CRUD 有,回路无 e2e |
| Cron 池监控前端 | — | — | ❌ 0 测试 | 5s 轮询无守卫 |
6. 实施期间发现的新问题(v1.1.1 P0 期间)
这里记录在按本扫描实施时新暴露的真实问题(不在原 seam/T 列表里),是测试防护网的首要价值。
6.1 assign_task 缺原子 CAS(真 bug,T1 暴露)— ✅ RESOLVED (38e3817)
症状:tests/integration/test_worker_task_resilience.py::test_concurrent_assign_via_two_sessions 失败。
两个独立 session(两个 worker 进程)并发调用 assign_task 同一 pending task 时,两个都返回 True——任务被双认领。
根因:backend/agenteval/intelligent_eval/task_queue.py 的 assign_task 是非原子的 读-检查-写:
task = session.get(...); if task.status != "pending": return False
task.status = "assigned"; session.commit()
SQLite(check_same_thread=False + StaticPool)下两个 session 各自读到 pending、各自检查通过、各自 commit,无行级锁/CAS。
影响:Worker Skill 端点 POST /tasks/{id}/assign 可被双认领 → 两个 cron 同时执行同一任务 → 重复执行、可能双写 report/decision log。当前顺序 assign(单 session)通过只是因为 session 内 serializable。
修复方向(待开独立 issue,不在 #3 范围):
assign_task改为UPDATE ... WHERE id=? AND status='pending',按 affected_rows 判定(真正的 CAS)。- 同样审计
complete_task的并发(虽然 converge 测试通过,但非原子写)。 get_next_task的SELECT ... LIMIT 1也非原子,应改UPDATE ... WHERE status='pending' ... RETURNING抢占式。
6.2 Webhook 缺重试 + 缺去重(两个真缺口,T6 暴露)— ✅ RESOLVED (f85eca1)
症状:test_webhook_retries_on_failure 与 test_webhook_dedupes_repeat_triggers xfail。
backend/agenteval/intelligent_eval/alerts.py::AlertManager._send_webhook:
try:
httpx.post(self.webhook_url, json=payload, timeout=5.0); ...
except Exception as e:
_logger.error(f"Failed to send webhook: {e}") # 吞掉,不重试
- 无重试:首次失败仅 log,
webhook_sent保持False,无后续尝试。 - 无去重:
check_alerts每次触发都会调_send_webhook(alert);同一 alert 在恢复前可被重复发送。
影响:瞬时网络抖动丢告警;告警风暴期间 webhook 被反复打。
修复方向(待开独立 issue,不在 #5 范围):
- 重试:失败后排队 N 次重试(指数退避),或记入 outbox 表后台重发。
- 去重:
alert.webhook_sent=True后跳过;或加webhook_in_flight锁。
当前状态:两个 xfail 守卫在 test_openclaw_client_and_webhook.py,CI 不阻塞;修复后移除 xfail 即转绿。
6.3 前端 CronPoolMonitor 不响应 visibilitychange(S6 缺口,T2 暴露)
症状:frontend/web/src/pages/CronPoolMonitor.test.tsx 的 it.fails('listens to visibilitychange ...')。
CronPoolMonitor 当前的 useEffect 只做了 setInterval(loadData, 5000) + clearInterval 清理,不监听 document.visibilitychange。
影响:当浏览器标签页被切换到后台或最小化时,仍每 5 秒轮询后端 /api/openclaw/cron-pool/* —— 浪费网络/CPU,且与 useIntelligentEvalRead 的"终态停轮询"行为漂移。
修复方向(待开独立 issue,不在 #6 范围):
- 在
useEffect里document.addEventListener('visibilitychange', ...);hidden时清除 timer 并清掉interval引用,visible时立即loadData()并重建 interval;cleanup 同时removeEventListener。 - 或抽统一
usePolling(loader, { intervalMs, pauseWhenHidden })hook 让智能评估读模型与 Cron 池共用。
当前状态:it.fails 守卫在 CronPoolMonitor.test.tsx,CI 显示 1 expected fail(vitest 等价 xfail),不阻塞;修复后移除 it.fails 即转绿。
6.4 无告警→auto_scale 联动(真缺口,T7 暴露)— ✅ RESOLVED (6d32653)
症状:tests/integration/test_alert_autoscale_link.py::test_check_alerts_triggers_auto_scale_on_high_utilization xfail。
backend/agenteval/web/routers/openclaw_cron_pool.py::check_alerts 走完整条规则链只创建 AlertHistoryDB 行,从不调 cron_pool.scale_up / scale_down。高利用率告警入库后无人(或外部 cron)触发 auto_scale,生产需要操作员看 Web UI 手动点。
注意(已修正的早期误判):AlertManager 在 router 内 per-request 新建看似 bug,但 check_rules 逻辑对 duration_minutes==0 的规则(task_backlog、stuck_rate)走 fall-through 分支直接创建 alert,不需要 triggered_at 持久化。task_backlog 端到端测试 xpass 即证明。pool_utilization(duration=10)若端到端测试,会暴露 per-request triggered_at 丢失,但当前 alert 联动缺口更优先。
影响:高利用率/任务积压/卡死率告警频发但扩缩容不会自动发生(依赖人盯),或发生时机滞后。
修复方向(待开独立 issue,不在 #6 范围):
check_alerts在创建 alert 后按alert_type映射调用cron_pool.scale_up/scale_down;或注册 webhook 接收方;或独立 worker 轮询unresolved alerts→ 调auto_scale。- 顺带把
pool_utilization(duration_minutes=10)规则的triggered_at持久化(如果后续要端到端测持续窗口)。
当前状态:✅ RESOLVED in 6d32653 — AlertManager.maybe_autoscale(alert) called from the async router, exercises cron_pool.scale_up(1) (capped at MAX_POOL_SIZE). Test rewritten to use task_backlog (duration_minutes=0) so a single check_alerts call fires the alert.
6.5 decision-logs 不去重(T8 暴露)— ✅ RESOLVED (4bcab06)
症状:tests/integration/test_decision_log_immutability.py::test_decision_log_dedupes_identical_entries xfail. Three identical POSTs to the same eval with the same (decision_type, context) produced three identical rows in intelligent_eval_decision_logs.
根因:agenteval.intelligent_eval.decision_logs.create_decision_log was append-only — it did not look up an existing row with the same context JSON before inserting.
影响:Worker re-emitting the same decision within a single minute (e.g. task_backlog re-fire from a stuck cron retry) pollutes the audit table.
修复方向(已在 4bcab06 实施):在 create_decision_log 内部,先用 SELECT … WHERE eval_id=? AND decision_type=? 找到现有行,若其 get_context() == context 则直接返回该现有行;否则 INSERT。append-only 不变性仍然成立(同 (eval, decision_type, context) 不会重复;context 不同的会追加新行——这正是 test_decision_log_is_append_only_on_context_change 验证的)。
当前状态:✅ RESOLVED — xfail 守卫已移除,测试通过(3 identical POSTs → 1 DB row;append-only on context change 仍通过)。
薄弱/缺失点(加固清单 T)
薄弱/缺失点(加固清单 T)
| # | 缺口 | 位置 |
|---|---|---|
| T1 | Worker Skill 三端点并发/竞争(双 worker 拉 next 不得同 task;complete 与 stuck 同时发生不丢任务) | task_queue / worker 回路 |
| T2 | CronPoolMonitor 页面/hook 测试(unmount 清理、不可见暂停轮询) | 前端 |
| T3 | router 内嵌 ORM 契约测试(decision-logs、heartbeat 一旦下沉的守卫) | 集成 |
| T4 | auto-scale 振荡(连续调度不抖动翻转) | cron_pool |
| T5 | OpenClawClient 单元测试(subprocess 参数、token 位置、JSON 解析失败路径) | openclaw_client |
| T6 | webhook 失败重试/去重(已发送但失败、重发不重复建 alert) | alerts |
| T7 | 告警→扩缩容端到端联动(check_alerts → auto_scale 贯穿链路) | 集成 |
| T8 | decision-logs 不可变性/去重(同 eval+type+context 不重复;context 修改追加非覆盖) | worker_skill |
| T9 | stuck 检测临界值边界(last_active_at 临界) | cron_pool |
| T10 | e2e 故障链路(heartbeat→busy→kill→requeue 完整闭环) | 集成 e2e |
4. 深化 Phase 框架(v2 风格,内容待 tickets 细化)
| Phase | 内容 | 对应 seam |
|---|---|---|
| P0 测试加固(防护网) | 覆盖矩阵清零 + T1–T10 补齐;不改生产代码 | — |
| P1 收敛领域知识 | 时段/欠账/严重度 → shared 模块;stuck 结算收敛 | S1, S4 |
| P2 解耦 DB | decision/task_queue 走读模型/repository,逻辑纯化 | S3 |
| P3 下沉 Router 逻辑 | decision-logs/heartbeat/tasks 收进 service | S2 |
| P4 边界收尾 + ADR | openclaw_client 配置化、前端轮询统一、删旧、记录 ADR | S5, S6, S7 |
顺序依赖:P0 先行(锁行为)→ P1(收敛)→ P2(解耦,为 P3 提供 repository 能力)→ P3(router 下沉)→ P4(边界+ADR)。每阶段 CI 全绿、可独立合入。
5. 收尾发布(v1.1.1)
- 测试/深化全部合入 main、CI 全绿后统一发布:
bump 1.1.0 → 1.1.1→ 完整回归(后端全套 + 前端 + tsc + 生产构建 + ruff + 迁移往返)→ 生产部署演练(副本验证迁移/恢复/读取)→ CD 自动部署到生产(1.0.0→1.1.1)→ 健康检查 + 关键路径 smoke - 附带收尾:CI 镜像 push 到私有 registry(持久化)