model-iteration: runDic 单一可信源 + label-master 逐条调用

- 新增 scripts/resolve_run_ids.sh:从 workflow5/ 解析出 SFT_RUNDIC / EVAL_RUNDIC,禁止 agent 心算 +1
- submit_sft_via_cml.sh 拆成 <SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC] 三参,yaml 用 SFT_RUNDIC,cml workflow run 用 EVAL_RUNDIC
- program.md §746 / §5.2 / §1.2 同步约束,并加 R2 落盘多 +1 的踩坑案例
- §4.0.1 Step A.5 / §4.5 label-master 复核改逐条调用(≤8 路并发),禁止批量塞多条 query

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
hupenglong1
2026-05-25 11:42:44 +08:00
parent 362321c460
commit 57f5b60a3e
6 changed files with 606 additions and 74 deletions
+131 -25
View File
@@ -18,7 +18,7 @@ when_to_use: |
| ❌ 禁止说的话 | ✅ 正确做法 |
|---------|---------|
| "要不要我做训练集近邻检索?" | 这是 §2.3 分析必做项,**直接做**,做完把结果落 report |
| "要不要我做训练集近邻检索?" | 这是 §2.3 分析必做项,**直接做**,做完把结果落 dist-analysis 报告(`workflow<runDic>.md` |
| "下一步可以继续吗?" | 永远不问。看 program.md 流程图自己判断 |
| "我先把控制权交回,等你来问跑完了吗" | watcher 接手 UI 同步,长任务用 `bash(run_in_background=true, wait_for_completion=true)` 提交后本轮主动结束;后端会在产物落盘后自动起新一轮把结果送回,**直接进 Step 1** |
| "这一步是关键决策点,需要你拍板" | program.md 没写就不是。**自主决策 + 落 iteration_log** |
@@ -48,16 +48,17 @@ when_to_use: |
- [ ] watcher 声明已写入 program-state.jsonl
- [ ] cml workflow + 等 metric_diff 落盘已用 `bash(run_in_background=true, wait_for_completion=true)` 提交(**不要**在 bash 内 sleep+poll
### Step 1 准入(`dist-analysis`
### Step 1 准入(`dist-analysis`,含原 Step 2 报告产出
- [ ]`metric_diff/lark_template.json` 提分层指标(specific / 大盘车载 / 目标专项)
- [ ]`metric_diff/specific_comparison.csv` 提取 baseline 错误分布
- [ ] **§2.3 失败 case 与训练数据的关联**:每条错例在 `train_set/zk_intent/*.jsonl` 做近邻检索(前 3 近邻),输出"有近邻 / 无近邻"分类
- [ ] **§2.3 mislabel 命中必须落盘 CSV**:列固定为 `file,line,query,old_label,是否改(1/0),建议新label`,写到 `$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_<runDic>.csv`(前端「分层结果分析」卡片就读这一份)。本轮无 mislabel 也写一份只含表头的空 CSV,明示"已检查、无候选"。**禁止**只 print 到 stdout 或只写进 workflow.md——卡片读不到 CSV 等同漏交
- [ ] **§2.3 判别器自检**:写完任何"把 raw output 抽成可比对值"的函数后(complex 二值 / tag / function / code label / slot 等都算)、跑全量前——端点验证(每类训练侧 ≥1 条 + eval 侧 ≥1 条;二分类 4 条、N 分类 ≥2N 条)+ 报告并列原文 + 零计数兜底(详见 program.md §2.3 自检小节)
- [ ] **§2.3 Reward 对齐检查**:全量失败 case 用 `zk_reward_fn` 验 reward 方向(不抽样)
- [ ] §0.1 Gold drift 检查(如未做)
### Step 2 准入(`report`
- [ ] §2.4 根因归类表(每个 pattern 必归一类,可并列但要主次)
- [ ] 写入 `results/workflow<runDic>.md`
- [ ] 写入 `results/workflow<runDic>.md`(根因 / 跨轮 diff / 需求集合深度分析 / 天花板诊断)
- [ ] `output/relabel_candidates_<runDic>.csv` 已落盘(无候选则空表头 CSV
- [ ] error_registry 追加本轮错误
### Step 3 准入(`hypothesis` — analysis 收尾,不是 train 开头
@@ -65,7 +66,7 @@ when_to_use: |
- [ ] 假设必须有依据(指向 §2.3 / §2.4 的具体发现)
- [ ] **`step:"hypothesis"` entry 写在当前 roundR{n})名下**——不要写成 R{n+1}。后端把 hypothesis 归类为 analysis 类,是 R{n}·Baseline / R{n}·Analysis 的最后一张卡,不是 R{n+1}·Train 的开头。这样 gateHuman Check / Review)会插在 hypothesis 卡之后、R{n+1}·Train 之前,**用户看到假设内容再拍板是否进 train**。
### Step 1 → Step 2 → Step 3 边界(**NEVER STOP 硬连接**R0 / R1+ regression 都适用)
### Step 1 → Step 3 边界(**NEVER STOP 硬连接**R0 / R1+ regression 都适用)
- [ ] dist-analysis 算完 deltanew_fail / new_fix / persistent / 子集分布)那一刻起,**同一轮 bash 不许结束**:紧接着写 `results/workflow<runDic>.md`(包含完整指标表 + 病灶定位 + 假设 + 回滚/继续建议)→ append `iteration_log.jsonl` R{n} entry`results` 字段填本轮 metric`hypothesis` 字段填下一动作)
- [ ] **regression 场景同样适用**:哪怕 R1 出现导航bvt 纯劣化、可聊可控大幅 -25 这种"必须回滚"信号,**先把分析落到 workflow<runDic>.md 和 iteration_log,再用 chat reply 给人回滚选项**。文件先落、聊天再发——顺序不能反。
- [ ] **不许"诊断只写聊天回复 / 等用户拍板再补盘"**:前端「分层结果分析」卡片读的是 `chat_root/results/workflow<runDic>.md`;你不写盘卡片永远显示上一轮,看不到 R1 结论。
@@ -75,10 +76,11 @@ when_to_use: |
### Step 4 准入(`augment`,需要修改训练集才进)
- [ ] **§4.0 原始训练数据清洗(增强前必做)**——按 4 种动作走完逐 pattern 判定
- [ ] §4.0.1 Step A:候选定位输出 csv**不直接改**
- [ ] §4.0.1 Step B:量级判定走对应支线(≤50 自动 / 51-200 全量人审 / >200 触发 #3,全量交付
- [ ] §4.0.1 Step A.5**先走 label-master 预审**(用推荐标签覆盖 new_label,剔除 label-master 不认同要改的条目
- [ ] §4.0.1 Step B:量级判定基于 label-master 处理后的**残留候选量**走对应支线(≤50 自动按 label-master 推荐改 / 51-200 全量人审 / >200 触发 #3,全量交付)
- [ ] §4.0.1 Step C:备份 `.bak` 文件
- [ ] §4.1 输入准备 / §4.2 GPT 调用 / §4.3 sanity check / §4.4 写入
- [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` + H2 `augment_<runDic>.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),verdict 写 `results/data_clean_<runDic>/label_master_review.jsonl`,不通过比例 ≤ 5% 才能进 Step 5
- [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` 只跑层 1(格式)—— 语义已在 §4.0.1 Step A.5 完成;H2 `augment_<runDic>.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),verdict 写 `results/data_clean_<runDic>/label_master_review.jsonl`,不通过比例 ≤ 5% 才能进 Step 5
### Step 4 → Step 5 边界(**NEVER STOP 硬连接**,反复踩坑)
- [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft_via_cml.sh` → 挂 watcher
@@ -148,6 +150,75 @@ echo '{"step":"cml","status":"running","progress":0.4,"ts":"'$(date -Iseconds)'"
UI 卡底部进度条会跟着动;不写就一直显示初始进度。
### R5 — bg 任务作用域 = 单 step(**反复踩坑**
bg 任务(`bash(run_in_background=true)`)的合法范围是**一个 step 内的"提交一个远端动作 + 等它的产物落盘"**——比如「提 cml workflow run + 等 metric_diff 落盘」、「提 SFT custom_train + 等 `_SUCCESS`」。这是 §「典型用法」推荐的标准结构。
**禁止**把 bg 范围扩到跨 step
| ❌ 禁止 | ✅ 正确 |
|---|---|
| 一个 bg 串行 `等 SFT _SUCCESS → 提 cml workflow run(评测)→ 等评测 lark_template.json` | sft watcher fire complete 那一刻,agent 主对话**当轮**完成:写 `cml=running` + 起新 bg(提评测 + 等 lark_template.json+ 注册新 watcher |
| bg 内部 sleep+poll 跨多个 step 的状态 | 每个 step 一个 bg + 一个 watcherwatcher fire = step 边界 = 主对话轮回来推进 |
**为什么是死规则**bg 脚本本身不会 append `program-state.jsonl`,状态只活在 bg 进程内存。一个 bg 跨 step 跑起来后,前端从第一个 step complete 之后就完全感知不到后续 step——卡片永远停在第一个 step,watcher 也没注册新的。**bg 进程死了 / session 重连 / 中途想插手** 都补不回来,因为 program-state.jsonl 上没任何线索。
**自查判据**:bg 脚本里出现新 step 的关键词(`cml workflow run` 提评测、`submit_sft_via_cml.sh`、对应新 step 的 `echo running`),就是越界。bg 只该做"提一个远端 job 然后等它的产物",**绝不能跨 step 边界**。
**watcher fire → 当轮 4 件套**(漏一件就断片):
1. echo 上一 step `complete`watcher 已自动写也算)
2. echo 下一 step `running`
3. 起新 bg「提交下一 step 的远端动作 + 等产物」
4. 注册新 watcher 监听下一 step 的产物文件
回主对话轮前自问一句:**"如果我现在被 kill,单看 program-state.jsonl 最后一行,下一个 agent 能不能接着干?"** 答不上来就是 4 件套没补全。
**绝不"事后补 status"绕过 watcher**:发现 step 在 UI 上一直 running、watcher 没自动标 complete**正确做法是 debug watcher**(看 `_SUCCESS` / `lark_template.json` 是否真的落盘、watcher 进程是否还在),**不是**自己手 echo 一条 `step=complete` 把状态条点亮。后者掩盖 bug 但留下错位的 run_id,下一轮 watcher 会接着错。
### R6 — 同秒 running + complete 视为虚假完成(**反复踩坑**)
每个真实 step 都有最低耗时下限:
| step | 最低耗时 | 原因 |
|---|---|---|
| `cml` | 数分钟到数十分钟 | 远端 workflow 启动 + 评测 |
| `sft` | 30 分钟以上 | 训练 pod |
| `augment` | 数分钟 | GPT 仿写 + sanity + label-master 复核 |
| `dist-analysis` | 1-3 分钟 | 读 metric_diff + 跨轮 diff + 写 workflow.md |
| `gold-drift` | 1-2 分钟 | drift 检测 |
如果 `step=running``step=complete` 两条 entry 的 `ts` 在**同一秒**(或差距远小于该 step 最低耗时),就是**虚假完成**——agent 平铺 echo 状态绕过了真实工作。**禁止**。
最常见踩坑:human-review / human-check 拿到用户决策后,agent 在同一轮里把 `hypothesis + augment + sft + watcher` 一口气 echo 出来,只真做了最后一步(提 SFT),中间的 `augment` 被跳过——`augment_<runDic>.jsonl` 根本没生成,前端 augment 卡永远空。
**正确做法**human-review 之后必须按 program.md 逐 step 真跑:
1.`hypothesis=running` → 真写假设到 iteration_log → 写 `hypothesis=complete`
2.`augment=running` → 真跑 §4.0.1 + §4.1 + §4.2 + §4.3 + §4.5 label-master 复核 → 写 `augment=complete`**这一步至少 5 分钟**
3.`sft=running` → 提交 cml custom_train + 起 watcher
**自查**:连续两个 step 的 `ts` 间隔 < 60 秒,必然有一个是假的。复盘时 grep program-state.jsonl 看相邻 entry 时间戳。
**前端表现的迷惑性**:虚假完成的 step **状态条仍显示 ✓ complete**(因为 entry 写了 complete),但**详情卡空**(因为 backend 按 step 关联的产物文件读内容——augment 卡读 4 个 `augment_<runDic>.*` 文件,文件不存在 = 卡片空)。**状态条说做完了、详情卡却空**——这是最具迷惑性的失败模式,必须从源头杜绝。
### R7 — `step=running` 与对应 `watch` 条目必须同事务落盘(**反复踩坑**)
watcher 触发 complete 时使用的 `run_id` **必须等于触发它的 watch 条目的 run_id**。后端 scanner 改造后实施两道闸门:
1. **每个 step 只 register 最新一条 watch 条目**append-only jsonl 里历史 round 的 watch 不再被复活)
2. watch 条目的 `run_id` 必须**等于**该 step 最新一条 `status=running` 条目的 `run_id`,否则跳过等下一轮 scanner
这意味着 agent 一侧必须遵守:
- **写 `step=running, run_id=R<N>` 之后,必须紧接着写 `watch {step, run_id=R<N>, ...}`**——同一秒、同一个工具调用块内、不要被别的写入打断。理想是把"reset status + watcher 声明"做成一次 append 数组写入。
- **跨 round 的 watch path 即使重复也要重发**(如同样的 `_SUCCESS` 路径),不能依赖"上一 round 那个 watch 还在"——`run_id` 不一样,watcher 任务也是新的。
- **`run_id` 字段必填**,缺失会被 scanner 当成 `'?'` 处理,`'?' != 'R2'` 直接 skipwatcher 永远起不来。
**反模式(实际踩过的坑)**
- agent 写完 `sft=running` 后没在同事务里写 watch → scanner 这一轮扫到时只有 status,没有匹配 run_id 的 watch → 用历史 watch 的 path 注册 watcher(旧逻辑下),run_id 错位
- watcher fire 后 agent 看 status 没动,又手动 echo 一条 `sft=complete`(违反 R5 末段)→ 后端的 scanner 已经修,但 agent 协议层面还是要 hold 住
**自查**grep `program-state.jsonl` 里所有 `watch` 条目,每条的 `run_id` 必须能在前面(同一 round)找到匹配的 `step=<step>, status=running, run_id=<同值>` 条目。
### State 文件位置
```
@@ -175,8 +246,7 @@ mkdir -p "$SESSION_OUTPUT"
|----------|----------|
| `cml` | CML 评测 |
| `gold-drift` | Gold Drift |
| `dist-analysis` | 分层结果分析 |
| `report` | 问题分析 & 报告 |
| `dist-analysis` | 分层结果分析 & 报告(含根因归类、workflow<runDic>.md 产出) |
| `hypothesis` | 形成假设 |
| `augment` | 数据增强 |
| `verify` | 修改返回验证 |
@@ -280,7 +350,7 @@ echo '{"watch":{"step":"cml","kind":"file_exists","path":"'$ROOT/workflow$RUNDIC
要把日志推到 UI 底部那块黑色 Live Log 区,追加:
```bash
echo '{"log":{"ts":"11:55:10","iter":"R3","text":"Step 3 开始: 问题分析 & 报告生成"}}' >> "$SESSION_OUTPUT/program-state.jsonl"
echo '{"log":{"ts":"11:55:10","iter":"R1","text":"Step 1 dist-analysis: 写 workflow<runDic>.md,根因归类完成"}}' >> "$SESSION_OUTPUT/program-state.jsonl"
```
### 不要做的事
@@ -339,6 +409,11 @@ echo '{"log":{"ts":"11:55:10","iter":"R3","text":"Step 3 开始: 问题分析 &
2. **假设驱动**:每轮必须能回答"这轮验证了什么?学到了什么?下一轮改什么?"
3. **NEVER STOP**:全程不打断用户,循环直到达标或人类主动打断
4. **从 basemodel 训练**:每轮 SFT 的 `--model_path` 必须是 basemodel,禁止从上轮 ckpt 继续训
5. **复杂度优先**:分析类任务(近邻检索 / 聚类 / 相似度统计)选算法前先估数据规模。**忌**对几万行训练集用 `sklearn.fit_transform` 这类要全量物化稀疏矩阵或 embedding 矩阵的 API — 单次 600s 仍可能 timeout。**>1 万行训练集做近邻必须先粗筛后精排(倒排索引 → 每错例 50–300 候选 → 精排),禁止 O(M·N) 全量比对**——37×30k=1.1M 次朴素比较 ~3min,粗筛后 7.4k 次 ~10s。错例侧的"剪枝"只是常数优化,不能替代粗筛。详见 [references/program.md §2.3](references/program.md)
6. **大文件先切片**>10MB 量级的远端 CSV/JSONL(如 `specific_comparison.csv`),先用 bash `awk/grep/head` 在远端切出目标子集再处理。**忌**直接 `pd.read_csv` 整体加载或把 DataFrame 当返回值穿过工具调用 — 会撞 runtime 的输出/返回大小限制(实测 99MB 直接读 → "太大了" 失败)
7. **跨脚本复用走 /tmp pickle 缓存**:同一轮 dist-analysis 里 all_train.jsonl 这种"多个脚本都要读的训练集"必须缓存——第一次读完用 `pickle.dump(rows, '/tmp/all_train_<runDic>.pkl')`pod /tmp 是本地 SSD),后续脚本以 `if path.exists(): pickle.load(path) else: 重建 + 落盘` 模式启动。倒排索引同理。**忌**每个脚本都重新 `for line in open(jsonl): json.loads(line)` 全表 — 30k 行 NFS jsonl 每次 515s,从本地 pickle unpickle <1sv32 R0 5 次重读累计 ~50s 可省。脚本文件名最好写 cache key(如 `kn_search_v2_<runDic>.py`),换轮换 runDic 自动失效。
8. **stdout 即 LLM 下一轮 input,分级 print**:每个分析脚本的 stdout 会原样喂回 LLM 当下一轮上下文,每 KB 都要 prefill。**只 print 决策需要的聚合**(端点抽样总数、各 verdict 的计数、top-3 exemplars 文字),**明细落 `/tmp/<step>_<runDic>_detail.json`**(每条错例的 3 近邻 + 相似度 + token overlap 等)后续要回看再 `cat` 该文件。v32 R0 实测 kn_search 一次 stdout 18KB、judge_check 4.7KB、mislabel 11.3KB60% 是重复的 per-case 全量 dump,LLM 看完只用一句话总结——这部分 prefill cost 是直接浪费。还有:`cat > /tmp/x.py <<EOF ... EOF` 模式 runtime 会把每行 PS2 提示符 `> ` 回显进 stdout(实测一次累计 1–2KB),改用 `python3 -c "open('/tmp/x.py','w').write(open('/dev/stdin').read())" <<'PY' ... PY``base64 -d` 注入,可以彻底消掉 `> > >` 噪声。
9. **一律走 bash + 脚本文件,禁用 python_exec**:所有 python 任务把代码写到远端 `/tmp/xxx.py`,用 `bash``python3 -u /tmp/xxx.py 2>&1 | tee /tmp/xxx.log`。**忌**用 python_exec — 它 stdout 全缓冲、turn 结束才 flush,超时被 kill 时 buffer 直接丢,故障时连 phase 时序都看不到(实测 600s/1800s 撞墙后 `tool_result` 没有任何 stdout 字段)。bash + tee 模式 stdout 实时落盘,超时也能 `tail /tmp/xxx.log` 看到死在哪。
## 达标条件
@@ -365,8 +440,7 @@ step key 跟 Step 编号映射:
|------|----------|------|
| Step 0 | `cml` | CML 评测当前模型 |
| Step 0 (附带) | `gold-drift` | Gold drift 检查 |
| Step 1 | `dist-analysis` | 分层结果分析 |
| Step 2 | `report` | 问题分析 & 报告 |
| Step 1 | `dist-analysis` | 分层结果分析 + 问题分析 & 报告(写 workflow<runDic>.md |
| Step 3 | `hypothesis` | 形成假设(写 iteration_log |
| Step 4 | `augment` | 数据增强 / 生成 |
| Step (干预后) | `verify` | 修改返回验证 |
@@ -377,8 +451,7 @@ step key 跟 Step 编号映射:
```
LOOP:
Step 0: CML 评测(当前模型)+ Gold drift 检查
Step 1: 分层结果分析(需求集合 → 大盘车载 → specific test
Step 2: 问题分析 & 报告(根因归类 / 跨轮 diff / 天花板诊断)
Step 1: 分层结果分析 + 问题分析 & 报告(需求集合 → 大盘车载 → specific test;根因归类 / 跨轮 diff / 天花板诊断 / 写 workflow<runDic>.md
Step 3: 形成假设(写入 iteration_log.jsonl
Step 4: 数据生成(仅 data 归因时做)
Step 5: SFT 训练(submit_sft_via_cml.sh
@@ -423,6 +496,27 @@ assets/
- 跨子集净退步(#4
- 连续 3 轮目标子集净提升 ≤ 0.5pp(#5
### ⛔ 落 gate 前必读:30 秒自检三问(**任一不过 = 不写 gate,直接进下一步**)
写 gate entry / 在 chat 里向用户提问之前,**逐条过这三问**。下面任何一段流程示例都默认你已经过了这三问;过不了,再漂亮的 summary/proposal/ask 也是干扰用户。
**Q1. 是真 §HiTL 信号吗?**
- ✅ 真信号:label-master 预审后残留 > 50/200、Gold drift ≥ 10、跨子集净退步、连续 3 轮无提升 等列表里写明的条件
- ❌ 凑出来的理由:`第一次跑想让用户校方向` / `我担心副作用` / `想让用户拍板更稳` / `proposal 听起来风险大` —— 这些都是脑补,不是信号
**Q2. `ask` 是真分叉吗?**
- ✅ 真分叉 = 用户的判断能改变下一步动作:候选量收窄策略、回滚 vs 续训、换目标子集思路、改阈值
-**"H? 假设组合"不是分叉**`H1+H2 一起 vs 只跑 H1``先做 H1 还是先做 H2``激进 vs 保守 vs 中庸``要不要追加 H3` —— 假设的**拆分、组合、顺序、激进度**全是 agent 自己根据信号该决定的,决定完写进 `proposal` 公布即可,**不要甩给用户**
**Q3. 拆 H 的依据是数据信号还是脑补 trade-off?**
- ✅ 数据信号:mislabel 近邻数、no_neighbor 占比、pattern 分布、其他子集的实测分数差
- ❌ 脑补:`改这 12 条可能会让 X 子集下降` / `H2 加多了可能过拟合` —— 没跑过就是推测;推测可以写进 `proposal` 当 caveat"预期回收 ~10 条,trade-off 待 R1 验证"),**做不出 ask**
> **反复踩坑案例**5/24 真实复盘):R0 baseline 跑完,37 错例里 12 条 mislabel(远低于 50 阈值),agent 自己脑补"H1 修标可能拖累线上挖掘召回complex 子集",于是抛 `H1+H2 一起 vs 只跑 H1(激进 vs 保守)` 让用户拍板。**三问全挂**:信号没命中(Q1 ❌,12 < 50)、ask 是 H 组合(Q2 ❌)、副作用是推测(Q3 ❌)。**正确做法**:定下方案写进 proposal 公布,直接进 augmenttrade-off 留给 R1 评测验证。
如果三问都过 = 真有事要用户拍 → 按下面 `### 写 gate 节点` 的写法落盘 + chat ask。
如果有一条不过 = 没事 → 不写 gate、不在 chat 里凑问题、直接进下一步。
### 写 gate 节点(让 UI 显示 Human Check / Review 卡)
凡是要让用户拍板的检查点,**必须** append 一条 gate entry 到 `program-state.jsonl`**再** 用 chat reply 提问。UI 会在当前 round section 之后插入「Human Check」或「Human Review」横向卡片(IN PROGRESS 状态)。
@@ -435,10 +529,10 @@ assets/
| 时机 | gate 类型 | run_id |
|---|---|---|
| R0 baseline 分析完成、要让用户确认是否进 R1 train命中 HiTL 信号 / 候选量大 / 目标子集偏低 / 第一次跑想让用户校方向 等 | `human-check` | `R0` |
| R0 baseline 分析完成、命中 §HiTL 信号要让用户拍板(label-master 预审后残留候选超阈值、目标子集异常、跨子集分歧大 等真实信号;⛔ **不是**"第一次跑想让用户校方向" / "我担心 H1 修标的副作用" / "想让用户在激进/保守里选"这种凑出来的理由——先过上面的自检三问 | `human-check` | `R0` |
| R{n≥1} analysis 完成、命中 §HiTL 信号(gold drift / regression / 跨子集退步 / 连续 3 轮无提升 等) | `human-review` | `R{n}` |
| §4.0.1 Step B:候选 > 200 条命中触发条件 #3 | `human-check` | 当前 round |
| 其他 HiTL 信号(要批改旧标签 > 50 条、Gold drift ≥ 10 条 等) | `human-check`(开始前)/ `human-review`(结果后) | 当前 round |
| §4.0.1 Step B**label-master 预审后**残留候选 > 200 条命中触发条件 #3(原扫数不算数) | `human-check` | 当前 round |
| 其他 HiTL 信号(label-master 预审后残留 > 50 条、Gold drift ≥ 10 条 等) | `human-check`(开始前)/ `human-review`(结果后) | 当前 round |
判断标准就一条:**只要你下一步打算 chat-ask 用户拍板,就先写 gate entry 再问**。
@@ -447,11 +541,16 @@ assets/
**强烈推荐结构化三段写法**`summary` / `proposal` / `ask`)——UI 会渲染成"现状 / 提议 / 请选"三个带标签行,用户一眼看明白。`reason` 留作 fallback。
```bash
# R0 baseline 之后想让用户拍板是否进 train(推荐结构化)
# ⛔ 写之前已经过了上面 §"30 秒自检三问"——否则不要写这条 gate。
# ⛔ ask 必须是真分叉。⛔ "H1+H2 一起 vs 只跑 H1"、"激进 vs 保守"、"先 H1 还是先 H2" 这类
# "假设组合"统统不是分叉——是 agent 自己根据信号决定的,决定完写进 proposal 公布即可。
# R0 baseline 后命中真实 HiTL 信号(这里是候选量超阈值),让用户拍板更精细 pattern
# ⚠️ 给用户排板的候选数量必须是 §4.0.1 Step A.5 label-master 预审之后的残留数(推荐 != old_label 的那部分),
# 不是 wide pattern 原始扫出来的数。原始数 label-master 还要剔掉一大半,先用原始数找用户=干扰用户判断。
echo '{"step":"human-check","status":"running","run_id":"R0",
"summary":"R0 baseline 完成;复杂导航过召专项0511 = 58.89%(53/90)37 错全为 complex 误判",
"proposal":"H1(标签纠错):7 条原标 complex=true 但实际是简单导航的样本改回 complex=false。 H2(数据增强):仿写 100 条 complex=false 的简单导航 query 补进训练集,平衡正负样本",
"ask":"是否按 H1+H2 一起进 R1 train?还是先只跑 H1 验证一轮?",
"summary":"R0 baseline 完成;复杂导航过召专项0511 = 58.89%(53/90)wide pattern 原扫 412 条,label-master 预审后残留 287 条(> 200 阈值)",
"proposal":"H1(标签纠错):按 label-master 推荐改这 287 条 complex=true complex=false。 H2(数据增强):仿写 100 条 complex=false 的简单导航 query 补进训练集",
"ask":"残留 287 条仍偏多,是全部按 label-master 推荐改、还是先收窄到 query 含「打开/进入」的子集(约 110 条)单独审一轮?",
"ts":"'$(date -Iseconds)'"}' >> $S
# R1 评测发现 regression
@@ -462,7 +561,8 @@ echo '{"step":"human-review","status":"running","run_id":"R1",
"ts":"'$(date -Iseconds)'"}' >> $S
# fallback:只写 reason 也能跑(前端会启发式切分),但不如结构化清晰
echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量 412 命中规则上限,需要人工定更精细 pattern 收窄","ts":"'$(date -Iseconds)'"}' >> $S
# 注意 reason 里的"412"也是 label-master 预审后残留数,不是原扫数
echo '{"step":"human-check","status":"running","run_id":"R0","reason":"label-master 预审后残留 412 条仍超阈值,需要人工定更精细 pattern 收窄","ts":"'$(date -Iseconds)'"}' >> $S
```
**顺序不能反**:先 echo gate entry → 再 chat reply 给用户。否则用户先看到聊天问话、UI 里却没卡,会困惑"流程是不是卡死了"。
@@ -476,7 +576,7 @@ echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量
| `run_id` | ✅ | 当前所在轮次(决定 gate 插在哪个 section 后) |
| `summary` | 🔼 | **现状一句话**:跑了什么、关键数字。例:`R0 baseline 完成;专项 58.89%(53/90)37 错全为 complex 误判` |
| `proposal` | 🔼 | **打算怎么干**:每个 H 单独说"H? (类型):具体做什么"。详见下面规则 |
| `ask` | 🔼 | **让用户选什么**用问句给出 A/B 选项。例:`是否按 H1+H2 进 R1 train?还是先只跑 H1 验证一轮?` |
| `ask` | 🔼 | **让用户选什么**必须是真分叉(用户的判断能改变下一步动作)。例:`label-master 预审后残留 287 条偏多,全改、还是收窄到「打开/进入」子集(~110 条)单独审一轮?` ⚠️ 写候选数量必须是 §4.0.1 Step A.5 label-master 预审后的残留数,不是原扫数。⛔ 反例:`H1+H2 一起 vs 只跑 H1`——假设组合是你定的,不甩给用户 |
| `reason` | ⭕ | 兜底用:没写 summary/proposal/ask 时前端会拿 reason 做启发式切分。但**优先用结构化三段**,别只写 reason |
| `ts` | ✅ | ISO 时间戳 |
@@ -496,6 +596,12 @@ echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量
- 流程自洽说明(`需要人工逐条审 1/0``走 sanity check``过 label-master 复核`)—— 这些是 agent 内部流程,对用户决策没用
- 候选量区间括号注释(`100~150 触发 51-200 区间`)—— 数量 OK,区间归属归属是规则细节,删
**`ask` 字段尤其要注意:**
**不要把"H? 假设组合"作为分叉抛给用户**——如 "H1+H2 一起 vs 只跑 H1"、"先做 H1 还是先做 H2"。假设的拆分和组合是 agent 自己根据信号决定的,决定完写进 proposal 公布即可,不需要用户拍板。`ask` 只在**真有分叉**时才写:候选量收窄策略、回滚还是续训、目标子集换思路 等用户判断能改变下一步动作的场景。
如果一个 R0/R{n} 没有任何真分叉(只是想"汇报+确认"),不要硬凑 ask、也不要写 gate——直接进下一步。
写 proposal 时问自己:**"这句话如果交给一个新加入的产品同学看,他能不能 5 秒内明白要干什么"**。能 = ✅;得回头查 §X.X.X 才能懂 = ❌,重写。
**用户拍板回复后**append `status:"complete"` 同 step + run_id 一行,gate 卡变 COMPLETED;然后才继续往下走(继续/回滚/调参)。
@@ -524,7 +630,7 @@ xiaomi_cloudml:
|---|---|---|
| `results/iteration_log.jsonl` | 每轮假设/干预/判定完整记录 | hypothesis / log |
| `results/error_registry.jsonl` | 跨轮错误追踪(case_hash → 出错轮次) | log |
| `results/workflow<runDic>.md` | 每轮回归分析报告 | dist-analysis / report |
| `results/workflow<runDic>.md` | 每轮回归分析报告 | dist-analysis |
| `output/relabel_candidates_<runDic>.csv` | **阶段一**:预计修改训练集候选清单(complex 翻转候选等) | **dist-analysis(分层结果分析)** |
| `results/data_clean_<runDic>/modified_samples.jsonl` | **阶段二**:确认修改最终落盘(用户审核 1/0 后实际生效的改动) | **augment(数据增强)** |
| `results/data_clean_<runDic>/deleted_samples.jsonl` | 旧数据清洗存档(删除项) | — |
+189 -47
View File
@@ -16,12 +16,11 @@
用户发 **"开始,<需求集合名>"**(如 `"开始,icl_test"`)→ **立即用当前模型跑一轮评测**,不再中途确认参数,直到报告完成:
1. **Setup 检查**cml 环境 + SSH key(见下)、从 `config.yaml` 读默认参数、初始化 `error_registry.jsonl`
2. **准备评测参数**:确定 `model_path_new`(当前模型)/ `model_path_old`(基线),自动分配 runDic(扫描 `run_history_dir` 最大值 +1;首次评测两者设为同一基准模型
2. **准备评测参数**:确定 `model_path_new`(当前模型)/ `model_path_old`(基线),runDic 一律由 `eval "$(./scripts/resolve_run_ids.sh)"` 解析出来——**不要自己 `ls workflow5 \| tail -1` 心算 +1**;首次评测两者设为同一基准模型
3. **CML 评测**Step 0):执行 `cml workflow run`,后台轮询 `metric_diff/lark_template.json` 直到结果就绪
4. **分层结果分析**(Step 1):按优先级逐层检查 需求集合(≥95%)→ 大盘车载(降幅≤0.3%)→ specific test(降幅≤1%
5. **问题分析 & 报告**Step 2):根因归类(reward/data/格式/hparam)、跨轮 diffpersistent/new/regressed)、需求集合深度分析(训练数据关联 + reward 对齐),写入 `results/workflow<runDic>.md`
6. **决策分支**:全部达标 → 部署并结束;否则 → 形成假设(Step 3)→ 按归因干预:数据增强(Step 4)/ 改 reward / 改格式 / 调超参 → SFT 训练(Step 5)→ 记录到 `iteration_log.jsonl`(Step 6)→ 回到第 3 步评测新 checkpoint
7. **全程不打断用户**,循环直到达标或人类打断
4. **分层结果分析 + 问题分析 & 报告**(Step 1):按优先级逐层检查 需求集合(≥95%)→ 大盘车载(降幅≤0.3%)→ specific test(降幅≤1%;做根因归类(reward/data/格式/hparam)、跨轮 diffpersistent/new/regressed)、需求集合深度分析(训练数据关联 + reward 对齐)、SFT 天花板诊断,写入 `results/workflow<runDic>.md`
5. **决策分支**:全部达标 → 部署并结束;否则 → 形成假设(Step 3)→ 按归因干预:数据增强(Step 4)/ 改 reward / 改格式 / 调超参 → SFT 训练(Step 5)→ 记录到 `iteration_log.jsonl`(Step 6)→ 回到第 3 步评测新 checkpoint
6. **全程不打断用户**,循环直到达标或人类打断
> **关键**"开始"的第一个动作永远是**评测当前模型**,而不是直接训练。先看清楚当前模型在各个集合上的表现和错误分布,再基于证据形成本轮假设 → 做干预。**不要没看评测就开始训练。**
@@ -64,7 +63,7 @@ ssh -T git@git.n.xiaomi.com
"$AUTORESEARCH_CHAT_ROOT/ai-planning"
```
之后所有训练数据读写都走 `$AUTORESEARCH_CHAT_ROOT/ai-planning/...`(包括 augment_<runDic>.jsonl 写入、近邻检索、训练集修改)。`prepare_and_train_sft.py` `Path(__file__)/../ai-planning` 解析数据目录,因为脚本被推到了 `$AUTORESEARCH_CHAT_ROOT/scripts/` 旁边,自然落在 chat 副本上
之后所有训练数据读写都走 `$AUTORESEARCH_CHAT_ROOT/ai-planning/...`(包括 augment_<runDic>.jsonl 写入、近邻检索、训练集修改)。`prepare_and_train_sft.py` 在脚本顶部统一以 `CHAT_ROOT = Path(__file__).resolve().parent.parent` 解析 chat 工作区(脚本本身位于 `$AUTORESEARCH_CHAT_ROOT/scripts/`),`AI_PLANNING``zk_trainer_dir` 都基于此推导,**不要在 agent 侧手动 patch `Path(__file__)`**——以前需要改两处的坑已在脚本侧统一
zk_trainer 的 clone 在首次 SFT 前进行(见 §5.0),目标也是 `$AUTORESEARCH_CHAT_ROOT/zk_trainer`
@@ -185,11 +184,11 @@ for k in prev:
**决策**
- 全部达标 → 部署(`python modules/cloudml_deploy.py <rl_checkpoint>`),记录,循环结束
- 不达标 → 进入 Step 2,**分析顺序:需求集合 → 车载大盘 → specific test**
- 不达标 → Step 1 内继续做问题分析(见下文),**分析顺序:需求集合 → 车载大盘 → specific test**
> **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接进入 Step 2 分析基准模型的错误分布,为后续迭代建立对照基准。
> **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接 Step 1 内分析基准模型的错误分布,为后续迭代建立对照基准。
### 2. 问题分析 & 报告
### 1.A 问题分析 & 报告(在 Step 1 同一 step 内继续做,不再单独开 step)
目标不是"列错误",是回答三个问题:
- **根因属于哪一类?**reward / data / 格式)
@@ -207,7 +206,7 @@ for k in prev:
| 旧对新错率 | B 数 / 总数 × 100% |
| 相对基线变化 | 新模型准确率 − 基线准确率(百分点) |
| 准确率 | 该子集 GSB 统一准确率(`cleaned_predict` vs `label` |
| 分流错误 | `origin_predict_base``ComplexTask``origin_predict_dev``complex=false` 前缀开头。complex 判断本身错了,**即使 cleaned tag 相同也计为错** |
| 分流错误 | base 模型和 dev 模型对该 case 的「是否复杂」二值判断不一致(一边判复杂、一边判不复杂),cleaned tag 是否相同不影响判定。具体怎么从两列原始输出里抽出「是否复杂」这个判断,由 agent 分析前先 head 当前评测产出反推,**不要照抄历史固定字符串** |
| 语义错误 | `cleaned_predict_base != cleaned_predict_dev`,意图/tag 本身判错 |
| 持久错误 | 该 case 在上一轮也错(查 `error_registry.jsonl` |
| 新引入错误 | 该 case 在上一轮对,本轮错 —— **最危险的信号,说明上轮干预有副作用** |
@@ -216,8 +215,8 @@ for k in prev:
| 字段 | 含义 |
| --- | --- |
| `origin_predict_base` | 旧模型原始输出,可能是 `ComplexTask(tag="...")` |
| `origin_predict_dev` | 新模型原始输出,始终以 `complex=true/false` 前缀开头 |
| `origin_predict_base` | 旧模型原始输出。具体格式(是否含 `complex=` 前缀、tag 包装、自定义 class 名等)以当前评测产出为准,**分析前 head 一下实物** |
| `origin_predict_dev` | 新模型原始输出。同上,格式以当前产出为准 |
| `cleaned_predict_*` | 清洗后输出,用于 GSB 对比 |
| `label` / `code_label_base` | ground truth`label` 为空时回退解析 `code_label_base` |
@@ -251,7 +250,104 @@ for k in prev:
2. **失败 case 与训练数据的关联**:对每个失败 case 在 `ai-planning/data/train_set/zk_intent/` 下所有 `.jsonl`(包括历史增强 `augment_*.jsonl` 和原始种子训练文件,排除 `*_valid.jsonl`)里做近似检索(前 3 近邻),三档判定:
- **无近邻**(最高相似度 < 阈值)→ 分布外,补数据
- **有近邻 + 近邻 label 与本 case gold 全部一致** → 训练数据正确,SFT 学不动 → 加 epoch / 改 reward
- **有近邻 + 任一近邻 label 与 gold 矛盾** ⚠️ → **训练集打错标**,必须**逐条列出该 mislabeled 训练样本**`{train_file:line, hash, 当前 label, 应改 label, 与失败 case 相似度, 违反的标签规则}`,进入下一轮的数据修订清单
- **有近邻 + 任一近邻 label 与 gold 矛盾** ⚠️ → **训练集打错标**,必须**落盘到 `$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_<runDic>.csv`**,列固定为 `file,line,query,old_label,是否改(1/0),建议新label`(注意:CSV 落 `output/`**不是** `results/`;前端「分层结果分析」卡片就读这一份)。**禁止**只把 mislabeled 列表打印到 stdout 或仅写进 workflow.md——卡片读不到 CSV 等同于本步漏交。本轮无 mislabel 也要写一份只含表头的空 CSV,明示"已检查、无候选"
⚠️ **判别器自检(写完任何"把 raw output 抽成可比对值"的函数后、跑全量前必做,跳过=本步无效)**
适用范围:凡是从原始模型输出里抽出某个判定值供后续比对/统计的函数都算判别器,无论抽的是什么——
- 二值(如 complex true/false、是否为 Agent
- 字符串/枚举(如 tag 名、function 名、code label
- 结构化字段(如某个 slot 的值)
1. **采端点**(覆盖**所有要区分的类**,每类训练侧 ≥1 条 + eval 侧 ≥1 条;二分类→4 条,N 分类→≥2N 条):
- 训练侧:`head` 几条 `train_set/zk_intent/*.jsonl`,靠 cleaned label / 现成 gold 字段挑出每类各 ≥1 条**已知真值**的样本。
- eval 侧:`head` 几条评测 CSV 的 `origin_predict_base` / `origin_predict_dev` 列实物,肉眼判一下属于哪类,每类各 ≥1 条。
- **禁止**只 head 一条就开写代码——一条样本不能验证判别器对所有类都能分对。
2. **跑判别器在端点上**(用真实抓到的字符串当输入,**不要**用想象出来的格式):
```
判别器(已知 X 类的训练样本 output) == "X" # 对每个类都跑一遍
判别器(已知 X 类的 eval baseline 输出) == "X" # 训练侧+eval 侧两侧都过
```
任一不对 → 判别器没对齐当前数据格式(最常见死法:把历史文档/旧版本里的 class 名/字符串当成 hardcoded substring 写进了 `'XXX' in out`,结果当前数据里根本没出现过这个字面量),**禁止跑全量**,回去重写。
3. **最终报告强制并列原文**:每条候选必须同时打印**原始 output 字符串前 80 字符**和**判别器返回值**,读者肉眼可对一遍——光报"近邻一致 N 条 / mislabel M 条"不够。
4. **零计数兜底**mislabel 跑出 = 0 时**禁止**直接结论"训练集没问题",必须先回去验步骤 2 的端点是否全过——zero-count 默认是判别器 bug 信号、不是结论信号。
⚠️ **算法选型**:错例集 O(几十)、训练集 O(几万) — 不要用复杂度高、要把训练集全量物化成稀疏矩阵或 embedding 的算法(如 `sklearn.TfidfVectorizer.fit_transform` 全量 + cosine — 实测 35k 行字符 n-gram 600s 仍 timeout)。
⚠️ **>1 万行训练集做近邻必须先粗筛后精排,不许直接 O(M·N) 全量比对**——37 错例 × 30k 训练 = 1.1M 次精排,朴素写法实测 ~3 min(v32 R0 case:错例侧逐条扫训练集 + 每例剪枝仍 182s,且后续每加一步 mislabel 定位都要再跑一遍)。两段式:
1. **粗筛(倒排索引,全表只扫一次)**:训练集一次性建 `token → set(line_idx)` 倒排表,key 用 query 的 2-gram 字符 token30k 行 query 通常 3k5k 唯一 token,建表 O(N) 秒级)。
2. **错例侧逐条取候选**:每条错例提它自己的 2-gram tokensunion 训练集对应 posting list → 通常每条得到 50–300 候选行(不是 30k)。
3. **精排(小集合上做精细打分)**:仅对粗筛候选跑 Jaccard / char-overlap / Levenshtein 取 top-3。
预算对照(37 × 30k):朴素 O(M·N)=1.1M 次精排(~3 min);倒排粗筛后 ~37×200=7.4k 次精排(~10s)。错例侧的"轻量剪枝"只是常数优化,**不能替代粗筛**——粗筛把候选基数砍到 1/100 才是数量级提速的来源。
⚠️ **训练集 + 倒排索引必须 pickle 缓存到 /tmp,不要每个脚本重建**dist-analysis 一轮要跑 kn_search / judge_check / mislabel_list / cross_subset 等多个脚本,每个都要读 30k 行 all_train.jsonl + 重建倒排索引——v32 R0 实测 5 次重读累计 ~50s 全是浪费。pod `/tmp` 是本地 SSD`pickle.dump` 之后所有后续脚本 unpickle <1s。模板:
```python
# 每个会读 all_train 的脚本头部统一这一段,cache key 用文件 mtime 自动失效
import os, json, pickle
from pathlib import Path
ALL_TRAIN = Path(os.environ['AUTORESEARCH_CHAT_ROOT']) / 'ai-planning/data/train_set/zk_intent/all_train.jsonl'
CACHE = Path(f'/tmp/all_train_{int(ALL_TRAIN.stat().st_mtime)}.pkl') # mtime 当版本号
IDX_CACHE = Path(f'/tmp/all_train_idx_{int(ALL_TRAIN.stat().st_mtime)}.pkl')
if CACHE.exists():
rows = pickle.loads(CACHE.read_bytes()) # <1s
else:
rows = [json.loads(l) for l in ALL_TRAIN.open('r', encoding='utf-8')] # 515s NFS
CACHE.write_bytes(pickle.dumps(rows))
# 倒排索引同样缓存(first script 建好就一直复用)
if IDX_CACHE.exists():
inv = pickle.loads(IDX_CACHE.read_bytes())
else:
inv = {}
for i, r in enumerate(rows):
q = r.get('query', '')
for tok in {q[k:k+2] for k in range(len(q)-1)}: # 2-gram
inv.setdefault(tok, set()).add(i)
IDX_CACHE.write_bytes(pickle.dumps(inv))
```
忌:每个脚本头都重写一遍 `for line in open(jsonl): json.loads(line)`;用 `/tmp/<runDic>.pkl` 而忽略 mtime(训练集中途被改不失效);把 cache 写到 NFS(白做了)。
⚠️ **stdout 是下一轮 LLM 的 input,分析脚本只 print 决策聚合,明细落 `/tmp/<step>_<runDic>_detail.json`**:每个分析脚本的 stdout 会原样喂回 LLM 当下一轮上下文,每 KB 都要 prefill。v32 R0 实测 kn_search 一次 stdout 18KB37 错例 × 3 邻居 × 6 列)、judge_check 4.7KB、mislabel 11.3KB,整个 dist-analysis 累计 ~50KB 全是重复模式行。kn_search 模板(其他步骤同构):
```python
# 不要 print 整张 candidate × neighbors 表;只 print 三档分布 + 每档 top-3 范例
import json, collections, os
buckets = collections.Counter()
exemplars = collections.defaultdict(list)
detail = [] # 全量明细,落盘不 print
for case in failed_cases:
neigh = topk(case, k=3) # 粗筛 → 精排已就位
verdict = classify(case, neigh) # 'no_neighbor' / 'consistent' / 'mislabel'
buckets[verdict] += 1
if len(exemplars[verdict]) < 3:
exemplars[verdict].append({
'query': case['query'][:50],
'gold': case['gold'],
'top_neigh': [{'q': n['query'][:50], 'label': n['label']} for n in neigh],
})
detail.append({'case': case['idx'], 'verdict': verdict, 'neigh': neigh})
detail_path = f'/tmp/kn_search_{os.environ["RUN_DIC"]}_detail.json'
json.dump(detail, open(detail_path, 'w'), ensure_ascii=False)
# 只 print 决策需要的聚合(目标 < 2KB)
print(f'[kn_search] total={len(failed_cases)} buckets={dict(buckets)} detail={detail_path}')
for v, exs in exemplars.items():
print(f'-- top-3 of {v} --')
for e in exs:
print(json.dumps(e, ensure_ascii=False))
```
忌:把每条错例的 k 个邻居全 print;`DataFrame.to_string()` 直接喂 stdout(自带 padding,一张表 2KB+);用 `cat > /tmp/x.py <<EOF ... EOF` 写脚本(runtime 会把每行 PS2 提示符 `>` 回显进 stdout,一次累计 1–2KB),改 `python3 - <<'PY' ... PY` 或 `python3 -c "$(base64 -d <<<'...')"` 注入。
⚠️ **大文件 I/O**>10MB 量级的远端 CSV/JSONL(如本步要读的 `specific_comparison.csv`,常见 90+MB),**先用 bash `awk/grep/head` 在远端切目标子集再处理**。直接 `pd.read_csv` 整体加载、或把 DataFrame 当返回值穿过工具调用 → 会撞 runtime 输出/返回大小限制(实测 99MB 直接读报"太大了")。
⚠️ **执行通道**:所有 python 一律走 `bash` + `/tmp/xxx.py` + `python3 -u … | tee /tmp/xxx.log`,禁用 `python_exec`。`python_exec` stdout 全缓冲、turn 结束才 flush,超时被 kill 时 buffer 直接丢光(实测 600s/1800s 撞墙后 `tool_result` 没有任何 stdout 字段)。bash + tee 模式实时落盘,超时也能 `tail /tmp/xxx.log` 看到死在哪个 phase。
3. **Reward 对齐检查**(全量失败 case,不抽样):用 `zk_reward_fn` 对"正确 label"和"实际输出"分别打分,验证 reward 方向是否和准确率一致。如果 reward 给错误输出的分更高 → reward 函数本身就有问题。
#### 2.4 根因归类
@@ -499,10 +595,14 @@ def get_label(row: dict) -> str:
return clb.strip("[]'").replace('\\n', '\n ')
def classify(row: dict) -> tuple[bool, bool]:
"""返回 (is_triage_err, is_semantic_err)"""
opb, opd = row['origin_predict_base'], row['origin_predict_dev']
"""返回 (is_triage_err, is_semantic_err)
is_triage_err 的判定无法用通用代码——agent 先 head 当前
origin_predict_base / origin_predict_dev 实物,根据真实格式现写 parser
抽出 base / dev 的「是否复杂」二值判断再比对。
"""
cpb, cpd = row['cleaned_predict_base'], row['cleaned_predict_dev']
is_triage = 'ComplexTask' in opb and opd.startswith('complex=false')
is_triage = ... # TODO: 见 docstring,按当前评测产出格式现写
is_semantic = cpb != cpd
return is_triage, is_semantic
@@ -544,7 +644,7 @@ for r in b_rows:
stats[sc][tag] += 1
```
### 3. 本轮假设(基于 Step 12 的证据)
### 3. 本轮假设(基于 Step 1 的证据)
**只有证据充分时才往下走。** 在开始训练前,写入 `results/iteration_log.jsonl` 的 `hypothesis` 字段:
- **假设**:本轮要验证什么?(例:"分流错误主因是 reward 对 complex 误判的惩罚太弱"
@@ -557,14 +657,14 @@ for r in b_rows:
### 4. 数据生成(GPT-5.4 从 badcase 增强)
**只在 Step 2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由:
**只在 §2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由:
| 归因 | 本步动作 | 跳到哪步 |
|---|---|---|
| `data` | 做 Step 4 数据增强(在已有数据基础上增删改) | Step 5(SFT 重训) |
| `格式`prompt / label schema 不对齐) | 改 prompt 模板或 label 渲染逻辑 | Step 5SFT 重训,格式变了必须重新 SFT) |
| `hparam` | 改 `config.yaml`LR / batch / epoch 等) | Step 5SFT 重训) |
| 假设本身站不住(Step 2 证据不支持 Step 3 的假设) | 不做任何训练 | **回 Step 3**,重写假设 |
| 假设本身站不住(Step 1 证据不支持 Step 3 的假设) | 不做任何训练 | **回 Step 3**,重写假设 |
换句话说:Step 4 是"data 归因专属"的干预入口,其他归因各有各的干预点,往下找对应的 Step 就行。
@@ -643,14 +743,44 @@ with open(out_csv, 'w', encoding='utf-8-sig') as fp:
候选阶段(246 条等量级)的 CSV 必须写到 `output/relabel_candidates_<runDic>.csv` 让分析卡片可读;确认修改阶段(用户审核后落盘的 135 条)写入 `results/data_clean_<runDic>/modified_samples.jsonl` 让数据增强卡片可读。**不要混落**——把候选 CSV 写到 `/tmp/` 或 `data_clean_/` 都会让 UI 看不到。
**Step B:量级判定(决定是否走人审)**
> **runDic 命名语义(backend 已对齐,改 backend 时勿 revert**augment 类产物(`data_clean_<N>/`、`augment_raw/augment_<N>_raw.jsonl`、`zk_intent/augment_<N>.jsonl`、`label_master_review.jsonl`)的 `<N>` 永远是**「源轮次的 runDic」**,也就是 `R{n-1}.runDic`——agent 在 R{n} 跑 augment 时处理的是 R{n-1} 评测落地的错例,文件命名沿用 R{n-1} 的 workflow id。Backend 的 step-detail 端点 (`server.py` 内 `_step_artifact_paths('augment', ...)` 调用方) 在 `step=='augment'` 时也是查 `R{n-1}` 的 runDic,跟落盘命名一一对应。其他 stepcml / dist-analysis / hypothesis / gold-drift)的产物按本轮自己的 runDic 命名,没有偏移。**不要把 augment 产物改名成 R{n} 的 runDic**——会同时打破历史归档和 backend 解析。
>
> ⚠️ **runDic 不许手算**augment 落盘、SFT yaml、归档目录、cml workflow run 用的 runDic **必须**从 `scripts/resolve_run_ids.sh` 取,不准 agent 自己 `ls workflow5 | tail -1` 然后心算 +1。规则简单到没必要靠模型:
>
> ```bash
> eval "$(./scripts/resolve_run_ids.sh)"
> # 现在 shell 里有:
> # $SFT_RUNDIC = R{n-1}.runDic augment 落盘 / yaml / 归档全用这个,不 +1)
> # $EVAL_RUNDIC = R{n-1}.runDic + 1 (仅 cml workflow run 时使用,唯一一次 +1
> ```
>
> 已踩坑:R2 augment 把 `data_clean_<N>/` 写成了 `EVAL_RUNDIC`(多 +1 一次),跳过了 R1.runDic 整段命名空间,前端 augment 卡片直接读不到。**写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。
| 候选量 | 处理 |
**Step A.5label-master 预审(量级判定之前必跑,强制)**
把 Step A 产出的候选交给 label-master 做语义判定,**用 label-master 的"推荐标签"覆盖候选原始推测的 new_label**,目的是在拿去人审之前先消化掉 label-master 自己就能定的那部分,缩小残留候选量。
具体步骤:
1. 抽出每条候选的 `(file, line, query, old_label, suspected_new_label)`
2. **逐条**调 `Skill(skill="label-master", args=...)`——每次只传**一条** `(query, old_label)`**不**给 suspected_new_label,避免锚定),让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 输出 → `{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`。
- ⚠️ **禁止把多条 query 塞进同一次 Skill 调用**——批量调用会让 label-master 在多条之间相互锚定,分错率显著升高(已踩坑)
- 可在同一轮 assistant response 里并发多次 Skill 调用(建议 ≤ 8 路并发)以提升吞吐
3. 把 label-master 的"推荐标签"**回写到** `relabel_candidates_<runDic>.csv` 覆盖原 `建议新label` 列;新增列 `verdict`、`label_master_理由`,便于回查
4. 收尾时按以下规则筛 candidate list**残留候选 = 真正进入 Step B 的列表**):
- `推荐标签 == old_label`label-master 不认同要改 → 从候选里**剔除**(这条原 label 可能本来就是对的)
- `推荐标签 != old_label`label-master 同意要改(不论与 suspected_new_label 是否一致)→ **保留**`new_label = 推荐标签`
**Step B:量级判定(基于 label-master 处理后的残留候选量)**
| 残留候选量 | 处理 |
|---|---|
| ≤ 50 条 | 程序化 sanity check + 自动改 |
| 51 ~ 200 条 | **必须**全量导出到飞书 sheet 让人逐条审 1/0(不抽样) |
| ≤ 50 条 | 程序化 sanity check + 按 label-master 推荐自动改(不再走人审) |
| 51 ~ 200 条 | **必须**全量导出到飞书 sheet 让人逐条审 1/0(不抽样);导出时带 `query / old_label / 推荐标签 / label_master_理由` 四列 |
| > 200 条 | **强制 H-i-T-L 介入**(触发条件 #3),让人定更精细 pattern 收窄 |
注:阈值仍按原来 autoresearch 的人审规则;变化只在于残留候选已经过 label-master 一遍语义筛减,避免把 label-master 自己就能定的条目也塞进飞书让人重审。
**Step C:备份(强制,覆盖前必做)**
```bash
@@ -725,7 +855,7 @@ ai-planning/data/train_set/zk_intent/
#### 4.1 输入
- **Badcase 来源**Step 2`纯模型GSB == 'B'` 的 case。按两个维度排优先级:
- **Badcase 来源**Step 1 里 `纯模型GSB == 'B'` 的 case。按两个维度排优先级:
1. **测试集维度**:本次需求集合 > 其他 specific test / 大盘(需求集合是当前瓶颈,优先补)
2. **跨轮维度**`new` / `regressed` > `persistent`(新引入/回退的先处理,持久错误兜底)
@@ -969,7 +1099,7 @@ def check_label_rules(query: str, label: str, history: list = None) -> tuple[boo
##### 触发时机
每轮 Step 2 错例分析后,对**所有未被 R1~R{N} 已知规则覆盖的错例**做自动归纳。
每轮 Step 1 错例分析后,对**所有未被 R1~R{N} 已知规则覆盖的错例**做自动归纳。
##### Confidence 的定义(两个独立指标,必须同时满足)
@@ -1103,14 +1233,14 @@ JSONL 行里的 `sub_cate` 字段仅用于内部路由/去重,归档时丢弃
#### 4.5 label-master 标签复核(落盘后、SFT 前,**强制**)
任何写入训练目录的修改/新增样本都必须经 [label-master skill](../../label-master/SKILL.md) 复核**两层**:格式 + 语义。**没过这关不许进 Step 5。**
任何写入训练目录的修改/新增样本都必须经 [label-master skill](../../label-master/SKILL.md) 复核:格式 + 语义。**没过这关不许进 Step 5。**
**复核范围**(两份必须过):
**复核范围**(两份必须过格式层;语义层只对 H2):
| 文件 | 来源 | 复核什么 |
|---|---|---|
| `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl` | H1 改标(complex 翻转 / tag 改写等) | 改后的 `output` 字段 |
| `$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl` | H2 仿写(新增训练样本 | `output` 字段 |
| 文件 | 来源 | 层 1(格式) | 层 2(语义,Skill 调 label-master |
|---|---|---|---|
| `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl` | H1 改标(complex 翻转 / tag 改写等) | ✅ 必跑 | ❌ 已在 §4.0.1 Step A.5 完成,**不再重跑**label-master 不会推翻自己的推荐,重跑只是浪费 token) |
| `$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl` | H2 仿写(新增训练样本,未经过 §4.0.1) | ✅ 必跑 | ✅ 必跑 |
**层 1:格式校验(确定性,自动跑)**
@@ -1128,16 +1258,17 @@ python skills/label-master/scripts/validate_label_output.py \
任一行 failtag 不存在 / Agent 包装错 / function 引用错)→ **必须修,不许直接跳过**。修完原地重跑直到全过。
**层 2:语义复核(Skill 调用 label-master Agent**
**层 2:语义复核(Skill 调用 label-master Agent,仅 H2**
格式过了不代表标对了。每条修改/新增样本要用 label-master 做边界判断
H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重跑**。H2 仿写是 §4.2 GPT 新增的样本,没经过 Step A.5,必须在此补一次语义判定
1. 抽出 `(query, output_after)` `(query, output)`,按 `sub_cate` 分组(每组 ≤30 条作为一批)
2. 每批用 `Skill(skill="label-master", args=...)` 调用,args 里给
- 全部 `(query, 当前 label)`
- 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定每条
- 输出格式:每条 → `{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`
3. 收集 verdict,写入 `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/label_master_review.jsonl`,每行一条 verdict
1. `augment_<runDic>.jsonl` 抽出 `(query, output)`
2. **逐条**调 `Skill(skill="label-master", args=...)`——每次只传**一条** `(query, 当前 label)`
- 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定该条
- 输出格式:`{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`
- ⚠️ **禁止把多条 query 塞进同一次 Skill 调用**——批量调用会让 label-master 在多条之间相互锚定,分错率显著升高(已踩坑)
- 可在同一轮 assistant response 里并发多次 Skill 调用(建议 ≤ 8 路并发)以提升吞吐
3. 收集 verdict,写入 `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/label_master_review.jsonl`,每行一条 verdict(只含 H2 条目)
**verdict 处置规则(自动)**
@@ -1345,19 +1476,25 @@ 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 命名空间。
```bash
cd "$AUTORESEARCH_CHAT_ROOT"
./scripts/submit_sft_via_cml.sh <RUNDIC> [PREV_RUNDIC]
eval "$(./scripts/resolve_run_ids.sh)" # 解析出 $SFT_RUNDIC / $EVAL_RUNDIC
./scripts/submit_sft_via_cml.sh "$SFT_RUNDIC" "$EVAL_RUNDIC"
# 例:R29 训练(上一轮 R28
./scripts/submit_sft_via_cml.sh 17756 17755
# 等价示例(仅供阅读,**实际调用一律走 eval 上面那行**):
# R29 上一轮已落盘的 workflow 是 17755,因此 SFT_RUNDIC=17755, EVAL_RUNDIC=17756
# ./scripts/submit_sft_via_cml.sh 17755 17756
```
`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`,否则直接报错退出。
脚本做的事:
1. 渲染 `scripts/sft_train_job.yaml.tpl` 为本轮 yaml(替换 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 评测
4. 任务 succeed → 验证 `sft_output/_SUCCESS` 存在 → 自动起 cml workflow run**评测的 `runDic=$EVAL_RUNDIC`**(这是整条流水线唯一一次 +1
5. 任务 failed/killed → 写日志报警
##### yaml 模板要点(`scripts/sft_train_job.yaml.tpl`
@@ -1523,9 +1660,14 @@ tail -f /tmp/r<RUNDIC>_logs/cml_watcher.log
1. **触发原因**:哪一条信号 + 具体数字
2. **现状量化**:目标子集 +X / 其他子集 -Y / 大盘 ±Z
3. **2-4 个选项**(用 AskUserQuestion 工具):每个选项含预期收益 + 风险
4. **推荐选项**:基于经验给出推荐(标 "(推荐)"
5. **本地产物**:全量错例 csv / 飞书 sheet 链接
3. **全量 case 直接铺进对话**(凡是"该不该改 / 该不该删 / 该不该新增 N 条"型决策都适用,**包括但不限于** H-i-T-L #1/#2/#3/#6 与 T1/T2/T3/T5):
- 必须把"经过 label-master 认证的全部候选"按 pattern 分组、每条一行(紧凑表)贴到**同一条** ask 消息体里,不是只给 CSV 路径或飞书链接,**也不要分段连发**——一段全铺,让用户一次滚完
- **不允许抽样、不允许"前 N 条样例"、不允许"代表 case"**——抽样让用户看不到边界外的长尾,决策无意义
- 每条至少含:`query / old_label / 推荐标签 / 理由(≤40字)`H1(改标)类必含 `file:line`H2(仿写)类必含 `pattern_id`
- **高置信 + 低置信(潜在影响半径 / 类似 case)都得铺,缺一不可**:当 ask 里出现「确认要改 X 条 + 还有 Y 条类似的 / 潜在影响 Y 条 / 同 pattern 还有 Y 条疑似」这种二段叙述时,**Y 条也必须全量铺进同一条对话消息**(同样按 pattern 分组、紧凑表),并对每条标 `confidence=high / low`。理由:用户的决策本身就是「只改 X」vs「扩到 X+Y」vs「再收窄 pattern」,看不到 Y 就只能瞎选。**只展示高置信 X 条、把 Y 条藏在数字背后**视为违反 skill。
4. **2-4 个选项**(用 AskUserQuestion 工具):每个选项含预期收益 + 风险
5. **推荐选项**:基于经验给出推荐(标 "(推荐)"
6. **本地产物**:全量错例 csv / 飞书 sheet 链接(作为对话铺陈的备份和事后回查渠道,**不替代**第 3 项)
### 介入后的恢复
@@ -26,7 +26,8 @@ from pathlib import Path
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)
AI_PLANNING = Path(__file__).resolve().parent / "ai-planning"
CHAT_ROOT = Path(__file__).resolve().parent.parent
AI_PLANNING = CHAT_ROOT / "ai-planning"
TRAIN_SET_DIR = AI_PLANNING / "data" / "train_set"
DATA_TRAIN_DIR = AI_PLANNING / "data_train"
GENERATE_TRAIN_SCRIPT = DATA_TRAIN_DIR / "generate_train.py"
@@ -148,7 +149,7 @@ def run_sft_training(data_dir: str, model_path: str, model_type: str, train_outp
epochs: int = 3, lr: float = 1e-5, max_seq_length: int = 1024,
per_device_batch: int = 1, grad_accum: int = 1):
"""基于 zk_trainer 的 accelerate launch 方式启动 SFT 训练。"""
zk_trainer_dir = str(Path(__file__).resolve().parent / "zk_trainer")
zk_trainer_dir = str(CHAT_ROOT / "zk_trainer")
if not os.path.isdir(zk_trainer_dir):
log.error(f"zk_trainer 目录不存在: {zk_trainer_dir}")
return None
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# resolve_run_ids.sh —— SFT/EVAL runDic 单一可信来源
#
# 规则(与 program.md §746 + §5.2 对齐):
# SFT_RUNDIC = workflow5/ 下「已落盘」(含 metric_diff/lark_template.json)的最大 runDic
# 即 R{n-1}.runDic —— augment 落盘、SFT yaml、modified_samples 归档全用这个值
# EVAL_RUNDIC = SFT_RUNDIC + 1
# 仅 cml workflow run 提交本轮评测时使用
#
# 用法:
# eval "$(./scripts/resolve_run_ids.sh)"
# echo "$SFT_RUNDIC $EVAL_RUNDIC"
#
# 失败行为:找不到任何已落盘 workflow 时打 ERR 到 stderr 并 exit 1,调用方 set -e 时直接终止。
set -euo pipefail
WF_ROOT=${WF_ROOT:-/mnt/xiaoai-zk-model-train-tj5/workflow5}
if [ ! -d "$WF_ROOT" ]; then
echo "ERR: WF_ROOT=$WF_ROOT 不存在或不可读" >&2
exit 1
fi
# 只挑「已落盘」的 workflow:必须存在 metric_diff/lark_template.json
# 这样可以避免 CML 创建了空目录就被算成最大值
MAX=$(
for d in "$WF_ROOT"/workflow*; do
[ -d "$d" ] || continue
[ -f "$d/metric_diff/lark_template.json" ] || continue
name=$(basename "$d")
echo "${name#workflow}"
done | sort -n | tail -1
)
if [ -z "$MAX" ]; then
echo "ERR: $WF_ROOT 下没有任何已落盘的 workflow(含 metric_diff/lark_template.json" >&2
exit 1
fi
echo "SFT_RUNDIC=$MAX"
echo "EVAL_RUNDIC=$((MAX+1))"
@@ -0,0 +1,117 @@
jobName: "sft-train-r{RUNDIC}"
description: "AutoResearch R{RUNDIC} SFT training (40k+ samples, qwen3-4B base, 3 epochs FSDP2)"
accessType: PUBLIC
imageConfig:
imageUrl: micr.cloud.mioffice.cn/vllm-image/ai-arch-llm-prod:vllm-v0.12.0-f098b188
imageCommand: |-
set -ex
cd {AUTORESEARCH_ROOT}
# 0a. 验环境 + 装缺的包(vllm 镜像 py3.12 + torch 已含)
python3 --version
python3 -c "import torch; print(f'torch={torch.__version__}')"
pip install -q --no-deps "accelerate==1.7.0" 2>&1 | tail -3
pip install -q --ignore-installed blinker 2>&1 | tail -2
pip install -q peft 2>&1 | tail -3
pip install -q wandb bitsandbytes 2>&1 | tail -3
pip install -q luigi mlflow scikit-learn openpyxl pyyaml sentencepiece tiktoken protobuf pynvml datasets 2>&1 | tail -3
pip install -q "transformers>=4.45" 2>&1 | tail -3
python3 -c 'import torch, accelerate, transformers, peft; print(torch.__version__, accelerate.__version__, transformers.__version__, peft.__version__)'
# 0c. 把 HF cache 重定向到容器本地大盘(默认在 ~/.cache 可能是 juicefsmmap 易 SIGBUS
export HF_HOME=/tmp/hf_cache
export HF_DATASETS_CACHE=/tmp/hf_cache/datasets
export TRANSFORMERS_CACHE=/tmp/hf_cache/transformers
mkdir -p /tmp/hf_cache/datasets /tmp/hf_cache/transformers
df -h /tmp /dev/shm 2>/dev/null || true
# 强制 datasets 加载进内存而不是 arrow mmap(避免 /dev/shm 溢出 SIGBUS
export HF_DATASETS_IN_MEMORY_MAX_SIZE=20000000000
export HF_DATASETS_NUM_PROC=1
export TOKENIZERS_PARALLELISM=false
export OMP_NUM_THREADS=1
export MKL_NUM_THREADS=1
# 关闭 NCCL 用 shm 通信(改 socket 通道)
export NCCL_SHM_DISABLE=1
export NCCL_P2P_DISABLE=0
export NCCL_DEBUG=WARN
# 0b. 把上一轮产出搬走(cml job 内幂等,本地搬过就跳过)
if [ -d sft_output ] && [ ! -d sft_output_r{PREV_RUNDIC} ]; then
mv sft_output sft_output_r{PREV_RUNDIC}
fi
rm -rf sft_output
# 1. 组装数据
python3 {AUTORESEARCH_ROOT}/prepare_and_train_sft.py prepare \
--output_dir {AUTORESEARCH_ROOT}/sft_data
# 2. 启动训练(绝对路径,避免相对路径在容器内找不到 zk_trainer
python3 {AUTORESEARCH_ROOT}/prepare_and_train_sft.py train \
--data_dir {AUTORESEARCH_ROOT}/sft_data \
--model_path /mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507 \
--model_type qwen3 \
--train_output {AUTORESEARCH_ROOT}/sft_output \
--epochs 3 --lr 1e-5
# 3. 训练成功标记(被 watcher 检测)
[ -f {AUTORESEARCH_ROOT}/sft_output/config.json ] && \
touch {AUTORESEARCH_ROOT}/sft_output/_SUCCESS
# 挂载 wangsenhao + xiaoai-zk-model-train-tj5 + verl_zk 所在卷
juiceFsMountConfigs:
- volume: wangsenhao
juiceFsCluster: tj5-common
subPath: /
mountPath: /mnt/wangsenhao
readOnly: false
- volume: xiaoai-zk-model-train-tj5
juiceFsCluster: tj5-common
subPath: /
mountPath: /mnt/xiaoai-zk-model-train-tj5
readOnly: false
envConfigs:
- key: PYTHONPATH
value: {AUTORESEARCH_ROOT}/zk_trainer
- key: WANDB_DISABLED
value: "true"
- key: WANDB_MODE
value: offline
- key: VOLUME_PREFIX
value: /mnt/xiaoai-zk-model-train-tj5
- key: HF_HOME
value: /mnt/wangsenhao/.hf_cache
# h20-96g 8 卡 FSDP2
queueId: "6052"
priority: 5
preemptible: false
framework: pytorch
resourceConfigs:
- nodeRole: worker
nodeNumber: 1
perNodeResourceSpec:
resourcePriority: GUARANTEED
resourceName: cloudml.ng2h20-8-8.20-199
resourceNumber: 8
# 故障自动重试(节点级失败)
retryConfig:
enableRetry: true
maxRetryTimes: 2
policySets:
- NodeFailure
# 失败/完成飞书告警
alertConfig:
enableAlert: true
alertItems:
- alertConditions:
- FAILED
- SUCCEED
alertLevel: P2
alertReceivers:
persons:
- wangsenhao
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
# 用 cml custom_train submit 提交 SFT 训练,训练完成后自动起 cml workflow run 评测
#
# 用法:
# ./submit_sft_via_cml.sh <SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]
#
# 推荐调用方式(runDic 由 resolve_run_ids.sh 决定,不要手敲):
# eval "$(./scripts/resolve_run_ids.sh)"
# ./scripts/submit_sft_via_cml.sh "$SFT_RUNDIC" "$EVAL_RUNDIC"
#
# 语义(与 program.md §746 + §5.2 对齐):
# SFT_RUNDIC = R{n-1}.runDicaugment / yaml / 归档全用这个,不 +1)
# EVAL_RUNDIC = R{n-1}.runDic + 1(仅本步起 eval 时使用,仅在这一处 +1)
# PREV_RUNDIC = SFT_RUNDIC - 1(默认;只用作上一轮 sft_output 改名)
set -euo pipefail
SFT_RUNDIC=${1:?usage: $0 <SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]}
EVAL_RUNDIC=${2:?usage: $0 <SFT_RUNDIC> <EVAL_RUNDIC> [PREV_RUNDIC]}
PREV_RUNDIC=${3:-$((SFT_RUNDIC-1))}
# 防御:EVAL_RUNDIC 必须严格大于 SFT_RUNDIC,否则一定是调用方算错
if [ "$EVAL_RUNDIC" -le "$SFT_RUNDIC" ]; then
echo "[cml-sft] ❌ EVAL_RUNDIC ($EVAL_RUNDIC) 必须 > SFT_RUNDIC ($SFT_RUNDIC)" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT=${AUTORESEARCH_ROOT:-/mnt/wangsenhao/autoresearch-zk}
MODEL_OLD=${MODEL_OLD:-/mnt/wangsenhao/verl_zk/qwen4b_cispo_wokl_add_bvt_2/global_step_5/actor/huggingface}
TPL=${SFT_TRAIN_JOB_TEMPLATE:-$SCRIPT_DIR/sft_train_job.yaml.tpl}
YAML=/tmp/sft_train_job_r${SFT_RUNDIC}.yaml
if [ -f ~/.cloudml-cli/.profile ]; then
source ~/.cloudml-cli/.profile
else
echo "[cml-sft] ❌ 未找到 ~/.cloudml-cli/.profile,请先安装并初始化 cml" >&2
exit 1
fi
# 1. 渲染 yaml 模板(用 SFT_RUNDIC,不是 EVAL_RUNDIC
sed \
-e "s|{RUNDIC}|${SFT_RUNDIC}|g" \
-e "s|{PREV_RUNDIC}|${PREV_RUNDIC}|g" \
-e "s|{AUTORESEARCH_ROOT}|${ROOT}|g" \
"$TPL" > "$YAML"
echo "[cml-sft] yaml: $YAML (SFT_RUNDIC=$SFT_RUNDIC EVAL_RUNDIC=$EVAL_RUNDIC PREV_RUNDIC=$PREV_RUNDIC)"
# 2. 提交训练任务
SUBMIT_OUT=$(cml custom_train submit --filename "$YAML" 2>&1)
echo "$SUBMIT_OUT"
JOB_ID=$(echo "$SUBMIT_OUT" | grep -oE 't-[0-9]+-[a-z0-9]+' | head -1)
if [ -z "$JOB_ID" ]; then
echo "[cml-sft] ❌ 提交失败" >&2
exit 1
fi
echo "[cml-sft] ✅ JobID: $JOB_ID"
# 3. 后台 watcher:等训练成功 → 自动起评测(用 EVAL_RUNDIC
WATCHER_LOG=/tmp/r${SFT_RUNDIC}_logs/cml_watcher.log
mkdir -p "$(dirname "$WATCHER_LOG")"
nohup bash -c '
JOB_ID='"$JOB_ID"'
SFT_RUNDIC='"$SFT_RUNDIC"'
EVAL_RUNDIC='"$EVAL_RUNDIC"'
LOG='"$WATCHER_LOG"'
ROOT='"$ROOT"'
MODEL_NEW="$ROOT/sft_output"
MODEL_OLD='"$MODEL_OLD"'
EVAL_WORKFLOW_ID=f-20260408161444-wu3pz
EVAL_VERSION=v28
source ~/.cloudml-cli/.profile
echo "[$(date)] 等待 cml job $JOB_ID 完成..." >> "$LOG"
while true; do
STATE=$(cml custom_train describe "$JOB_ID" 2>/dev/null | grep -o "\"state\": \"[a-z]*\"" | head -1 | sed "s/.*: \"//;s/\"//")
case "$STATE" in
succeed)
echo "[$(date)] 训练成功" >> "$LOG"
break ;;
failed|killed)
echo "[$(date)] ❌ 训练 $STATE" >> "$LOG"
exit 1 ;;
*)
sleep 60 ;;
esac
done
# 验证产出
[ ! -f "$MODEL_NEW/_SUCCESS" ] && [ ! -f "$MODEL_NEW/config.json" ] && {
echo "[$(date)] ❌ 产出缺失" >> "$LOG"; exit 1
}
# 起评测(这里且仅这里用 EVAL_RUNDIC
echo "[$(date)] 启动 CML 评测 EVAL_RUNDIC=$EVAL_RUNDIC" >> "$LOG"
cml workflow run \
--workflow_id $EVAL_WORKFLOW_ID --version $EVAL_VERSION \
--global_inputs runDic=$EVAL_RUNDIC \
--global_inputs model_path_new=$MODEL_NEW \
--global_inputs model_path_old=$MODEL_OLD >> "$LOG" 2>&1
echo "[$(date)] cml workflow run 提交完成 (workflow$EVAL_RUNDIC)" >> "$LOG"
' > /tmp/r${SFT_RUNDIC}_logs/watcher_runner.log 2>&1 &
WATCHER_PID=$!
echo "[cml-sft] watcher PID: $WATCHER_PIDlog: $WATCHER_LOG"
# 4. 立即输出可查看命令
cat <<EOF
[cml-sft] 任务提交完成,关键命令:
查看任务状态: cml custom_train describe $JOB_ID
查看实时日志: cml custom_train logs $JOB_ID --follow
停止任务: cml custom_train kill $JOB_ID
watcher log: tail -f $WATCHER_LOG
训练完成后,evaluation workflow 会自动启动;评测产物在
/mnt/xiaoai-zk-model-train-tj5/workflow5/workflow${EVAL_RUNDIC}/
JobID: $JOB_ID
SFT_RUNDIC: $SFT_RUNDIC (yaml / sft_output 命名)
EVAL_RUNDIC: $EVAL_RUNDIC (workflow 评测目录)
EOF