将「已删即 404」语义收进 IntelligentEvalRepository 单一接缝,消除三处独立裁决; 任务监控开始隐藏已删评估的任务(本 Phase 唯一刻意行为变化)。 - repository.py 新增 visible() 谓词与 require_live_eval() 服务接缝; 六处裸谓词统一走它,get()/get_including_deleted() 语义不变。 - decision_logs.py 删除本地 _require_eval,三处调用迁至 repository 接缝; count_decisions 由 len(.all()) 改为 func.count。 - task_queue.py list_tasks 与 stats 过滤已删评估的任务(行为变化)。 - web/routers/intelligent_evals.py: _require_eval_exists → _require_live_eval, 把 LookupError 翻译为 404;expired 会话 Markdown 标注下沉至 read_model.report_markdown_by_eval;配置快照 11 字段序列化收至 config_snapshot.snapshot_to_dict 单一出口。 - AGENTS.md 登记可见性纪律(已知陷阱 #6)。 - 补 characterization 测试锁定四处契约;更新 task_queue 测试以使用 真实 eval_id(可见性过滤后字面 eval_id 不再可见)。
202 lines
12 KiB
Markdown
202 lines
12 KiB
Markdown
# AGENTS.md
|
||
|
||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||
|
||
## 项目概述
|
||
|
||
**AgentEvalTool** 是智能体质量评估工具集平台(v1.0.0),用于评估 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 → 消息通道(TutuApiChannel)
|
||
EvalRule → 评估规则(keyword_match / response_time / llm_score)
|
||
↓
|
||
SQLite Storage(Repository 模式)
|
||
```
|
||
|
||
### 关键抽象(均在 `backend/agenteval/`)
|
||
|
||
- **`channels/base.py`** — `EvalChannel` 抽象:`send()` / `poll_reply()` / `health_check()`,V1 实现为 `TutuApiChannel`(tutu-api HTTP + Bearer Token)
|
||
- **`evaluation/engine.py`** — `EvalEngine`:异步执行,遍历 Scenario.cases → messages → 调用 channel → 执行规则 → 写入 DB;通过 `asyncio.Event` cancel_token 支持真取消;`progress_callback` 实时推送事件给 WebSocket
|
||
- **`evaluation/rules/`** — `EvalRule` 抽象,三种实现:`keyword.py`(关键词匹配)、`response_time.py`(延迟阈值)、`llm_score.py`(LLM-as-judge)
|
||
- **`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_api_key()`(X-API-Key 鉴权)
|
||
|
||
**扩展性模式**:
|
||
- **规则注册表**(`evaluation/rules/__init__.py`):`@register_rule` 装饰器将规则类注册到 `_RULE_REGISTRY`,`get_rule(type, params)` 按名称实例化。添加新规则只需创建类并加装饰器。
|
||
- **通道工厂**(`channels/factory.py`):`ChannelFactory` 维护 `ChannelType → EvalChannel` 映射,`register()` 类方法支持插件式注册新通道类型。
|
||
|
||
### 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` 表示模型自身支持的文本、图像、音频和视频模态,三者禁止混用。厂商、区域、上下文窗口、最大输出和模型特性属于描述性元数据,会写入评测运行快照,但不直接改变网关请求参数。
|
||
|
||
### 前端
|
||
|
||
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 跑旧版
|
||
|
||
## 配置
|
||
|
||
- `.env`(参考 `.env.example`):`AGENTEVAL_API_KEY`(X-API-Key 鉴权)、`AGENTEVAL_ALLOWED_ORIGINS`(CORS,首个非 localhost URL 用作 OpenClaw WS Origin)、`AGENTEVAL_OPENCLAW_*` 系列(代理上游地址 + 认证 token)
|
||
- `config/config.json`:tutu-api 通道配置(敏感信息,只读挂载到容器)
|
||
- 无 CI/CD,部署依赖手动执行 `scripts/deploy-t480.sh`
|
||
|
||
## 测试结构
|
||
|
||
```
|
||
tests/
|
||
├── conftest.py # 共享 fixtures(内存 SQLite DB、TestClient)
|
||
├── unit/
|
||
│ ├── mock_channel.py # 测试用 MockChannel
|
||
│ ├── test_engine.py # EvalEngine 单元测试(asyncio_mode=auto)
|
||
│ ├── test_cascade.py # 级联删除测试
|
||
│ └── test_settings.py # Pydantic Settings 测试
|
||
└── integration/
|
||
└── test_runs_api.py # Runs API 集成测试
|
||
```
|
||
|
||
`pytest.ini_options` 中 `pythonpath = ["backend", "."]`,测试直接 import `agenteval` 和 `cli`。
|
||
|
||
## 部署关键信息
|
||
|
||
- **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")。
|