# 数学任务（Rule Judge） — Agentic RL 最佳实践

> **任务类型**: 数学推理（竞赛数学）  
> **基座模型**: Qwen3.5-9B  
> **训练任务 ID**: `ft-202605131703-f963`  
> **训练任务名**: `cangyu_qwen3.5-9b_hendrycks_math_full_python_interpreter_only_new_16k_24gpus_lv45_trace_test-96e2afe2`  
> **Rollout 处理器**: `CalcXRolloutProcessor`（MCP + LangGraph）  
> **Reward 处理器**: `MathRewardProcessor`（format 0.2 + accuracy 0.8）

---

## 1. 数据来源与准备

### 1.1 数据集简介

[Hendrycks MATH](https://huggingface.co/datasets/EleutherAI/hendrycks_math) 是由 Dan Hendrycks 等人发布的竞赛数学题库，涵盖 7 个子领域：

| 子领域 | 说明 |
|--------|------|
| Algebra | 代数 |
| Counting & Probability | 计数与概率 |
| Geometry | 几何 |
| Intermediate Algebra | 中级代数 |
| Number Theory | 数论 |
| Prealgebra | 初等代数 |
| Precalculus | 微积分预备 |

每道题均带有标准答案（以 `\boxed{}` 格式包裹），题目按难度分为 Level 1-5。

### 1.2 数据文件

本 demo 已将训练和验证数据放入 `data/` 目录，可直接使用：

- `data/hendrycks_math_train_lv45.jsonl` — 训练集（约 1.7 MB，Level 4-5）
- `data/hendrycks_math_test_lv45.jsonl` — 验证集（约 1.0 MB，Level 4-5）

本 demo 使用 **Level 4-5 的中高难度题目**——低难度题（Level 1-3）模型基线已能解决，正确率高导致 reward 区分度低，用高难度题可以获得更有效的训练信号。

运行 `submit_job.py` 时，提交脚本会先检查脚本同级 `data/` 下是否存在上述两个目标文件；如果都存在则跳过下载，如果缺失则自动运行 `scripts/download_hendrycks_math.py` 生成后再继续提交。

### 1.3 数据生成方式（可选）

如需从头生成数据，可手动运行 [`scripts/download_hendrycks_math.py`](scripts/download_hendrycks_math.py)（`main()` [L87](scripts/download_hendrycks_math.py#L87)）：

```bash
python scripts/download_hendrycks_math.py
```

脚本会从 HuggingFace 下载 `EleutherAI/hendrycks_math` 全部 7 个子集，提取 `problem` 和 `\boxed{}` 答案，并同时输出完整训练/测试集和提交脚本需要的 Level 4-5 文件：

- `data/hendrycks_math_train.jsonl`
- `data/hendrycks_math_test.jsonl`
- `data/hendrycks_math_train_lv45.jsonl`
- `data/hendrycks_math_test_lv45.jsonl`

### 1.4 JSONL 数据 Schema

每行一个 JSON 对象：

```json
{
  "messages": [
    {"content": "Find the sum of all positive integers n such that ...", "role": "user"}
  ],
  "rollout_extra": {
    "solution": "\\frac{1}{2}",
    "dataset": "hendrycks_math",
    "level": "Level 5",
    "type": "Algebra"
  },
  "ground_truth": "\\frac{1}{2}"
}
```

| 字段 | 说明 |
|------|------|
| `messages` | 对话格式，仅包含一条 user 消息（题目） |
| `rollout_extra.solution` | 标准答案（从 `\boxed{}` 中提取的纯文本） |
| `rollout_extra.dataset` | 数据集标识，用于 reward 中选择验证策略 |
| `rollout_extra.level` | 题目难度级别 |
| `rollout_extra.type` | 数学子领域 |
| `ground_truth` | 标准答案副本，用于 reward 校验入口 |

---

## 2. 训练参数配置

> 配置文件：[`submit_job.py`](submit_job.py)

### 2.1 核心超参数

> 源码位置：[`submit_job.py` L197-215](submit_job.py#L197-L215)

```python
hyper_parameters={
    "n_epochs": 1,
    "learning_rate": 1e-6,
    "max_prompt_length": 8192,
    "max_length": 16384,
    "batch_size": 128,
    "eval_steps": 2,
    "nnodes_training": 2,
    "nnodes_rollout": 1,
    "save_strategy": "steps",
    "save_steps": save_steps // 2,
    "save_total_limit": 2,
    "lr_scheduler_type": "linear",
}
```

### 2.2 参数说明与选择理由

| 参数 | 值 | 说明 |
|------|-----|------|
| `n_epochs` | 1 | 训练集较大（~3500 样本），1 epoch 已足够；RL 场景过多 epoch 容易过拟合 |
| `learning_rate` | 1e-6 | RL 训练推荐用较小学习率，避免策略崩溃 |
| `lr_scheduler_type` | linear | 线性衰减，lr 从 1e-6 均匀降至 0 |
| `max_prompt_length` | 8192 | prompt 含 system prompt + 题目 + 多轮工具调用历史 |
| `max_length` | 16384 | prompt + response 总长度上限，需容纳多轮工具交互 |
| `batch_size` | 128 | 每批采样的 prompt 数；越大方差越小但更费算力 |
| `eval_steps` | 2 | 每 2 步在验证集上评估一次 |
| `save_steps` | `save_steps // 2` | 每半个 epoch 保存一次 checkpoint |
| `save_total_limit` | 2 | 最多保留 2 个 checkpoint，节省磁盘 |
| `nnodes_training` | 2 | 训练节点数（2 节点 × 8 GPU = 16 GPU） |
| `nnodes_rollout` | 1 | 推理节点数（1 节点 × 8 GPU = 8 GPU） |

### 2.3 资源配置

> 源码位置：[`submit_job.py` L224-228](submit_job.py#L224-L228)

```python
resource_config={
    "charge_type": "mtu_postpaid",   # 按量后付费
    "mtu_spec_code": "MTU4",         # GPU 规格
    "mtu_capacity": 24,              # 算力单元数
}
```

### 2.4 函数组件运行时

> 源码位置：[`submit_job.py` L107-124](submit_job.py#L107-L124)

Rollout 和 Reward 的运行时配置相近：

```python
rollout_runtime = {
    "cpu": 2,
    "memory_size": 4096,           # 4GB 内存
    "disk_size": 10240,            # 10GB 磁盘
    "concurrency": 30,             # 每实例最大并发数
    "capacity": 30,                # 初始实例数
    "min_capacity": 30,
    "max_capacity": 60,            # 弹性伸缩上限
    "memory_scale_threshold": 0.6, # 内存利用率超 60% 触发扩容
    "concurrency_scale_threshold": 0.6,
    "env": {"ENABLE_TRAJECTORY": "true"},  # 记录完整对话轨迹
}
```

---

## 3. 工具设计配置与跟踪

### 3.1 架构总览

```mermaid
flowchart LR
    subgraph RolloutProcessor["CalcXRolloutProcessor"]
        LLM["策略模型<br/>Qwen3.5-9B"]
        Agent["LangGraph<br/>Agent 节点"]
        Tools["LangGraph<br/>Tools 节点"]
    end

    subgraph MCPServer["MCP Server :10086"]
        PI["python_interpreter"]
        CI["code_interpreter.py<br/>execute_python()"]
    end

    Agent -->|"tool_calls"| Tools
    Tools -->|"SSE"| PI
    PI --> CI
    CI -->|"stdout+stderr"| Tools
    Tools -->|"tool results"| Agent
    Agent -->|"final answer"| Output["RolloutOutput"]
```

### 3.2 工具：python_interpreter

> MCP 注册：[`functions/rollout/math_rollout.py` L70-87](functions/rollout/math_rollout.py#L70-L87)

**注册方式**：通过 MCP (Model Context Protocol) Server 暴露，使用 SSE 传输协议，端口 `10086`。

**底层执行器**：[`functions/rollout/code_interpreter.py` L106](functions/rollout/code_interpreter.py#L106) 中的 `execute_python()` 函数。

**执行环境**：
- 通过 `subprocess.run` 在独立子进程中执行代码（进程隔离，崩溃不影响主进程）
- 硬超时 30 秒，输出上限 8000 字符（超出截断）

**预装包**（通过 `_CODE_PREAMBLE` 自动注入，[L26-38](functions/rollout/code_interpreter.py#L26-L38)）：

| 类别 | 模块 |
|------|------|
| 数学/科学 | `math`, `cmath`, `decimal`, `fractions`, `statistics`, `random`, `numpy`（as `np`） |
| 容器/算法 | `collections`, `itertools`, `functools`, `operator`, `bisect`, `heapq` |
| 字符串/IO | `string`, `re`, `json`, `io`, `sys`, `os`, `copy`, `typing` |
| 常用符号（from math） | `sqrt`, `pi`, `e`, `inf`, `log`, `exp`, `sin`, `cos`, `tan`, `ceil`, `floor`, `factorial`, `gcd`, `comb`, `perm` 等 |

**不可用**：`scipy`、`sympy`、`pandas` —— 不预装这些库，引导模型用 numpy 和基础运算自行推导。

**前处理**（代码执行前）：
1. **Markdown 围栏剥离**（[`_strip_markdown_fences` L46](functions/rollout/code_interpreter.py#L46)）：LLM 有时会用 ` ```python ``` ` 包裹代码，执行前自动去除
2. **末尾裸表达式自动 print**（[`_auto_print_last_expr` L67](functions/rollout/code_interpreter.py#L67)）：若代码最后一条语句是裸表达式（如 `1+1`），自动包装为 `print(repr(...))`，模拟 REPL 行为，确保计算结果能返回给模型

**后处理**（执行结果返回前）：
- stdout 超过 8000 字符时截断并附加 `...(output truncated)...`
- stderr 非空时追加为 `[stderr]` 区段
- 无任何输出时返回 `(no output)`

### 3.3 System Prompt

> 源码位置：[`functions/rollout/math_rollout.py` L114-122](functions/rollout/math_rollout.py#L114-L122)（`DEFAULT_SYSTEM_PROMPT`）

```
You are a math assistant. You have one tool:
- `python_interpreter`: run a Python script (numpy available as np;
  no scipy/sympy) for computation, equation solving, enumeration,
  modular arithmetic, etc.

Always verify your calculations by calling the tool instead of computing
in your head. Think step by step, then put your final answer in \boxed{}.
```

**设计要点**：
- 明确告知可用工具及其能力范围
- 强调"用工具验证而非心算"——鼓励模型调用工具
- 要求使用 `\boxed{}` 格式输出——与 reward 中的 format 奖励对齐

### 3.4 LangGraph 状态机

> 源码位置：[`functions/rollout/math_rollout.py` L234-243](functions/rollout/math_rollout.py#L234-L243)（`_build_graph`）

```mermaid
stateDiagram-v2
    [*] --> Agent
    Agent --> Tools: 有 tool_calls
    Agent --> [*]: 无 tool_calls（最终回答）
    Tools --> Agent: 返回工具结果
```

关键参数：
- `max_turns`: 10（默认），通过 `recursion_limit = max_turns * 2 + 5` 控制
- 每次工具调用记录 `tool_call_durations` 用于性能追踪

### 3.5 可观测性

> 源码位置：[`functions/rollout/math_rollout.py` L56-60](functions/rollout/math_rollout.py#L56-L60)（import），[L189](functions/rollout/math_rollout.py#L189)（`trace_tool`），[L292](functions/rollout/math_rollout.py#L292)（`trace_client`），[L419](functions/rollout/math_rollout.py#L419)（`@observe_processor`）

| 机制 | 说明 |
|------|------|
| `trace_client(llm)` | 自动追踪每次 LLM 调用的 input/output/token_usage |
| `trace_tool(tools)` | 自动追踪每次工具调用的独立 span |
| `@observe_processor` | 追踪整个 `process()` 调用的 span |
| `ENABLE_TRAJECTORY` | 环境变量，开启后记录完整对话轨迹到可观测性系统 |

### 3.6 Rollout Metrics 自定义追踪

Rollout 处理器通过 `rollout_metrics` 字段上报自定义指标，框架自动聚合到训练监控面板（`trace _ rollout_metrics _ *`）。

**实现方式**（见 [`functions/rollout/math_rollout.py` L325-384](functions/rollout/math_rollout.py#L325-L384)）：

```python
start_time = time.time()
tool_call_durations: list[float] = []

# 将 durations 列表通过 LangGraph config 传入 tools 节点
config = RunnableConfig(configurable={
    "tool_call_durations": tool_call_durations,
    ...
})

# _execute_tools 中记录每次工具调用耗时
t0 = time.time()
result = await tool_node.ainvoke(state, config)
durations.append(time.time() - t0)

# rollout 结束后汇总写入 rollout_metrics
agent_output = AgentOutput(
    rollout_metrics={
        "latency": time.time() - start_time,          # 端到端延迟
        "tool_call_count": len(tool_call_durations),   # 工具调用次数
        "tool_call_total_time": sum(durations),        # 工具调用总耗时
        "tool_call_avg_time": avg(durations),          # 工具调用平均耗时
    },
    ...
)
```

**自定义扩展**：只需在 `rollout_metrics` 字典中添加新的 key-value 即可。例如添加 `"code_length": len(code)` 追踪生成代码长度，框架会自动将其聚合为 `trace _ rollout_metrics _ code_length _ avg/sum` 等指标。

**本 demo 追踪的指标**：

| 指标 | 说明 | 训练面板对应 |
|------|------|------------|
| `latency` | 单次 rollout 端到端耗时（秒） | `trace _ rollout_metrics _ latency _ avg` |
| `tool_call_count` | 工具调用次数 | `trace _ rollout_metrics _ tool_call_count _ avg` |
| `tool_call_total_time` | 所有工具调用的累计耗时 | `trace _ rollout_metrics _ tool_call_total_time _ avg` |
| `tool_call_avg_time` | 单次工具调用平均耗时 | `trace _ rollout_metrics _ tool_call_avg_time _ avg` |

---

## 4. 奖励设计配置与跟踪

> 实现文件：[`functions/reward/math_reward.py`](functions/reward/math_reward.py)（`MathRewardProcessor` [L37](functions/reward/math_reward.py#L37)）
> 答案验证：[`functions/reward/math_verify.py`](functions/reward/math_verify.py)（`verify_math_answer` [L654](functions/reward/math_verify.py#L654)，`extract_answer_by_format` [L118](functions/reward/math_verify.py#L118)）

### 4.1 奖励公式

$$
R_{\text{total}} = 0.2 \times R_{\text{format}} + 0.8 \times R_{\text{accuracy}}
$$

### 4.2 子奖励详解

#### Format 子奖励 (权重 0.2)

- **检测方式**：调用 `extract_answer_by_format(content)` 严格检测 `\boxed{}` 格式
- **评分**：
  - 成功提取 → **+1.0**
  - 未找到 → **-1.0**（惩罚分，负值比零值训练信号更强）

#### Accuracy 子奖励 (权重 0.8)

- **验证流程**：调用 `verify_math_answer(content, ground_truth)` 进行两层验证：
  1. **Hendrycks 标准字符串归一化**：`strip_string()` + `is_equiv()` 精确匹配
  2. **SymPy 符号等价性回退**：`_math_equal()` + `_symbolic_equal()` 代数等价判断
- **评分**：正确 → **1.0**，错误 → **0.0**

### 4.3 答案验证层级

```mermaid
flowchart TB
    Input["模型输出"] --> Extract["extract_answer_by_format<br/>提取 boxed 内容"]
    Extract -->|"提取成功"| Layer1["Layer 1: 字符串归一化<br/>strip_string + is_equiv"]
    Extract -->|"提取失败"| Zero["accuracy = 0.0"]
    Layer1 -->|"匹配"| Score1["accuracy = 1.0"]
    Layer1 -->|"不匹配"| Layer2["Layer 2: SymPy 符号验证<br/>parse_latex + symbolic_equal"]
    Layer2 -->|"等价"| Score2["accuracy = 1.0"]
    Layer2 -->|"不等价"| Score0["accuracy = 0.0"]
```

### 4.4 Reward Metrics 追踪

框架自动采集以下 reward_metrics 到训练监控面板：

| 指标 | 说明 |
|------|------|
| `format` | 格式分（-1 或 1） |
| `accuracy` | 正确性分（0 或 1） |
| `total` | 加权总分 |

### 4.5 装饰器模式

```python
@reward_func("math_reward")          # 注册为 reward 函数，自动生成 process() 入口
class MathRewardProcessor:
    @sub_reward_func("format", sub_weight=0.2)    # 定义子奖励及权重
    def format_score(self, input): ...

    @sub_reward_func("accuracy", sub_weight=0.8)
    def accuracy_score(self, input): ...

    @aggregate_func                               # 定义聚合逻辑
    def aggregate(self, sub_rewards): ...
```

---

## 5. 部署推理验证

MATH 任务暂未提供独立的部署验证脚本。可参考 U-MATH Demo 的验证框架 `test/test_deployed_umath.py`（见 U-MATH Demo）进行适配：

- Rollout 处理器可直接复用 `CalcXRolloutProcessor`（与 U-MATH 完全共用）
- Reward 验证改用 `MathRewardProcessor` 的 format + accuracy（SymPy 精确匹配，不需要 LLM judge）
- 数据集路径改为 `data/hendrycks_math_test_lv45.jsonl`

适配要点：
1. 将 `UMathRewardProcessor` 替换为 `MathRewardProcessor`
2. 移除 LLM judge 相关配置（MATH 不需要）
3. `rollout_extra` 中 `solution` 字段格式已与 `verify_math_answer()` 对齐，无需额外处理

---

## 6. 最终效果

> **训练任务 ID**: `ft-202605131703-f963`  
> **训练任务名**: `cangyu_qwen3.5-9b_hendrycks_math_full_python_interpreter_only_new_16k_24gpus_lv45_trace_test`  
> **训练步数**: 28 步（1 epoch）

### 6.1 关键指标汇总

| 指标 | 初始值 (Step 1) | 最终值 (Step 28) | 提升幅度 |
|------|-----------------|-----------------|---------|
| **训练 reward (total mean)** | 0.192 | **0.957** | +399% |
| **训练 accuracy mean** | 0.316 | **0.950** | +201% |
| **训练 format mean** | -0.308 | **0.988** | 从负分到接近满分（boxed 格式率大幅提升） |
| **验证 reward mean@1** | 0.241 (step 0) | **0.907** (step 28) | +276% |
| **工具调用次数 mean** | 0.41 | **2.14** | +422%，工具使用频率显著增加 |
| **Critic rewards mean** | 0.340 | **0.955** | 值函数与实际 reward 趋势一致 |

### 6.2 训练曲线

#### 训练 Reward

| Step | total reward mean | accuracy mean | format mean |
|------|------------------|---------------|-------------|
| 1 | 0.192 | 0.316 | -0.308 |
| 2 | 0.342 | 0.440 | -0.047 |
| 3 | 0.510 | 0.583 | 0.216 |
| 4 | 0.595 | 0.644 | 0.399 |
| 5 | 0.727 | 0.758 | 0.606 |
| 6 | 0.775 | 0.778 | 0.761 |
| 7 | 0.809 | 0.810 | 0.802 |
| 8 | 0.811 | 0.806 | 0.835 |
| 10 | 0.893 | 0.883 | 0.933 |
| 12 | 0.898 | 0.882 | 0.960 |
| 14 | 0.907 | 0.895 | 0.953 |
| 16 | 0.953 | 0.945 | 0.984 |
| 18 | 0.960 | 0.954 | 0.981 |
| 20 | 0.894 | 0.880 | 0.951 |
| 22 | 0.957 | 0.950 | 0.984 |
| 24 | 0.934 | 0.927 | 0.963 |
| 26 | 0.902 | 0.888 | 0.958 |
| 28 | 0.957 | 0.950 | 0.988 |

![MATH 训练 Reward 曲线](charts/train_reward.png)

#### 验证 Reward

| Step | validation reward mean@1 |
|------|--------------------------|
| 0 (baseline) | 0.241 |
| 2 | 0.606 |
| 4 | 0.740 |
| 6 | 0.797 |
| 8 | 0.887 |
| 10 | 0.881 |
| 12 | 0.889 |
| 14 | 0.908 |
| 16 | 0.895 |
| 18 | 0.912 |
| 20 | 0.895 |
| 22 | 0.914 |
| 24 | 0.907 |
| 26 | 0.899 |
| 28 | **0.907** |

![MATH 验证 Reward 曲线](charts/val_reward.png)

**验证 reward 趋势分析**：从 baseline 0.241 快速上升至 step 8 附近的 0.887，之后在 0.88~0.91 区间稳定震荡，说明模型在 step 8~10 后已接近收敛，后续训练主要在维持和微调。

### 6.3 工具使用趋势

| Step | tool_call_count mean | num_llm_calls mean | num_turns mean |
|------|---------------------|--------------------|----------------|
| 1 | 0.41 | 1.41 | 3.26 |
| 4 | 0.88 | 1.88 | 4.50 |
| 8 | 1.45 | 2.45 | 6.14 |
| 12 | 1.77 | 2.77 | 7.27 |
| 16 | 1.83 | 2.83 | 7.32 |
| 20 | 1.71 | 2.71 | 6.75 |
| 24 | 1.83 | 2.83 | 7.46 |
| 28 | 2.14 | 3.14 | 8.21 |

模型在训练过程中显著增加了 `python_interpreter` 工具调用（0.41 → 2.14），对话轮数也从 3.26 增长到 8.21，说明模型逐步学会了"先思考、再用工具验证、再修正"的多轮推理模式。

![MATH 工具调用趋势](charts/tool_calls.png)

### 6.4 训练效率指标

| 指标 | 初始值 | 最终值 | 说明 |
|------|--------|--------|------|
| Rollout latency (avg, s) | 150.8 | 85.3 | 初始输出长且发散，训练后输出更精炼、延迟降低 |
| Response length mean | 4440 | 1279 | 模型输出更简洁精炼 |
| Response clip ratio | 42.5% | 0.06% | 初始大量截断，训练后几乎消失 |
| Prompt clip ratio | 0% | 1.9% | 少量超长轨迹触发 prompt 截断 |
| Aborted ratio | 0% | 0% | 全程无样本被废弃 |

![MATH 响应长度与对话轮数](charts/response_turns.png)

### 6.5 训练稳定性指标

| 指标 | 趋势 | 说明 |
|------|------|------|
| KL loss | 0.002 → 0.018 | 增长平缓，策略未出现大幅偏移 |
| KL coefficient | 恒定 0.001 | 固定 KL 惩罚系数 |
| Learning rate | 9.64e-7 → 0 (linear decay) | 线性衰减到 0 |
| Actor loss | -0.102 → -0.004 | 趋近于零，训练收敛 |
| Gradient norm | 0.467 → 0.223 | 梯度逐步减小 |
| PG clip fraction | 0.002 → 0.00004 | 策略更新幅度逐步缩小 |
| Entropy | 0.366 → 0.179 | 输出分布更集中，策略趋于确定 |
| Merge failure avg | 0.361 → 0.995 | 接近 100%（fully async 预期行为） |

![MATH 学习率与 KL Loss](charts/lr_kl.png)

### 6.6 典型案例

> MATH 训练尚未导出验证集轨迹 CSV，暂无训练前后对比案例。
> 可参考 U-MATH demo 的 [6.6 节案例对比](../umath/umath_demo.md#66-典型案例训练前-vs-训练后轨迹对比)，两者共用相同的 `CalcXRolloutProcessor` 和 `python_interpreter` 工具，行为模式一致。

---

## 附录 A：目录结构与文件放置（重要）

> 本 demo 的 `extra_demos/math/` 目录已整理为**可直接运行的完整 demo**。目录内已包含：
> - `functions/` 包（含 `__init__.py`），与 `submit_job.py` 中的 `classpath` 对应；
> - `requirements.txt`、`start.sh` 等运行所需文件；
> - `data/` 与 `scripts/`，提交脚本会检查同级 `data/` 并按需运行 `scripts/` 下的数据脚本生成训练/验证数据（也可按 A.2 手动预下载）。

提交任务时，SDK 会根据 `FunctionComponentModel` 中的 `zipdir` 把 `extra_demos/math/` 整个打包上传，远程再按 `classpath` 加载模块。因此可直接在 `extra_demos/math/` 目录下运行。

### A.1 文件放置映射表

| demo 内文件路径 | 说明 |
|--------------|------------------------------------|
| `extra_demos/math/submit_job.py` | 任务提交入口 |
| `extra_demos/math/functions/rollout/math_rollout.py` | rollout 处理器 |
| `extra_demos/math/functions/rollout/code_interpreter.py` | Python 解释器工具 |
| `extra_demos/math/functions/reward/math_reward.py` | reward 处理器 |
| `extra_demos/math/functions/reward/math_verify.py` | 数学答案验证工具 |
| `extra_demos/math/scripts/download_hendrycks_math.py` | 数据下载脚本 |
| `extra_demos/math/data/hendrycks_math_train_lv45.jsonl` | 训练数据 |
| `extra_demos/math/data/hendrycks_math_test_lv45.jsonl` | 验证数据 |

> 关键约束：rollout / reward 代码**必须**放进 `functions/` 包内（与 classpath 对应），且 `functions/__init__.py` 必须存在。

### A.2 数据准备（可选）

`submit_job.py` 会自动检查同级 `data/` 并按需运行 `scripts/` 下的脚本生成数据。如需手动预下载，在 `extra_demos/math/` 下执行：

```bash
mkdir -p data
python scripts/download_hendrycks_math.py
```

### A.3 demo 目录已具备的运行环境文件

| 文件 | 用途 |
|------|------|
| `../../dashscope-1.25.23-py3-none-any.whl` | SDK 本地包（也可改用 PyPI 版本） |
| `./requirements.txt` | 函数组件依赖（langchain、mcp 等） |
| `./start.sh` | 本地启动函数组件服务示例 |

> MATH 的 accuracy 用 SymPy 精确匹配，不调用 LLM judge，因此**不需要** `llm_api_config.json`。

`extra_demos/math/` 目录结构如下：

```
extra_demos/math/
├── submit_job.py
├── start.sh
├── requirements.txt
├── functions/
│   ├── __init__.py
│   ├── rollout/
│   │   ├── math_rollout.py
│   │   └── code_interpreter.py
│   └── reward/
│       ├── math_reward.py
│       └── math_verify.py
├── data/
│   ├── hendrycks_math_train_lv45.jsonl
│   └── hendrycks_math_test_lv45.jsonl
└── scripts/download_hendrycks_math.py
```

随后即可按 **附录 B** 的清单执行 `python submit_job.py`。

---

## 附录 B：快速上手清单

> 前提：在 `extra_demos/math/` 目录下执行以下命令。

1. **安装依赖**：`pip install ../../dashscope-1.25.23-py3-none-any.whl && pip install -r requirements.txt`
2. **设置 API Key**：`export DASHSCOPE_API_KEY=sk-xxx`
3. **准备数据（可选）**：`submit_job.py` 会在提交前自动检查 `data/hendrycks_math_train_lv45.jsonl` 和 `data/hendrycks_math_test_lv45.jsonl`，缺失时自动运行下载脚本；如需提前准备，可手动执行 `python scripts/download_hendrycks_math.py`
4. **提交训练**：`python submit_job.py`
5. **查看指标**：在训练监控面板查看 reward/accuracy 曲线
6. **部署验证**：使用测试脚本对部署后的模型进行 full eval
