AgentEvalTool/AGENTS.md
sinohqb 1636ad9a66
All checks were successful
CI / test (pull_request) Successful in 4m22s
docs(ai-context): 合并 AI 上下文文档为单一 AGENTS.md 权威源
- AGENTS.md 改为工具无关的唯一权威正文:迁移 AGENT.md/CLAUDE.md 中仍有效的独有内容(判定与报告口径、require_auth 鉴权、ci-check.sh/pre-push、Gitea Actions 生效条件、简体中文约定),头部声明 CLAUDE.md/AGENT.md 为指针
- CLAUDE.md / AGENT.md 缩为入口指针,禁止再写入正文,杜绝三份副本漂移
- 同步修正关键抽象/配置中已过时的鉴权与 CI 描述(v0.5/v1.0 时代内容 vs 实际 v1.3.1 代码)
2026-09-03 11:59:23 +08:00

237 lines
18 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.

# AGENTS.md
本文件是仓库**唯一权威**的 AI 助手上下文,适用于所有按 `AGENTS.md` / `CLAUDE.md` / `AGENT.md` 约定加载上下文的工具(Claude Code、Codex、Qoder 等)。根目录 `CLAUDE.md` 与 `AGENT.md` 仅为入口指针,正文一律维护在本文件。
## 项目概述
**AgentEvalTool** 是智能体质量评估工具集平台(版本号以 `pyproject.toml` 为单一数据源,勿在此硬编码),用于评估 AI 数字员工(tutu-api 通道)和 AI 助手(OpenClaw)的服务质量。Python 3.11 后端 + TypeScript/React 前端,SQLite 持久化。领域术语词汇表见 `CONTEXT.md`,关键决策见 `docs/adr/`。
## 常用命令
### 后端
```bash
# 安装(含 dev 依赖)
pip install -e ".[dev]"
# 启动 Web 后台
agenteval server start --host 0.0.0.0 --port 8000
# 运行全部测试
pytest
# 运行单个测试文件
pytest tests/unit/test_engine.py -v
# 运行带覆盖率的测试
pytest --cov=backend/agenteval --cov-report=term-missing
# Lint
ruff check backend/
ruff format backend/
```
### 前端
```bash
cd frontend/web
npm install
npm run dev # 开发服务器
npm run build # 构建生产产物(部署前必须执行)
npx tsc --noEmit # 类型检查
```
### 数据库迁移
```bash
alembic upgrade head # 升级到最新
alembic revision --autogenerate -m "描述" # 生成新迁移
```
注意:`init_db()` 调用 `SQLModel.metadata.create_all()` 直接建表(用于全新 DB),Alembic 用于已有 DB 的增量变更。`migrations/env.py` 设置了 `render_as_batch=True`(SQLite 不支持 ALTER TABLE,batch mode 通过重建表来模拟)。
### 部署(t480)
```bash
scripts/deploy-t480.sh # 一键部署(推荐)
scripts/deploy-t480.sh --skip-build # 仅代码变更,跳过镜像重建
scripts/deploy-t480.sh --dry-run # 预览命令
```
## 核心架构
### 三层结构
```
CLI (Typer) / Web API (FastAPI)
↓
EvalEngine(异步评测执行引擎)
↓ 通过抽象接口
EvalChannel → 消息通道(tutu-api / openclaw / http)
EvalRule → 评估规则(keyword / response_time / llm_score / safety / semantic / …)
↓
SQLite Storage(Repository 模式)
```
### 关键抽象(均在 `backend/agenteval/`)
- **`channels/base.py`** — `EvalChannel` 抽象:`send()` / `poll_reply()` / `health_check()`;三种实现 `tutu.py`(tutu-api HTTP + Bearer Token)/ `openclaw.py` / `http.py`,`ChannelType` 枚举(`tutu-api` / `openclaw` / `http`)与工厂注册见 `channels/factory.py`
- **`evaluation/engine.py`** — `EvalEngine`:异步执行,遍历 Scenario.cases → messages → 调用 channel → 执行规则 → 写入 DB;通过 `asyncio.Event` cancel_token 支持真取消;`progress_callback` 实时推送事件给 WebSocket
- **`evaluation/rules/`** — `EvalRule` 抽象,注册表(`__init__.py` 导入即注册)已扩到 7 种实现:`keyword.py` / `response_time.py` / `llm_score.py`(LLM-as-judge,支持多维评分)/ `safety.py` / `semantic.py` / `fluency.py` / `json_schema.py`;跨规则隐式逻辑在 `evaluation/implicit_rules.py`
- **`models.py`** — 共享 Pydantic 模型:`EvalTarget` → `EvalRun` ← `Scenario`,`EvalRun` → `Turn` + `EvalResult`
- **`storage/`** — `db.py`(SQLite StaticPool + SQLModel 表定义)+ `repository.py`(`TargetRepository` / `ScenarioRepository` / `RunRepository` / `ResultRepository`)+ `file_repository.py`(`FileCategoryRepository` / `FileRecordRepository`)
- **`config/settings.py`** — Pydantic Settings,加载 `AGENTEVAL_` 前缀的环境变量(`.env` 文件),`@lru_cache` 单例
- **`web/deps.py`** — FastAPI 依赖注入:`get_db()`(Session 生命周期管理)、`require_auth()`(鉴权:`X-API-Key` 匹配 `AGENTEVAL_API_KEY` 或 `X-Auth-Token` 匹配 Web 登录会话,任一通过;两个凭据都未配置时 no-op 放行;`require_api_key` 是兼容别名)
**扩展性模式**:
- **规则注册表**(`evaluation/rules/__init__.py`):`@register_rule` 装饰器将规则类注册到 `_RULE_REGISTRY`,`get_rule(type, params)` 按名称实例化。添加新规则只需创建类并加装饰器。
- **通道工厂**(`channels/factory.py`):`ChannelFactory` 维护 `ChannelType → EvalChannel` 映射,`register()` 类方法支持插件式注册新通道类型。
### 判定与报告口径
通过率/判定口径是跨版本演进的敏感点(v0.5 判定语义 → v1.3 go/no-go),改动前先读相关 ADR,避免与报告层分叉:
- case 判定在 `evaluation/case_verdict.py`;run 级汇总在 `evaluation/run_summary.py`——`judged_pass_rate` 剔除连通用例、`pass_rate` 含执行失败(ADR-0002/0004)
- 隐式期望规则(期望叠加)在 `evaluation/implicit_rules.py`;go/no-go 判定在 `evaluation/go_no_go.py`
- 术语与口径基线见 `CONTEXT.md`
### API 端点 / Run 生命周期
`POST /api/runs` → API 先创建 `pending` 状态的 run,返回 run_id → 后台 `asyncio.Task` 调用 `EvalEngine.run(existing_run=...)` 复用该记录 → `ws://…/ws/runs/{run_id}` 推送实时进度。
**WebSocket 事件类型**(`/ws/runs/{run_id}`,由 `ConnectionManager.emit()` 广播):
| 事件 | 触发时机 | payload |
|------|---------|---------|
| `case_start` | 开始评测一个 case | `{case_id, case_index, total_cases}` |
| `messages_generated` | 动态 case 的 AI 生成消息完成 | `{case_id, messages}` |
| `turn_start` | 开始发送一轮对话 | `{case_id, turn_index}` |
| `turn_end` | 收到回复 | `{case_id, turn_index, reply, latency_ms}` |
| `turn_error` | 发送或接收失败 | `{case_id, turn_index, error}` |
| `rule_result` | 单条规则评估完成 | `{case_id, turn_index, rule_type, passed, score, reason}` |
| `case_end` | case 评测完成 | `{case_id, passed, score, error?}` |
| `run_completed` | 整个 run 完成 | `{run_id, status}` |
| `error` | case 级或 run 级错误 | `{case_id?, message}` |
前端 `sessionReducer.ts` 的 `WS_EVENT` action 处理以上所有事件类型。
### OpenClaw 代理
后端通过 `routers/proxy.py` 提供 HTTP + WebSocket 反向代理(路径前缀 `/openclaw`),将请求转发到 OpenClaw 服务(docker-compose 中 `openclaw-eval` 容器)。WS 桥接会自动注入认证 token 到 `connect.authenticate` 消息并重写 `Origin` 头。前端 `/openclaw` 路由以全屏 iframe 嵌入代理地址。
### 文件管理模块
`/api/files` 端点提供分类树 + 文件上传/下载功能。`FileCategoryDB` 自引用(`parent_id`)实现树形结构,`FileRecordDB` 关联分类。文件物理存储在 `data/uploads/`,按分类子目录组织。删除分类会级联删除子分类 + 文件记录 + 物理文件。
### 模型配置中心
`/api/model-configs` 统一管理工程使用的外部模型连接。`provider` 表示调用协议,`capability` 表示评测用途能力(对话、向量、审核),`input_modalities` / `output_modalities` 表示模型自身支持的文本、图像、音频和视频模态,三者禁止混用。厂商、区域、上下文窗口、最大输出和模型特性属于描述性元数据,会写入评测运行快照,但不直接改变网关请求参数。
### 智能评估执行机制(方案③触发式执行,ADR-0009/0010/0011/0012)
Worker 自动化由平台掌控节奏:`web/app.py` lifespan 只做 `scheduler_runtime.start()/stop()`,实际循环在 `intelligent_eval/scheduler.py`——每 60s(`SCAN_INTERVAL_SECONDS`)执行一次 `scan_once()`,单个节拍依次:
1. **requeue_stale_assigned_tasks**(task_queue.py):assigned 超时(>10min)且评估 executing → 回 pending(卡死恢复)
2. **expire_stale_running_sessions**:running 会话 60min 无新轮次 → expired
3. **watchdog 判死**:planning 双闸 / executing 超窗 / 连续触发失败超限 → failed
4. **scan_and_enqueue_tasks**:扫描 executing 评估,按时段到期 + 欠账(`domain.py::get_attention_reason`)→ 入队(每评估去重一条 pending/assigned)
5. **settle_tasks_for_finished_evals**:评估离开 executing 后,其 pending/assigned 任务回收为 completed
6. **supplement_decision_logs**:agent 未上报决策日志时按状态兜底补录(审计)
7. **analyst 催促**:末会话终态 10min 后落账 nudge,交给 worker 触发转 analyst 汇总报告
8. **触发**:fire-and-forget(不阻塞 60s 节拍)唤醒 worker / planner
关键约束(改动此机制前必读):
- **触发原语**:`docker exec openclaw-eval timeout 600 openclaw agent --agent main --session-id <prefix>-<ts> -m <msg> --json`,subprocess 侧 timeout 660
- **独立 session**:session-id 形如 `agenteval-worker-<ts>` / `agenteval-planner-<ts>`,**禁止复用 `--agent main` 的持久 session**——main session 多次触发累积上下文缓存(~12 万 token)后 worker 会幻觉输出而不执行 API 步骤(ADR-0010)
- **时段分布**:触发消息强制"仅执行当前时间对应时段(time_distribution)内欠账的会话",绝不创建未来时段会话——未来时段由后续 60s 节拍再次触发;所有时段会话到终态才转 analyst
- **状态一致性**:`submit_report` 要求会话全部 close 才允许 completed(`intelligent_eval/lifecycle/report_submission.py`);per-eval 触发冷却 10min 防 60s 节拍堆叠并发 agent
### 扩展域速览(v1.1+ 新增域,早期架构文档未覆盖)
- **Campaigns(评估活动)**:`evaluation/campaign_{lifecycle,runner,scheduler,read_model,report}.py` + `models.py` 的 `CampaignDB`;路由在 `web/routers/campaigns.py`;跨 run 聚合见 `comparison.py` / `analysis.py` / `go_no_go.py` / `cost_tracking.py`;后台任务登记在 `task_registry.py`
- **IntelligentEvals(智能评估域)**:`intelligent_eval/`——`domain.py`(优先级/注意力)、`lifecycle/`(状态机,终止态纪律见 ADR-0011/0012)、`task_queue.py`、`decision_logs.py`、`config_snapshot.py`、`read_model.py` / `report.py`;软删语义只在 `repository.py`(见陷阱 6)
- **Exploration(探索巡检)**:`exploration/patrol.py`(巡检)+ `judge.py` + `summary.py`,自带 models/errors/lifecycle
- **模型网关**:`model_gateway.py` 是统一模型调用出口,`model_protocols/` 为 openai / anthropic / dashscope / gemini 协议适配器;`services/` 承载 runs / model_configs / files 业务
- **AI 协作文档**:Gitea Issues 工作流、triage 标签、领域文档约定见 `docs/agents/`(issue-tracker / triage-labels / domain)
### 前端
SPA 由 FastAPI 托管(`GET /{full_path:path}` → `index.html`)。`frontend/web/src/api.ts` 是所有接口定义的单一出口,axios 拦截器统一处理错误。`useRunSession.ts` hook 管理 WebSocket 实时状态。
**路由采用 keep-alive 标签页模式**(非标准 `<Routes>`):所有页面同时挂载在 DOM 中,通过 `display: block/none` 切换可见性,保证页面状态在切 tab 时不丢失。路由配置集中在 `App.tsx` 的 `routeConfigs` 数组中,Zustand `tabStore` 管理标签页的打开/关闭/激活状态,`react-router` 仅用于 URL 同步。
**三种状态管理策略**:
- **Zustand**(`stores/tabStore.tsx`):全局标签页状态,唯一一个 store
- **useReducer**(`hooks/sessionReducer.ts`):评测会话状态(cases/turns/results/progress),纯 reducer 无副作用,hook 负责 WS/REST 副作用
- **useState**:各页面本地数据,无全局缓存(不使用 SWR/React Query)
**UI 交互一致性标准**(ADR-0005,全部管理页必须遵守):
- 页壳:`PageWrapper inline fullHeight`;详情/报告用页内 view 切换或右侧 Drawer 弹出(非新路由),列表常驻
- 表格:`Empty` 空态;超过 20 行才分页(pageSize 20);禁止 `scroll.y` calc hack,外层 div `overflowY: auto`
- 表单:`components/FormDrawer.tsx`(宽 640、页脚取消/确定、`submitting` loading、`destroyOnClose`)
- 危险操作(删除/取消/停止):`Popconfirm`
- 反馈:axios 拦截器统一 message,页面不重复弹错
- 活动状态轮询:`usePolling(fn, ms, enabled)` 5 秒静默轮询(`reload(true)`)
**Vite 开发代理**:dev server 端口 3000,`/api`、`/ws`、`/openclaw` 全部代理到 `localhost:8000`;生产构建时 `dist/` 由 FastAPI 直接托管。
**Ant Design CSS-in-JS 冲突**:Ant Design v5 的 emotion 运行时注入会覆盖静态 CSS 的 `flex`/`height` 规则。Runs 页采用"导航与内容分离"方案——Tabs 组件仅用于渲染导航头(`.nav-only-tabs` CSS 隐藏 `.ant-tabs-content-holder`),实际内容 div 由 flex 直接控制高度,绕过 Ant Design 内部 DOM。
**构建注意**:`@monaco-editor/react` 独立拆分为 `vendor-monaco` chunk(`vite.config.ts` 的 `manualChunks`)。设计 Token 在 `tokens.ts` 集中管理(组件直接 import,非 CSS 变量方案)。
## 开发约定
- **版本号单一数据源**:`pyproject.toml` → 由 `scripts/sync_version.py` 同步到 `package.json`,部署脚本自动执行
- **Python lint**:ruff,line-length 120,target py310
- **前端类型检查**:`tsc --noEmit`
- **文档文件名**带版本号,如 `architecture-v1.0.md`
- **SQLite 必须用 `StaticPool` + `check_same_thread=False`**,否则多线程下连接耗尽
- **所有 Router 用 `Depends(get_db)` 管理 Session**,后台任务也需 `try/finally` 关闭
- **`frontend/web/dist/`** 是构建产物,部署前必须 `npm run build`,否则 t480 跑旧版
- **本地 CI 入口**:`scripts/ci-check.sh`(版本一致性 + ruff + pytest + tsc;`--fast` 跳过 tsc)。pre-push hook 在 `.git/hooks/pre-push`(新 clone 需手动重装),push 自动跑 `ci-check.sh --fast`;紧急 `git push --no-verify` 后必须补跑
- **交互与文档使用简体中文**
## 配置
- `.env`(参考 `.env.example`):`AGENTEVAL_API_KEY`(机器调用凭据,X-API-Key)、`AGENTEVAL_ADMIN_PASSWORD`(Web 登录口令,设置后走会话 token)、`AGENTEVAL_ALLOWED_ORIGINS`(CORS,首个非 localhost URL 用作 OpenClaw WS Origin)、`AGENTEVAL_OPENCLAW_*` 系列(代理上游地址 + 认证 token)
- `config/config.json`:tutu-api 通道配置(敏感信息,只读挂载到容器)
- 无托管 CI 实际运行:Gitea Actions 工作流(`.gitea/workflows/ci.yml`)需实例启用 Actions + runner 才生效;本地质量门是 pre-push hook;部署依赖手动执行 `scripts/deploy-t480.sh`
## 测试结构
```
tests/
├── conftest.py # 根级 fixtures(内存 SQLite DB、TestClient)
├── unit/ # 数十个按领域划分的单测(engine/各规则、campaign_*、intelligent_eval_*、exploration_*、model_*、task_queue 等)
│ └── mock_channel.py # 测试用 MockChannel
└── integration/ # 数十个集成测试:每 router 一个 test_<area>_api.py(runs/campaigns/intelligent_evals/targets/...)+ 调度与端到端(test_*_scheduler*.py、test_intelligent_eval_e2e.py)
└── conftest.py # 集成级 fixtures
```
pytest 配置集中在 `pyproject.toml` 的 `[tool.pytest.ini_options]`:`pythonpath = ["backend", "."]`(测试直接 import `agenteval` 和 `cli`)、`asyncio_mode = "auto"`(async 测试无需显式装饰器)。
## 部署关键信息
- **t480**:`sola-t480`(192.168.8.145:8001),Docker Compose **双容器**(`agenteval` + `openclaw-eval`)
- **端口映射**:主机 8001 → 容器 8000(主机 8000 已被占用)
- **健康检查**:`curl http://192.168.8.145:8001/api/health` 返回 version/commit,脚本验证与本地 `pyproject.toml` 一致
- **回滚**:`docker tag t480-agenteval:<old-version> t480-agenteval:latest && docker compose up -d`
- **backend/ 通过 volume 热加载**,只有 Python 依赖变更才需要 `--build`
- **前端 dist/ 也通过 volume 挂载**,因此每次前端变更也需要重新构建(`./scripts/deploy-t480.sh` 自动处理)
- **openclaw-eval** 容器使用 `ghcr.io/openclaw/openclaw:latest` 镜像,通过 `AGENTEVAL_API_URL` 环境变量连接 agenteval
## 已知陷阱
1. **`index.css` 必须显式 import**:Vite 不会自动包含未被引用的 CSS。`main.tsx` 必须有 `import './index.css'`,否则全局样式(动画、布局覆盖、nav-only-tabs)全部无效且无报错。这是历史遗留 bug(项目创建以来就缺失),已修复但容易在重构时再次丢失。
2. **UTC 时间戳解析**:后端返回的 datetime 字符串不含时区标记(如 `"2026-07-14T16:40:00"`),JS 的 `new Date()` 会按本地时间解析导致偏差 8 小时。前端必须使用 `utils/date.ts` 的 `toDate()` 函数(检测并追加 `Z` 后缀),禁止直接 `new Date(str)`。根本修复需后端序列化时附带时区。
3. **Ant Design CSS-in-JS 覆盖**:Ant Design v5 的 emotion 运行时注入优先级高于静态 CSS 文件中的 `flex`/`height`/`overflow` 规则。需要弹性布局的容器避免依赖 `.ant-tabs-content` 等 Ant 内部 DOM,改用导航与内容分离方案。见上方前端架构说明。
4. **SQLite 连接池**:必须用 `StaticPool` + `check_same_thread=False`。使用默认连接池会导致多线程下连接耗尽(async + FastAPI 线程池混合场景)。
5. **`.env` 在 t480 上需手动维护**:部署脚本不会自动同步 `.env` 文件,新增环境变量需 SSH 到主机手动补全。容易导致 OpenClaw 代理等功能静默失效。
6. **智能评估可见性接缝**:逻辑删除(`deleted` 状态)语义只住在 `intelligent_eval/repository.py`——查询走 `IntelligentEvalRepository.visible()` 谓词,存在性校验走 `require_live_eval`(路由层翻译为 404)。禁止在其他模块直查 `IntelligentEvalDB` 判断删除态,否则删除语义会在多处漂移(Phase 1 收敛前的教训:快照、决策日志、任务监控各自实现了一遍"已删即 404")。
7. **AI 上下文文档结构**:`AGENTS.md` 是仓库唯一权威正文;`CLAUDE.md` / `AGENT.md` 仅是指针文件(引导工具读本文件),禁止往指针文件写入正文性内容——三份全量副本时代各自漂移的历史教训(各自持有独有段落、互不一致)勿重演。