# EvalScope Benchmark Docker 镜像构建实录

> 本文档记录 `evalscope-complete-py312` Docker 镜像的完整搭建过程、关键参数、踩坑与解决方案，供后续复现和维护参考。

---

## 1. 目标

构建一个**开箱即用**的 Docker 镜像，满足：
- 基于 Python 3.12
- 内置 evalscope 源码及全部依赖
- 除 `swe_bench` 系列外，其他 benchmark 无需再安装任何东西即可运行
- 支持代码执行 sandbox（humaneval、bigcodebench）
- 支持 Agent benchmark（tau2_bench、general_fc）
- 支持 function calling 评测（bfcl_v3）
- 在国内网络环境下可构建

---

## 2. 基础镜像选择

```dockerfile
FROM python:3.12-slim-bookworm
```

### 2.1 这句话是什么意思？

`FROM` 是 Dockerfile 的第一条指令，意思是：**我要基于哪个镜像开始构建**。

`python:3.12-slim-bookworm` 可以拆成三部分：

| 部分 | 含义 |
|------|------|
| `python` | 镜像名称，官方 Python 镜像 |
| `3.12` | Python 版本号 |
| `slim-bookworm` | 镜像变体标签 |

`slim` 表示精简版，只保留最基础的东西，体积小。  
`bookworm` 是 Debian 12 的代号，这是 Linux 发行版的一个版本。  

所以整句意思是：**基于官方 Python 3.12 精简版（Debian 12）镜像来构建我们的环境**。

类比理解：
> 就像你要装修房子，`FROM` 就是你选择的一套毛坯房。`python:3.12-slim-bookworm` 就是一套已经通了水电（装了 Python 和 pip）、但还没放家具（没装 evalscope 依赖）的毛坯房。

### 2.2 为什么选择这个镜像？

- **Python 3.12**：项目主力环境
- **slim**：体积小，比完整版少几百 MB
- **bookworm**：Debian 12 稳定，apt 包源丰富
- **已经包含 pip**：不需要自己装 Python

---

## 3. 关键构建参数

| 参数 | 值 | 说明 |
|------|-----|------|
| `DEBIAN_FRONTEND=noninteractive` | 环境变量 | 避免 apt 交互式提示 |
| `PYTHONUNBUFFERED=1` | 环境变量 | Python 输出不缓冲 |
| `PIP_NO_CACHE_DIR=1` | 环境变量 | 不保留 pip 缓存，减小镜像 |
| `PYTHONDONTWRITEBYTECODE=1` | 环境变量 | 不生成 .pyc |
| `PYTHONPATH=/opt/evalscope/evalscope` | 环境变量 | 解决 editable install 加载问题 |
| apt 源 | 清华镜像 | 国内加速 |
| pip 源 | 清华镜像 | 国内加速 |
| 基础包 | git/wget/curl/build-essential 等 | 编译依赖 |
| docker.io | apt 安装 | 容器内支持套 Docker 跑 sandbox |

---

## 4. Docker 命令参数详解（新手向）

如果你是 Docker 新手，先把下面几个概念和参数搞懂，后续命令就不难了。

### 4.1 核心概念

- **镜像（Image）**：一个只读的模板，相当于一个打包好的环境。比如 `evalscope-complete-py312:latest`。
- **容器（Container）**：镜像运行起来的实例。你可以把镜像理解为 Class，容器是 Object。
- **宿主机（Host）**：运行 Docker 的那台物理机/虚拟机。
- **容器内（Container）**：Docker 容器里面的环境。

### 4.2 Dockerfile 常用指令

本项目 `Dockerfile.py312` 里用到的指令：

| 指令 | 作用 | 示例 |
|------|------|------|
| `FROM` | 基础镜像 | `FROM python:3.12-slim-bookworm` |
| `ENV` | 设置环境变量 | `ENV PYTHONPATH=/opt/evalscope/evalscope` |
| `RUN` | 执行命令（每行会产生一层镜像） | `RUN pip install ...` |
| `COPY` | 把宿主机文件复制到镜像里 | `COPY bash/ /opt/evalscope/bash/` |
| `WORKDIR` | 设置工作目录 | `WORKDIR /opt/evalscope` |
| `CMD` | 容器启动时默认执行的命令 | `CMD ["/bin/bash"]` |

### 4.3 `docker build` 参数

```bash
docker build -f Dockerfile.py312 -t evalscope-complete-py312:latest .
```

| 参数 | 含义 |
|------|------|
| `-f Dockerfile.py312` | 指定用哪个 Dockerfile（默认是当前目录的 `Dockerfile`） |
| `-t evalscope-complete-py312:latest` | 给构建好的镜像打标签，`name:tag` 格式 |
| `.` | 构建上下文路径，Docker 会把这个目录下的文件传给构建进程 |

### 4.4 `docker run` 参数

这是用得最多的命令：

```bash
docker run -it --rm \
  --network host \
  -v /data1/sora/evalscope/datasets:/opt/evalscope/datasets \
  -v /data1/sora/evalscope/output:/opt/evalscope/output \
  -v /var/run/docker.sock:/var/run/docker.sock \
  evalscope-complete-py312:latest \
  bash -c "cd /opt/evalscope && python bash/run_lite.py ..."
```

| 参数 | 含义 |
|------|------|
| `docker run` | 创建并启动一个容器 |
| `-i` | 交互模式，保持 STDIN 打开 |
| `-t` | 分配一个伪终端，让你能看到彩色输出 |
| `-it` | 上面两个一起用，几乎必加 |
| `--rm` | 容器停止后自动删除，避免垃圾容器堆积 |
| `--network host` | 容器和宿主机共用网络，`localhost` 指向宿主机 |
| `-v 宿主机路径:容器内路径` | 挂载目录/文件，容器内的改动会反映到宿主机 |
| `-e KEY=VALUE` | 设置容器内环境变量 |
| `evalscope-complete-py312:latest` | 要运行的镜像名 |
| `bash -c "..."` | 容器启动后执行的命令 |

**挂载参数 `-v` 是本项目的核心**：

```bash
-v /data1/sora/evalscope/datasets:/opt/evalscope/datasets
```

意思是：把宿主机的 `/data1/sora/evalscope/datasets` 映射到容器内的 `/opt/evalscope/datasets`。这样容器就能读取宿主机上的数据，评测结果也能写回宿主机。

### 4.5 其他常用命令

```bash
# 查看本地镜像
docker images

# 查看运行中的容器
docker ps

# 查看所有容器（包括停止的）
docker ps -a

# 删除镜像
docker rmi evalscope-complete-py312:latest

# 删除容器
docker rm 容器ID

# 进入正在运行的容器
docker exec -it 容器ID /bin/bash

# 加载 tar.gz 镜像
docker load -i evalscope-complete-py312.tar.gz

# 导出镜像为 tar.gz
docker save -o evalscope-complete-py312.tar.gz evalscope-complete-py312:latest
```

### 4.6 为什么这个项目不需要 `--gpus all`

很多 Docker + GPU 的教程会写 `--gpus all`，但**本项目不需要**。

原因是：
- evalscope 容器本身只做 API 调用，不跑模型推理。
- 模型服务（sglang）是在宿主机上启动的。
- 容器只需要通过网络访问 `http://localhost:30000/v1`。

所以用 `--network host` 就够了，不需要把 GPU 分配给容器。

---

## 5. 构建步骤

### 5.1 准备构建上下文

#### `tools/docker/` 这个文件夹里都要放什么？

```
tools/docker/
├── Dockerfile.py312      # 构建配方
├── evalscope/            # evalscope 源码（从项目根目录同步过来）
├── bash/                 # bash 脚本（从项目根目录同步过来）
└── tau2-bench/           # tau2-bench 源码（从项目根目录同步过来）
```

**为什么需要同步？**  
因为 Docker 构建时只能访问 `tools/docker/` 这个目录下的文件（这叫**构建上下文**）。而项目的源码在 `/data1/sora/evalscope/` 根目录下，所以需要先把需要的部分复制到 `tools/docker/` 里。

#### `rsync` 命令是什么意思？

```bash
rsync -av --delete --exclude='*.log' --exclude='__pycache__' \
  bash/ tools/docker/bash/
```

拆开看：

| 参数 | 含义 |
|------|------|
| `rsync` | 文件同步工具 |
| `-a` | 归档模式，保留文件权限、时间等 |
| `-v` | 显示同步了哪些文件 |
| `--delete` | 删除目标目录中源目录没有的文件，保持完全一致 |
| `--exclude='*.log'` | 不同步 `.log` 日志文件 |
| `--exclude='__pycache__'` | 不同步 Python 缓存目录 |
| `bash/` | 源目录（项目根目录的 bash 脚本） |
| `tools/docker/bash/` | 目标目录（构建上下文中的位置） |

**用人话讲**：把 `bash/` 里的文件同步到 `tools/docker/bash/`，去掉日志和缓存，并且保证两边完全一样。

#### 为什么用 rsync 而不用 `cp -r`？

因为 `cp -r` 只是简单复制，不会删除目标目录里多余的旧文件。如果用 `cp -r`，以前删除的脚本可能还会留在 `tools/docker/bash/` 里，最终被打包进镜像。

### 5.2 构建镜像

```bash
cd /data1/sora/evalscope/tools/docker
docker build -f Dockerfile.py312 -t evalscope-complete-py312:latest .
```

### 5.3 环境是怎么装进 Docker 里的？

很多新手会问：Python 包是怎么装到镜像里的？答案是：**Dockerfile 里的 `RUN pip install ...` 命令会在构建时执行，把包装到镜像里**。

整个流程可以概括为：

```
毛坯房（python:3.12-slim-bookworm）
    ↓
装修第一层：换国内源、装系统工具（apt）
    ↓
装修第二层：RUN pip install ... 装 Python 包
    ↓
装修第三层：COPY 项目源码进镜像
    ↓
装修第四层：把源码用 pip install -e 安装好
    ↓
装修第五层：设置 PYTHONPATH、权限等收尾工作
    ↓
完工（evalscope-complete-py312:latest 镜像）
```

关键步骤对应 Dockerfile 里的这些行：

| Dockerfile 行 | 作用 |
|---------------|------|
| `FROM python:3.12-slim-bookworm` | 选毛坯房 |
| `RUN apt-get update && apt-get install ...` | 装系统工具 |
| `RUN pip install openai pandas ...` | 装 Python 包 |
| `COPY evalscope/ /opt/evalscope/evalscope/` | 复制源码 |
| `RUN pip install -e /opt/evalscope/evalscope/` | 安装 evalscope 本身 |
| `ENV PYTHONPATH=/opt/evalscope/evalscope` | 设置环境变量 |

**`RUN` 和 `COPY` 的区别**：
- `RUN`：执行命令，比如安装软件
- `COPY`：复制文件，比如把源码复制进去

**为什么先 `pip install` 再 `COPY` 源码？**  
因为 Docker 有缓存机制。如果依赖没有变化，只有源码变化，Docker 会直接使用之前装好的依赖层，只重新构建 COPY 和之后的层，节省大量时间。

### 5.4 导出 tar.gz

```bash
docker save -o evalscope-complete-py312.tar.gz evalscope-complete-py312:latest
md5sum evalscope-complete-py312.tar.gz > evalscope-complete-py312.tar.gz.md5
```

---

## 6. 安装的关键依赖

### 6.1 evalscope 核心依赖

```bash
pip install \
    openai pandas numpy pyyaml requests tqdm \
    tiktoken transformers \
    scikit-learn matplotlib seaborn plotly \
    jieba nltk rouge-score sacrebleu \
    sympy latex2sympy2_extended pillow \
    docker pexpect pytest \
    tabulate rich jsonlines jsonschema \
    langdetect word2number zhconv \
    modelscope pydantic overrides \
    more_itertools pylatexenc \
    rouge-chinese markdown \
    editdistance dotenv docstring_parser \
    colorlog
```

### 6.2 Sandbox 支持

```bash
pip install evalscope[sandbox] 2>/dev/null || pip install ms-sandbox 2>/dev/null || true
```

`ms-sandbox` 是 ModelScope 的 sandbox 实现，用于 humaneval、bigcodebench 的隔离代码执行。

### 6.3 terminal_bench 依赖

```bash
pip install "harbor>=0.8.0,<1.0.0"
```

### 6.4 tau2-bench 依赖

```bash
pip install \
    fastapi uvicorn psutil loguru \
    litellm tenacity deepdiff addict toml

# 同时把本地 tau2-bench 源码 editable 安装
pip install -e /opt/evalscope/tools/tau2-bench/ --no-deps
```

### 6.5 bigcodebench 评估依赖

```bash
pip install \
    tree-sitter tree-sitter-python tempdir termcolor wget gradio-client
```

### 6.6 bfcl_v3 依赖

```bash
pip install --no-deps bfcl-eval==2025.10.27.1
pip install \
    anthropic cohere==5.18.0 datamodel-code-generator==0.25.7 \
    faiss-cpu==1.11.0 google-genai==1.24.0 mistralai==1.7.0 \
    networkx==3.3 numpy==1.26.4 google-search-results \
    rank_bm25 html2text boto3 qwen-agent writer-sdk \
    tree-sitter tree-sitter-python tree-sitter-javascript tree-sitter-java
```

注意：`bfcl-eval` 默认会装 torch，这里用 `--no-deps` 避免把 torch 拉进来，再手动补缺少的依赖。

### 6.7 其他工具

```bash
pip install soundfile openpyxl
```

---

## 7. 踩坑记录

### 7.1 editable install 加载失败

**现象**：
```
ImportError: cannot import name 'run_task' from 'evalscope' (unknown location)
```

**原因**：evalscope 用 `pip install -e /opt/evalscope/evalscope/` 安装后，editable finder 有时不能正确加载 `__init__.py`。

**解决**：在 Dockerfile 末尾设置 `ENV PYTHONPATH=/opt/evalscope/evalscope`。

### 7.2 镜像里缺少新脚本

**现象**：其他机器拉下来跑 `run_group2.py` 报 `No such file or directory`。

**原因**：Docker 镜像构建后，项目根目录又新增了 `run_group1/2/3.py`、`run_1.py` 等文件，但镜像没有重新构建。

**解决**：每次修改 bash/ 目录后，必须重新同步到 `tools/docker/bash/`，然后重新 `docker build` 和 `docker save`。

### 7.3 镜像体积过大

**现象**：安装 bfcl-eval 默认会拉 torch，镜像暴涨几个 GB。

**解决**：
```bash
pip install --no-deps bfcl-eval==2025.10.27.1
```
再手动安装缺少的非 torch 依赖。

### 7.4 litellm 版本冲突

**现象**：tau2-bench 要求 `litellm>=1.80.15,<1.82.7`，但 bfcl-eval 可能装更高版本。

**解决**：在 Dockerfile 中显式约束：
```bash
pip install "litellm>=1.80.15,<1.82.7" "tenacity>=9.0.0"
```

### 7.5 国内网络下载慢/超时

**现象**：pip、apt、git clone 都很慢。

**解决**：
- apt 换清华源
- pip 换清华源
- git+https 安装 tau2-bench 时容易失败，改为本地 COPY 源码后 editable 安装

### 7.6 ModelScope 上传 API 参数错误

**现象**：
```
TypeError: HubApi.upload_file() got an unexpected keyword argument 'model_id'
```

**原因**：新版 ModelScope SDK 参数名是 `repo_id`，不是 `model_id`。

**解决**：
```python
api.upload_file(
    repo_id='SoraAmami/evalscope-docker',
    path_or_fileobj='evalscope-complete-py312.tar.gz',
    path_in_repo='evalscope-complete-py312.tar.gz'
)
```

### 7.7 数据集路径重复

**现象**：`/data1/sora/evalscope/datasets/datasets/` 下又有一层数据。

**原因**：evalscope 会在 `dataset_dir` 后自动拼 `datasets/`。如果把 `dataset_dir` 指向了已经包含 `datasets/` 的路径，就会重复。

**解决**：始终让 `dataset_dir` 指向 `datasets/` 的父目录。

### 7.8 运行时缺少 tokenizer

**现象**：`transformers` 提示 `PyTorch was not found`。

**原因**：镜像里没有 torch（ intentional，为了减小体积）。

**影响**：evalscope 的 tokenizer 加载通常只需要 `transformers` 的 tokenizer 部分，实际运行时通过 API 调用模型，不需要本地 PyTorch。该警告可忽略。

---

## 8. 镜像验证

构建完成后，验证镜像内关键组件：

```bash
docker run --rm evalscope-complete-py312:latest bash -c "
  python -c 'import evalscope; print(evalscope.__file__)' && \
  python -c 'import evalscope.api.agent' && \
  python -c 'import docker' && \
  python -c 'import harbor' && \
  python -c 'import tau2' && \
  python -c 'import bfcl_eval' && \
  python -c 'import soundfile' && \
  ls -la /opt/evalscope/bash/run_1.py /opt/evalscope/bash/run_group*.py
"
```

---

## 9. 三个评测版本

为了兼顾快速验证和完整评测，提供三个版本：

| 版本 | 预计时间 | 用途 |
|------|----------|------|
| **Lite** | ~2-4h | 快速冒烟，每个能力域 1-2 个 benchmark |
| **Standard** | ~20-28h | 常规能力评测，覆盖主要 benchmark |
| **Full** | ~3-5 天 | 完整评测，全部 benchmark + 多次采样 |

每个版本都覆盖 5 大能力域：代码生成、推理/数学、知识、长上下文、智能体/工具。

### 9.1 Lite 版

- 代码：`humaneval`
- 推理/数学：`aime24`、`gsm8k`
- 知识：`mmlu_pro`、`simple_qa`
- 长上下文：`longbench_v2`
- 智能体/工具：`bfcl_v3`

运行：
```bash
python bash/run_lite.py --model YourModel --api-url http://localhost:30000/v1 --limit 20
```

### 9.2 Standard 版

- 代码：`humaneval`、`live_code_bench`、`bigcodebench`
- 推理/数学：`aime24`、`aime25`、`aime26`、`hmmt26`、`gsm8k`、`competition_math`、`bbh`、`drop`
- 知识：`gpqa_diamond`、`mmlu_pro`、`simple_qa`、`super_gpqa`、`mmlu`、`cmmlu`、`arc`、`hellaswag`、`trivia_qa`、`winogrande`
- 长上下文：`longbench_v2`、`openai_mrcr`
- 智能体/工具：`tau2_bench`、`general_fc`、`bfcl_v3`

运行：
```bash
python bash/run_standard.py --model YourModel --api-url http://localhost:30000/v1 --limit 100
```

### 9.3 Full 版

覆盖 `run.py` 中全部 benchmark，使用完整数据集和多次采样配置。

运行：
```bash
python bash/run.py --model YourModel --api-url http://localhost:30000/v1 --limit none
```

---

## 10. 文件清单

```
tools/docker/
├── Dockerfile.py312                  # 镜像构建定义
├── evalscope-complete-py312.tar.gz   # 导出的镜像
├── evalscope-complete-py312.tar.gz.md5
├── deploy.sh                         # 目标机器部署脚本
├── export_image.sh                   # 导出脚本
├── build_*.log                       # 构建日志
└── README.md                         # 使用说明
```

---

## 11. 重新构建流程

```bash
cd /data1/sora/evalscope

# 1. 同步最新代码
rsync -av --delete bash/ tools/docker/bash/
rsync -av --delete evalscope/ tools/docker/evalscope/
rsync -av --delete tau2-bench/ tools/docker/tau2-bench/

# 2. 构建
cd tools/docker
docker build -f Dockerfile.py312 -t evalscope-complete-py312:latest .

# 3. 导出
docker save -o evalscope-complete-py312.tar.gz evalscope-complete-py312:latest
md5sum evalscope-complete-py312.tar.gz > evalscope-complete-py312.tar.gz.md5

# 4. 上传到 ModelScope（可选）
python3 -c "
from modelscope.hub.api import HubApi
api = HubApi()
api.login('ms-3d554a39-6e07-496d-8022-0b0ee64a6389')
api.upload_file(
    repo_id='SoraAmami/evalscope-docker',
    path_or_fileobj='evalscope-complete-py312.tar.gz',
    path_in_repo='evalscope-complete-py312.tar.gz'
)
"
```
