# 子设计 4：自动运行部署与结果展示

**Date:** 2026-04-20
**Status:** Draft for discussion

---

## Summary

这个子设计回答两个技术落地问题：

1. evaluation daily run 应该通过什么部署方式自动执行
2. evaluation 结果应该在系统的什么位置对外展示

结论先行：

- **推荐部署方式：Kubernetes CronJob + 独立 evaluation runner 进程**
- **推荐展示入口：Investibook 后台管理页 `/admin/evaluation-runs` 及相关详情页**

不推荐把新的 evaluation daily run 继续做成“挂在 API 进程里的常驻 scheduler”，因为新的 evaluation 系统比现有轻量 `/api/admin/evaluations*` 更重、更长、更需要可观测性与重试控制。

---

## Why not in-process scheduler

当前代码库里已经有一个轻量的 in-process scheduler：

- `backend/investibook/core/scheduler.py`
- 由 `backend/investibook/main.py` 在 API 进程启动时按环境变量启用

这套机制适合：

- 轻量周期任务
- 单实例部署
- 容忍 pod 重启导致的任务漂移

但不适合新的 evaluation daily run，原因是：

- API pod 承担在线请求，不应再承载长时间 evaluation 批任务
- 多副本部署下，in-process scheduler 容易产生重复触发问题
- 发布/滚动升级时，scheduler 生命周期不稳定
- 任务重试、补跑、并发控制、资源隔离都不够清晰

因此，新 evaluation 系统应与现有 API 进程解耦。

---

## Recommended deployment model

推荐使用：

- **Kubernetes CronJob**
- 运行 **独立 evaluation runner**
- 与后端使用同一镜像、同一配置来源、同一 `DATA_DIR`

推荐形态：

```text
EKS / K8s
  ├── backend Deployment
  │     └── 负责 API / SSE / 交互式请求
  ├── frontend Deployment
  │     └── 负责页面展示
  └── evaluation CronJob
        └── 每天触发一次 runner，写入 evaluation tables
```

---

## Runner invocation

推荐不要让 CronJob 直接 curl 一个 API，而是运行一个明确的后端管理入口。

建议后续新增：

```text
python -m investibook.scripts.run_eval_suite --suite core-daily --mode scheduled
```

或等价的 `uv run` 命令。

这样做的好处：

- 不依赖 HTTP 自调用
- 更容易拿到明确退出码
- 更适合 K8s Job 重试语义
- 更容易复用现有 backend 代码、DB、配置和日志

### Runner responsibilities

这个 runner 应负责：

- 加载 suite 配置
- 生成一个 `eval_run`
- 逐个执行 case
- 控制 semantic judge 预算
- 写入 `eval_case_run`、`eval_judgment`、`eval_regression`
- 输出 run summary
- 非零退出码表示整批任务失败

---

## K8s deployment shape

### Container image

推荐直接复用 backend 镜像，而不是单独维护第二套镜像。

原因：

- 代码路径一致
- DB schema 和 config 读取逻辑一致
- 减少部署工件数量

### Shared configuration

CronJob 需要与 backend Deployment 对齐以下配置：

- `DATA_DIR`
- 模型 / judge 相关环境变量
- 访问外部系统的凭据
- 评测环境 profile

### Shared storage

如果 evaluation 结果继续写入当前 backend 使用的 SQLite / 数据目录，则 CronJob 必须挂载与 backend 相同的 PVC。

这要求：

- CronJob 与 backend 使用相同 `DATA_DIR`
- Job 生命周期内可以安全访问 DB
- 写入模式兼容 SQLite WAL

v1 可以直接复用现有数据层；后续若迁移到独立数据库，再把这个约束抽掉。

---

## Concurrency and idempotency

CronJob 方案下必须明确控制：

### 1. Single active run

同一 suite 在同一时刻只允许一个 active run。

建议：

- 启动前查询 `eval_run` 是否已有 `running`
- 若存在，则本次直接退出或标记 skipped

### 2. Idempotent scheduled run key

建议每次定时任务生成：

- `schedule_date`
- `suite_id`
- `run_reason = scheduled`

并形成唯一键，避免同一天重复创建 run。

### 3. Retry semantics

推荐使用：

- K8s Job 级别重试，只针对 runner 级失败
- case 级失败写入 DB，不依赖 Job 重试重做整批

也就是说：

- 单个 case fail 不应让整个 Job 崩溃
- 只有 runner 初始化失败、DB 不可用、配置错误等才返回非零退出码

---

## Why not call admin API directly

也可以做成：

- CronJob 通过内部网络调用 `/api/admin/evaluation-runs/run`

但这不是推荐方案，原因是：

- API 路径更适合手工触发和页面操作
- HTTP 调用在 Job 层的退出语义较弱
- 长任务放在 API handler 之后会让控制链路更复杂
- 调试时容易变成“API 创建后台任务 + Job 退出”，不利于任务可观测性

推荐做法是：

- **CronJob 跑 runner**
- **API 提供手工触发与查询**

两者共享同一套 service / runner 代码。

---

## Result surface recommendation

推荐把 evaluation 结果展示在现有后台管理体系中，而不是另起一个外部系统。

推荐新增页面：

- `/admin/evaluation-runs`
  run 列表页
- `/admin/evaluation-runs/[runId]`
  单次 run 详情页
- `/admin/evaluation-cases/[caseId]`
  case 历史趋势与基线对比页
- `/admin/evaluation-regressions`
  当前回归聚合页

这是对现有：

- `/admin/evaluations`
- `/admin/analytics`

的补充，而不是替代。

### Why admin UI

因为 evaluation 系统的主要受众是：

- 开发者
- oncall / 平台维护者
- prompt / skill / tool adapter 维护者

它不是终端用户能力，因此放在 admin 区最合适。

---

## Suggested page responsibilities

### 1. Run list page

展示：

- 每天 run 状态
- suite
- revision
- baseline
- pass rate
- regression count
- token cost
- started_at / finished_at

### 2. Run detail page

展示：

- run summary
- fail cases
- regression cases
- token breakdown
- failure taxonomy breakdown
- rerun failed cases 按钮

### 3. Case history page

展示：

- 某个 case 近期 run 结果趋势
- baseline 对比
- failure codes 变化
- semantic score 历史

### 4. Regression page

展示：

- 当前所有 active regressions
- 按严重程度排序
- 按 failure taxonomy 聚合
- 跳转到 case / run 详情

---

## API surface recommendation

建议新增一组 admin API：

- `GET /api/admin/evaluation-runs`
- `GET /api/admin/evaluation-runs/{run_id}`
- `POST /api/admin/evaluation-runs/run`
- `GET /api/admin/evaluation-runs/status`
- `GET /api/admin/evaluation-cases/{case_id}/history`
- `GET /api/admin/evaluation-regressions`
- `POST /api/admin/evaluation-runs/{run_id}/rerun-failed`
- `POST /api/admin/evaluation-baselines/promote`

职责区分：

- API 用于页面查询和人工触发
- CronJob 不直接依赖这些 API 作为主执行入口

---

## Observability

为了让 daily run 可运维，至少需要三层观测：

### 1. Runner logs

CronJob pod 日志记录：

- run_id
- suite_id
- started / finished
- case progress
- token summary
- failures

### 2. DB run summary

前端和 API 查询的标准来源：

- `eval_run`
- `eval_case_run`
- `eval_regression`

### 3. Optional metrics

后续可增加 Prometheus 指标：

- daily run duration
- active run count
- regression count
- token cost per run
- judge cache hit rate

---

## V1 recommendation

第一阶段建议直接采用：

- **K8s CronJob**
- **复用 backend 镜像**
- **新增 runner 命令**
- **结果写入现有 backend DB**
- **前端展示在 `/admin` 下的新 evaluation 页面**

这是对现有 repo 侵入最小、和现有部署方式最一致、也最容易长期维护的方案。

---

## Defaults

- 自动运行采用 `K8s CronJob`
- CronJob 调用独立 runner，而不是依赖 API 进程内 scheduler
- 结果展示放在 `/admin` 下
- v1 继续复用 backend 镜像、配置和 `DATA_DIR`
