# 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`，不会意外影响列表
- Leverage：`CampaignForm` 可被其他入口复用（如从报告页快速创建新活动）
- 可测试性：当前零测试，拆分后每个子组件可独立测试

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

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

Strong

#### 问题

一个文件包含 `BaseRepository`、`TargetRepository`、`ScenarioRepository`、`RunRepository`、`CampaignRepository`、`ResultRepository` 六个类。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.py`、`db/evaluations.py`、`db/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_session`、`RunRepository`、`ResultRepository`，在引擎内部管理数据库写入。核心评测执行引擎不是存储无关的——它既是领域逻辑（发消息、执行规则），又是持久化逻辑（写 run、写 result）。同样模式出现在 `campaign_runner.py`、`analysis.py`、`exploration/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（前端重复工具函数收敛），同样是小改造、高确定性收益。