AgentEvalTool 架构审查报告

2026-08-24 · 全面代码库健康审查

15,609
后端代码行 (101 文件)
11,576
前端代码行 (75 文件)
18,289
后端测试行 (101 文件)
1,347
前端测试行 (12 文件)

架构全景

graph TB subgraph Frontend["前端 (React + TypeScript)"] Pages["Pages
11 页面组件
~4,100 行"] Components["Components
~25 共享组件
~3,600 行"] API["API Layer
10 域模块
~720 行"] Read["Read Module
泛化资源接缝
~430 行"] Hooks["Hooks
7 hooks
~640 行"] end subgraph Backend["后端 (Python 3.11 + FastAPI)"] Routers["Routers
12 路由
~2,100 行"] Domain["Domain Layer
intelligent_eval / evaluation / exploration"] Storage["Storage Layer
6 repositories
~2,300 行"] Channels["Channels
3 adapters
~700 行"] end subgraph Persistence["持久化"] SQLite["SQLite
15+ 表定义
~700 行"] end Pages --> API Pages --> Components Pages --> Read Read --> API Hooks --> API API --> Routers Routers --> Domain Domain --> Storage Domain --> Channels Storage --> SQLite
整体评价:分层清晰,storage 不依赖 web/channels,domain 层通过 repository 模式与存储交互。 后端测试覆盖率高(测试行 > 源码行)。主要问题集中在前端大组件后端 god module重复工具函数三个方向。

改进候选点

1. Campaigns.tsx — 前端 God Component

frontend/web/src/pages/Campaigns.tsx · 1,018 行

Strong

问题

整个活动管理页面(列表、创建表单、详情视图、报告渲染、时间线、分析区、对比视图、探索式评测)全部塞在一个 1,018 行的组件里。包含 9 个 return 语句、286 个 JSX 标签。任何局部修改都需要在这个巨文件中导航,AI 代理处理时容易丢失上下文。

Before — 单一巨组件
Campaigns.tsx (1,018 lines)
├── 列表视图 + 筛选
├── 创建表单 (FormDrawer)
├── 展开行 (expandedRowRender)
│ ├── 时间线 (WindowTimeline)
│ ├── Run 时间线 (CampaignRunTimeline)
│ ├── 分析区 (AnalysisSection)
│ ├── 对比区 (PeriodComparisonSection)
│ └── 探索区 (ExplorationSection)
├── 报告视图 (单 report 查看)
├── 本地 StatusBadge, KpiItem, SectionTitle
└── fmtPct(), targetName(), rateColorName()
After — 按视图职责拆分
pages/Campaigns.tsx (~200 lines)
├── 列表壳 (table + filter + drawer)
└── 路由到子视图

components/campaigns/
├── CampaignForm.tsx (创建/编辑表单)
├── CampaignDetail.tsx (展开行容器)
├── CampaignReport.tsx (单报告视图)
├── CampaignAnalysis.tsx (分析区)
└── CampaignStatusBadge.tsx (状态标签)

utils/campaignFormat.ts (fmtPct, targetName)

收益

  • Locality:修改分析区只动 CampaignAnalysis.tsx,不会意外影响列表
  • LeverageCampaignForm 可被其他入口复用(如从报告页快速创建新活动)
  • 可测试性:当前零测试,拆分后每个子组件可独立测试

2. storage/repository.py — 六合一 Repository 文件

backend/agenteval/storage/repository.py · 814 行

Strong

问题

一个文件包含 BaseRepositoryTargetRepositoryScenarioRepositoryRunRepositoryCampaignRepositoryResultRepository 六个类。Phase 2+3 拆分后仍残留 814 行。每次修改某个域的 repository 都要在这个大文件中定位,且所有域的 import 都指向同一个模块(from agenteval.storage.repository import ...),形成宽接缝。

Before — 六合一
graph LR A[storage/repository.py
814 lines] --> B[BaseRepository] A --> C[TargetRepository] A --> D[ScenarioRepository] A --> E[RunRepository] A --> F[CampaignRepository] A --> G[ResultRepository]
After — 按域拆分
graph LR A[storage/base_repository.py] --> B[BaseRepository] C[storage/target_repository.py] --> D[TargetRepository] E[storage/scenario_repository.py] --> F[ScenarioRepository] G[storage/run_repository.py] --> H[RunRepository] I[storage/campaign_repository.py] --> J[CampaignRepository] K[storage/result_repository.py] --> L[ResultRepository]
注意:已有先例——async_job_repository.py(247 行)、exploration_repository.py(135 行)、model_config_repository.py(114 行)、file_repository.py(156 行)都已独立拆分。剩余 6 个是 Phase 2+3 未触及的遗留。

收益

  • Locality:修改 Run 查询逻辑只动 run_repository.py
  • 一致性:与已有的 async_job/exploration/model_config/file repository 对齐
  • 减少 import 耦合:需要 RunRepository 的模块不再被迫 import 整个 814 行文件

3. 前端重复工具函数 — personaLabel / fmtPct / targetName

跨 10+ 文件,至少 5 种重复模式

Worth exploring

问题

三个工具函数在多个组件中各自实现,逻辑完全相同:

函数 出现次数 位置
personaLabel() 3 ExecutionProcess.tsx:30, EvalOverview.tsx:9, EvalReport.tsx:24
fmtPct() 5 Campaigns.tsx:118, PeriodComparisonSection.tsx:31, ExplorationSection.tsx:34, CampaignRunTimeline.tsx:96, Reports.tsx:499
targetName() 2 Campaigns.tsx:150, IntelligentEvals.tsx:105
_translate() 错误翻译 3 routers/intelligent_evals.py:64, routers/exploration.py:45, routers/model_configs.py:29
Before — 各自实现
// ExecutionProcess.tsx:30
function personaLabel(persona: Record<string, unknown>): string {
  return (persona.background as string) || '未设置人设';
}

// EvalOverview.tsx:9
function personaLabel(persona: Record<string, unknown>): string {
  return (persona.background as string) || '未设置人设';
}

// Campaigns.tsx:118
function fmtPct(v: number | null) {
  return v == null ? '—' : `${(v * 100).toFixed(1)}%`;
}
After — 收敛到 utils
// utils/format.ts
export function fmtPct(v: number | null): string {
  return v == null ? '—' : `${(v * 100).toFixed(1)}%`;
}

// utils/persona.ts
export function personaLabel(
  persona: Record<string, unknown>
): string {
  return (persona.background as string)
    || '未设置人设';
}

// 所有消费者 import from utils

收益

  • Locality:修改百分比格式(如加千位分隔符)只需改一处
  • 一致性:消除不同文件中格式微妙的差异
  • 深度:工具函数本身就是深模块的候选——小接口,行为确定

4. runs.py Router — 编排逻辑泄漏到路由层

backend/agenteval/web/routers/runs.py · 211 行

Worth exploring

问题

_run_evaluation()(35-66 行)是一个后台协程,在 router 文件内创建 EvalEngine、管理 session 生命周期、调用 webhook。这是 use-case 编排逻辑,应该住在 service/use-case 层。同样,get_run_logs()(139-211 行)在 router 内做大量数据组装。

Before — 编排在 Router
graph TB A[POST /api/runs] --> B[routers/runs.py] B --> C[_run_evaluation()
后台协程] C --> D[EvalEngine] C --> E[get_session] C --> F[send_run_webhook] B --> G[get_run_logs
数据组装 70 行]
After — 编排下沉到 Service
graph TB A[POST /api/runs] --> B[routers/runs.py
~50 lines] B --> C[services/run_execution.py
RunExecutionService] C --> D[EvalEngine] C --> E[send_run_webhook] B --> F[services/run_read.py
RunReadService]

收益

  • 可测试性:编排逻辑可在无 FastAPI 上下文下单元测试
  • 复用:CLI 入口也可调用同一 service 触发评测运行
  • Seam 清晰:router 只做 HTTP 翻译,service 做编排

5. intelligent_eval/lifecycle.py — 单体状态机

backend/agenteval/intelligent_eval/lifecycle.py · 866 行

Worth exploring

问题

整个智能评估领域的状态机 + 所有领域操作集中在一个文件:create、delete、list、submit_plan、approve、reject、cancel、open_session、conduct_turn、close_session、submit_report,加上所有转换守卫。866 行,import 了 5 个模块。这是项目中最深的领域模块,但深度以"大"而非"杠杆"衡量。

权衡:这个文件是智能评估的核心状态机。ADR-0008 明确要求加深智能评估接缝。近期已经历多轮重构(watchdog 收敛、scheduler 拆分、decision-log 去重)。进一步拆分需要谨慎——状态转换的完整性是这个模块的核心价值,拆散可能反而增加跨文件追踪转换的成本。
当前 — 单体 lifecycle
lifecycle.py (866 lines)
├── _TRANSITIONS 转换表
├── create_eval()
├── delete_eval()
├── list_evals()
├── submit_plan()
├── approve() / reject()
├── cancel()
├── open_session()
├── conduct_turn() ← 最复杂
├── close_session()
└── submit_report()
可选 — 按阶段职责拆分
lifecycle.py (~200 lines)
├── _TRANSITIONS 转换表
├── create_eval() / delete_eval()
├── list_evals()
└── _transition() 守卫

plan_management.py
├── submit_plan() / approve() / reject()

session_execution.py
├── open_session() / conduct_turn()
├── close_session()

report_submission.py
└── submit_report()

收益

  • 可测试性conduct_turn() 的测试不需要 setup 整个状态机
  • Locality:修改计划审批逻辑只动 plan_management.py
  • 风险:状态转换完整性需要 characterization 测试锁定

6. storage/db.py — 15+ 表定义堆积

backend/agenteval/storage/db.py · 709 行

Speculative

问题

所有 SQLModel 表定义(EvalTargetDB、ScenarioDB、EvalRunDB、TurnDB、EvalResultDB、CampaignDB、IntelligentEvalDB、IntelligentEvalSessionDB、IntelligentEvalMessageDB、IntelligentEvalDecisionLogDB、IntelligentEvalTaskQueueDB、IntelligentEvalConfigSnapshotDB、ExplorationSessionDB、ExplorationMessageDB、ModelConfigDB、FileCategoryDB、FileRecordDB 等 17+ 个表)+ engine 初始化 + session 工厂全在一个文件。随着新领域实体增加,这个文件会持续增长。

审慎建议:当前 709 行尚在可控范围。拆分的收益主要在"找表定义时不用翻太多行"。但如果未来再加 5+ 表(如 A/B 测试、回归基线),建议按领域拆分为 db/targets.pydb/evaluations.pydb/intelligent_eval.py 等。现阶段优先级低于候选 1-4。

7. 前端测试覆盖空白

12 测试文件 vs 75 源文件 · 页面组件零覆盖

Speculative

问题

前端测试集中在 intelligent_eval/ 子组件和 read/ 模块(最近重构新增),但以下区域完全无测试:

未覆盖区域 代码行数 风险
所有 Page 组件 (11 个) ~4,100 行 高 — 用户直接交互界面
RunList, CaseDetail, PeriodComparison 等共享组件 ~1,200 行 中 — 被多页面复用
useRunSession, useFiles, usePolling hooks ~480 行 中 — 含 WS/REST 副作用
所有 API 模块 (10 个) ~720 行 低 — 薄包装,但类型安全靠 tsc
建议策略:不为测试而测试。优先在修改 Page 组件时(如候选 1 拆分 Campaigns.tsx)同步补测试。hooks 的测试优先级高于 Page(副作用逻辑更密集)。API 层靠 TypeScript 类型检查即可。

8. EvalEngine 与 Storage 耦合

backend/agenteval/evaluation/engine.py · 629 行

Speculative

问题

EvalEngine 直接 import get_sessionRunRepositoryResultRepository,在引擎内部管理数据库写入。核心评测执行引擎不是存储无关的——它既是领域逻辑(发消息、执行规则),又是持久化逻辑(写 run、写 result)。同样模式出现在 campaign_runner.pyanalysis.pyexploration/judge.py

graph LR subgraph Engine["EvalEngine (629 lines)"] A[send message] --> B[execute rules] B --> C[write to DB] end subgraph Storage["直接依赖 Storage"] D[get_session] E[RunRepository] F[ResultRepository] end Engine --> Storage
审慎建议:解耦 Engine 需要引入 Unit-of-Work 或 Repository 注入模式,改造量大。当前通过 MockChannel + 内存 DB 测试已能覆盖。除非需要替换存储后端(如迁移到 Postgres),否则改造 ROI 不高。标记为 speculative。

总结与优先推荐

# 候选点 强度 改造量 核心收益
1 Campaigns.tsx 拆分 Strong 消除前端最大 god component
2 storage/repository.py 拆分 Strong 与已有拆分模式对齐
3 前端重复工具函数收敛 Worth 消除 10+ 处重复
4 runs.py 编排下沉 Worth Router 回归纯翻译
5 lifecycle.py 状态机拆分 Worth 降低核心状态机认知负荷
6 storage/db.py 表定义拆分 Speculative 找表定义更快
7 前端测试覆盖补全 Speculative 页面级回归保护
8 Engine/Storage 解耦 Speculative 存储可替换性

Top Recommendation: 候选 2 — storage/repository.py 拆分

理由:改造量最小(已有 4 个独立 repository 先例,模式成熟),收益确定性最高(与已有模式对齐,消除 814 行 god file),风险最低(纯机械拆分 + import 路径更新,不涉及逻辑变更)。 建议作为下一步立即执行的改进。

次选:候选 3(前端重复工具函数收敛),同样是小改造、高确定性收益。