refactor: split submit_sft_via_cml.sh into submit_sft.sh + submit_cml_eval.sh

- submit_sft.sh: pure SFT submission, prints JobID and exits (no embedded watcher)
- submit_cml_eval.sh: pure CML eval, reads workflow_id/version from config.yaml
- jupyter_runtime write_text: rewrite to use Contents API instead of terminal websocket
- SKILL.md: restrict trigger to explicit UI click only
- program.md: update §5.2 docs to reflect two-script workflow

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
hupenglong1
2026-05-25 17:21:42 +08:00
parent d2c221e2da
commit a3ab74b506
8 changed files with 328 additions and 186 deletions
+41 -20
View File
@@ -1262,7 +1262,9 @@ python skills/label-master/scripts/validate_label_output.py \
H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重跑**。H2 仿写是 §4.2 GPT 新增的样本,没经过 Step A.5,必须在此补一次语义判定:
1.`augment_<runDic>.jsonl` 抽出 `(query, output)`
🚨 **覆盖率硬规则**`label_master_review.jsonl` 里 H2 部分的行数**必须等于** `augment_<runDic>.jsonl` 的行数(1:1 全量覆盖)。**禁止抽样、禁止"前 N 条 spot-check"、禁止"5/5 pass 推 100 OK"**——§5.0 准入门会用 `wc -l` 拦。已踩坑:R17817 跑了 5 条样本就上报"100 条 pass",被 §5.0 退回。
1. 逐行读 `augment_<runDic>.jsonl` **全部** `(query, output)` 对(N 条就是 N 条,不省略、不抽样)
2. **逐条**调 `Skill(skill="label-master", args=...)`——每次只传**一条** `(query, 当前 label)`
- 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定该条
- 输出格式:`{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`
@@ -1280,7 +1282,7 @@ H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重
**写完复核报告才允许进 Step 5。**verdict 文件不存在 / 没跑层 2 → Step 5 拒绝启动(5.0 的准入检查会查 `label_master_review.jsonl` 存在)。
🚨 **注意**label-master 是"知识 + Agent 推理"型 Skill,不是黑盒分类器。批量调用时严禁要求它返回 1/0 标量;必须输出 `verdict + 推荐标签 + 排除理由` 三件套,便于人类回查
🚨 **注意**label-master 是"知识 + Agent 推理"型 Skill,不是黑盒分类器。每一次调用都必须输出 `verdict + 推荐标签 + 排除理由` 三件套,便于人类回查;严禁要求它返回 1/0 标量。(批量调用本身已在第 2 步禁掉,这里再强调一次输出 schema。)
### 5. SFT 训练
@@ -1290,7 +1292,7 @@ H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重
1. `echo '{"step":"sft","status":"running",...}' >> $SESSION_OUTPUT/program-state.jsonl`
2. 跑层 1 + 层 2 label-master 复核(§4.5
3. 如果复核全过:在 SAME bash turn 里调 `bash scripts/submit_sft_via_cml.sh <RUNDIC> <PREV_RUNDIC>` 并立刻挂 watcher(§5.2 的 cml custom_train + workflow chain
3. 如果复核全过:在 SAME bash turn 里调 `bash scripts/submit_sft.sh <SFT_RUNDIC> [PREV_RUNDIC]` 并立刻挂 watcher(§5.2)。训练 `_SUCCESS` 落盘后的下一轮 step 单独用 `bash scripts/submit_cml_eval.sh <EVAL_RUNDIC>` 起评测,**不要**把评测串到 SFT bg 任务里——跨 step bg 是禁区
**反模式**(被反复踩坑,永远禁止):
@@ -1318,7 +1320,20 @@ if [ "$TOTAL" -gt 0 ] && [ $((NOT_PASS * 100 / TOTAL)) -gt 5 ]; then
exit 1
fi
# 2. zk_trainer 仓库已 clone
# 1b. coverage 断言:augment 行数必须被 review 完整覆盖(杜绝抽样外推)
# review.jsonl 同时包含 §4.0.1 Step A.5 的 mislabel candidate review + §4.5 的 augment 仿写 review
# 所以行数下界 = augment.jsonl 行数(mislabel 部分多出来的那批不影响下界)
AUG="$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_${RUNDIC}.jsonl"
if [ -f "$AUG" ]; then
AUG_N=$(wc -l < "$AUG")
REV_N=$(wc -l < "$REVIEW")
if [ "$REV_N" -lt "$AUG_N" ]; then
echo "label-master review 行数 $REV_N < augment 条数 $AUG_N — §4.5 必须逐条覆盖,禁止抽样外推(如 5/5 pass 推 100 OK),回 §4.5 把 augment.jsonl 的每一条都 append 一行 review entry"
exit 1
fi
fi
# 2. zk_trainer 仓库已 cloneURL 写死,不要换命名空间,clone 失败先看 ssh key
cd "$AUTORESEARCH_CHAT_ROOT"
[ -d zk_trainer ] || git clone git@git.n.xiaomi.com:wangsenhao/zk_trainer.git
```
@@ -1373,11 +1388,11 @@ R23-R28 期间累积了 `sft_output_r23` ~ `sft_output_r28`,多次需要回退
**自动评测 watcher**
R29 起 SFT 走 cml custom_train submit(见 5.2),watcher 直接复`submit_sft_via_cml.sh` 内嵌的轮询逻辑——`cml custom_train describe` 检测 succeed → 验产出 → 自动起 `cml workflow run` 评测。本地无 PID 可监控
R29 起 SFT 走 cml custom_train submit(见 5.2),训练`submit_sft.sh` 提交、评测用 `submit_cml_eval.sh` 单独起。这两步**不再串接**——agent 用 watcher 监听 `sft_output/_SUCCESS`watcher fire 后下一轮主动调 `submit_cml_eval.sh`。本地无 PID 可监控,全靠 watcher 串两个 step
##### Claude-side 双 watcher 强制要求(R17775 经验沉淀)
`submit_sft_via_cml.sh` 内嵌的 bash watcher **只负责"训练完 → 起评测"**,它不会通知 Claude。Claude 如果只挂一个"等评测产物"的 watcher,训练完成事件会被漏报(R17775 训练 19:57 完成,Claude 40+min 不知道,直到用户问)。
旧版 `submit_sft_via_cml.sh` 在脚本内嵌 watcher 自动起评测——已废弃(违反"bg 任务作用域 = 单 step"原则)。现在 `submit_sft.sh` 只负责提 SFT、立刻退出;起评测必须由 agent 在 SFT watcher fire 后**新一轮**手动调 `submit_cml_eval.sh`。Claude 如果只挂一个"等评测产物"的 watcher,训练完成事件会被漏报(R17775 训练 19:57 完成,Claude 40+min 不知道,直到用户问)。
**Claude 调用 submit 脚本后,必须立刻用 `Bash run_in_background` 起两个 watcher task**(一个都不能少):
@@ -1476,26 +1491,33 @@ R23-R28 期间用本地 `nohup python3 prepare_and_train_sft.py train ...` 启
##### 一键提交脚本
⚠️ **runDic 不许手敲**:先 `eval` 一下 `resolve_run_ids.sh` 拿到这一轮的 `$SFT_RUNDIC` / `$EVAL_RUNDIC`,再原样传给 `submit_sft_via_cml.sh`。R2 踩过的坑就是手算 runDic 时把 SFT yaml 的命名也跟着 +1,导致 `data_clean_<EVAL_RUNDIC>/` 跳过了 R1 命名空间。
⚠️ **runDic 不许手敲**:先 `eval` 一下 `resolve_run_ids.sh` 拿到这一轮的 `$SFT_RUNDIC` / `$EVAL_RUNDIC`,再原样传给两个独立脚本。R2 踩过的坑就是手算 runDic 时把 SFT yaml 的命名也跟着 +1,导致 `data_clean_<EVAL_RUNDIC>/` 跳过了 R1 命名空间。
提交流程拆成两步(**两个独立脚本,两个 step**):
```bash
cd "$AUTORESEARCH_CHAT_ROOT"
eval "$(./scripts/resolve_run_ids.sh)" # 解析出 $SFT_RUNDIC / $EVAL_RUNDIC
./scripts/submit_sft_via_cml.sh "$SFT_RUNDIC" "$EVAL_RUNDIC"
# 等价示例(仅供阅读,**实际调用一律走 eval 上面那行**):
# R29 上一轮已落盘的 workflow 是 17755,因此 SFT_RUNDIC=17755, EVAL_RUNDIC=17756
# ./scripts/submit_sft_via_cml.sh 17755 17756
# Step 5:提交 SFT 训练(不附带任何评测逻辑)
./scripts/submit_sft.sh "$SFT_RUNDIC"
# 训练 _SUCCESS 落盘后的下一轮 step 才起评测:
./scripts/submit_cml_eval.sh "$EVAL_RUNDIC"
```
`submit_sft_via_cml.sh` 接受三个位置参数:`<SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]``PREV_RUNDIC` 默认 `SFT_RUNDIC-1`,仅用作上一轮 `sft_output_r<PREV_RUNDIC>` 改名。脚本内部断言 `EVAL_RUNDIC > SFT_RUNDIC`,否则直接报错退出。
`submit_sft.sh <SFT_RUNDIC> [PREV_RUNDIC]`
脚本做的事:
1. 渲染 `scripts/sft_train_job.yaml.tpl` 为本轮 yaml(替换 `{RUNDIC}``$SFT_RUNDIC``{PREV_RUNDIC}``$PREV_RUNDIC`
2. `cml custom_train submit --filename <yaml>` 提交任务, `JobID`
3. 后台 watcher 进程:每 60s `cml custom_train describe` 轮询任务状态
4. 任务 succeed → 验证 `sft_output/_SUCCESS` 存在 → 自动起 cml workflow run**评测的 `runDic=$EVAL_RUNDIC`**(这是整条流水线唯一一次 +1
5. 任务 failed/killed → 写日志报警
2. `cml custom_train submit --filename <yaml>` 提交任务,打印 `JobID`,立刻退出
3. **不再内嵌 watcher**——agent 自己用 watcher 监听 `$AUTORESEARCH_CHAT_ROOT/sft_output/_SUCCESS`watcher fire 后下一轮再调 `submit_cml_eval.sh`
`submit_cml_eval.sh <EVAL_RUNDIC> [MODEL_NEW] [MODEL_OLD]`
1.`skills/model-iteration/assets/config.yaml``cml_eval.workflow_id``cml_eval.version`(版本不再硬编码,调版本就改 config.yaml
2. `MODEL_NEW` 默认 `$AUTORESEARCH_ROOT/sft_output``MODEL_OLD` 默认旧基线(也可以通过 env / 参数覆盖)
3. `cml workflow run --workflow_id ... --version ... --global_inputs runDic=$EVAL_RUNDIC ...` 起评测,**评测 runDic=$EVAL_RUNDIC**(整条流水线唯一一次 +1)
4. 立刻退出,UI 卡片状态走 program-state.jsonl 的 watcher 通道
##### yaml 模板要点(`scripts/sft_train_job.yaml.tpl`
@@ -1521,8 +1543,7 @@ cml custom_train logs <JOB_ID> --follow
# 停任务
cml custom_train kill <JOB_ID>
# watcher自动评测)日志
tail -f /tmp/r<RUNDIC>_logs/cml_watcher.log
# SFT 不再内嵌 watcher自动评测改成 agent watcher → 单独调 submit_cml_eval.sh
```
##### 与 nohup 路径的对比
@@ -1534,7 +1555,7 @@ tail -f /tmp/r<RUNDIC>_logs/cml_watcher.log
| 节点故障重试 | 无 | 自动 2 次 |
| 失败告警 | 无 | 飞书 P2 |
| 任务历史 | 仅本地 log | cml 平台可查 |
| 启动复杂度 | 单行 nohup | 单行 submit_sft_via_cml.sh |
| 启动复杂度 | 单行 nohup | submit_sft.sh + watcher + submit_cml_eval.sh |
##### 何时仍用 nohup 本地训