chore(skill): slim down model-iteration skill files (-820 lines)

SKILL.md: 653→607 lines, program.md: 1757→1187 lines.
Merged duplicate content, replaced verbose explanations with references.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangsenhao
2026-05-26 17:03:08 +08:00
parent 4dbf59116f
commit 89e77140e7
2 changed files with 102 additions and 718 deletions
+24 -70
View File
@@ -417,58 +417,24 @@ echo '{"log":{"ts":"11:55:10","iter":"R1","text":"Step 1 dist-analysis: 写 work
1. **评测优先**`"开始"` 信号的第一个动作永远是评测当前模型,绝不直接训练 1. **评测优先**`"开始"` 信号的第一个动作永远是评测当前模型,绝不直接训练
2. **假设驱动**:每轮必须能回答"这轮验证了什么?学到了什么?下一轮改什么?" 2. **假设驱动**:每轮必须能回答"这轮验证了什么?学到了什么?下一轮改什么?"
3. **NEVER STOP**:全程不打断用户,循环直到达标或人类主动打断 3. **NEVER STOP**:全程不打断用户,循环直到达标或人类主动打断
4. **从 basemodel 训练**:每轮 SFT 的 `--model_path` 必须是 basemodel,禁止从上轮 ckpt 续训 4. **从 basemodel 训练**:每轮 SFT 的 `--model_path` = `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507`,禁止从上轮 ckpt 续训
5. **复杂度优先**:分析类任务(近邻检索 / 聚类 / 相似度统计)选算法前先估数据规模。**忌**对几万行训练集用 `sklearn.fit_transform` 这类要全量物化稀疏矩阵或 embedding 矩阵的 API — 单次 600s 仍可能 timeout。**>1 万行训练集做近邻必须先粗筛后精排(倒排索引 → 每错例 50300 候选 → 精排),禁止 O(M·N) 全量比对**——37×30k=1.1M 次朴素比较 ~3min,粗筛后 7.4k 次 ~10s。错例侧的"剪枝"只是常数优化,不能替代粗筛。详见 [references/program.md §2.3](references/program.md) 5. **近邻检索先粗筛后精排**:>1 万行训练集禁止 O(M·N) 全量比对;倒排索引 → 50-300 候选 → 精排。详见 program.md §2.3
6. **大文件先切片**>10MB 量级的远端 CSV/JSONL(如 `specific_comparison.csv`),先用 bash `awk/grep/head` 在远端切出目标子集再处理。**忌**直接 `pd.read_csv` 整体加载或把 DataFrame 当返回值穿过工具调用 — 会撞 runtime 的输出/返回大小限制(实测 99MB 直接读 → "太大了" 失败) 6. **大文件先切片**>10MB CSV/JSONL bash `awk/grep/head` 切子集,禁止 `pd.read_csv` 整体加载
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 自动失效。 7. **pickle 缓存 /tmp**训练集 + 倒排索引缓存到 pod `/tmp/`(mtime 作 key 自动失效),禁止每脚本重建
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` 注入,可以彻底消掉 `> > >` 噪声。 8. **stdout 分级 print**:只 print 决策聚合(<2KB),明细落 `/tmp/<step>_<runDic>_detail.json`
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` 看到死在哪。 9. **一律 bash + 脚本文件**`python3 -u /tmp/xxx.py 2>&1 | tee /tmp/xxx.log`,禁用 python_exec
## 达标条件 ## 达标条件
- **需求集合(目标集)** ≥ 95%(**最高优先级,一切以此为准** - **需求集合(目标集)** ≥ 95%(**最高优先级**)
- **大盘集(车载)** 降幅 ≤ 0.3%(次要,目标集未达 95% 前不因大盘微降而回滚/停训 - **大盘集(车载)** 降幅 ≤ 0.3%(次要)
- **specific test** 降幅 ≤ 1%(次要,同上 - **specific test** 降幅 ≤ 1%(次要)
⚠️ **优先级铁律**:目标集 95% 是主线任务。只要目标集还没到 95%,其他集合轻微下降(大盘 ≤1%、specific ≤2%**不构成回滚理由**,继续迭代。只有目标集已达标后,才需要关注并修复其他集合的 regression ⚠️ **优先级铁律**:目标集未达 95% ,其他集合轻微下降(大盘 ≤1%、specific ≤2%)不构成回滚理由。
## 主流程 ## 主流程
读取 `references/program.md` 获取完整的 Step 0-7 迭代循环规范。在任何操作前必须读取该文件 完整 Step 0-7 规范见 `references/program.md`**操作前必读**
每个 step 的执行模板(**强制**,配合上文「Pipeline UI 同步约定」R1):
```text
1. echo '{"step":"<key>","status":"running","ts":"..."}' >> $SESSION_OUTPUT/program-state.jsonl
2. (执行该 step 的实际操作 / 调用工具 / 等结果)
3. 成功: echo '{"step":"<key>","status":"complete","ts":"..."}' >> ...
失败: echo '{"step":"<key>","status":"failed","error":"...","ts":"..."}' >> ...
```
step key 跟 Step 编号映射:
| Step | step key | 说明 |
|------|----------|------|
| Step 0 | `cml` | CML 评测当前模型 |
| Step 0 (附带) | `gold-drift` | Gold drift 检查 |
| Step 1 | `dist-analysis` | 分层结果分析 + 问题分析 & 报告(写 workflow<runDic>.md |
| Step 3 | `hypothesis` | 形成假设(写 iteration_log |
| Step 4 | `augment` | 数据增强 / 生成 |
| Step (干预后) | `verify` | 修改返回验证 |
| Step 5 | `sft` | SFT 训练 |
| Step 6 | `log` | 记录迭代日志 |
| 循环 | `next-round` | 回 Step 0 评测新 ckpt |
```
LOOP:
Step 0: CML 评测(当前模型)+ Gold drift 检查
Step 1: 分层结果分析 + 问题分析 & 报告(需求集合 → 大盘车载 → specific test;根因归类 / 跨轮 diff / 天花板诊断 / 写 workflow<runDic>.md
Step 3: 形成假设(写入 iteration_log.jsonl
Step 4: 数据生成(仅 data 归因时做)
Step 5: SFT 训练(submit_sft.sh)→ 训练成功后下一轮评测用 submit_cml_eval.sh
Step 6: 记录结果(iteration_log.jsonl / error_registry.jsonl
→ 回 Step 0
```
## 文件结构 ## 文件结构
@@ -623,31 +589,19 @@ echo '{"step":"human-check","status":"running","run_id":"R0","reason":"label-mas
实际差别在 UI 上不大(都是横向 dashed 卡),区分主要为了让用户从标题就大致知道是开始前还是结果后的检查点。 实际差别在 UI 上不大(都是横向 dashed 卡),区分主要为了让用户从标题就大致知道是开始前还是结果后的检查点。
## CML 配置(快速查阅) ## CML 配置
```yaml `references/program.md` §5.2。
# ~/.config/cloudml/config.yaml
default_config_context: cloudml5-config
xiaomi_cloudml:
cloudml5-config:
xiaomi_auth_type: key
xiaomi_cloudml_endpoint: https://cnbj6-cloudml5.api.xiaomi.net
xiaomi_cloudml_workspace_id: 10065
```
## 结果文件 ## 结果文件 → 前端卡片映射
| 文件 | 用途 | 前端卡片 | | 文件 | 前端卡片 |
|---|---|---| |---|---|
| `results/iteration_log.jsonl` | 每轮假设/干预/判定完整记录 | hypothesis / log | | `results/iteration_log.jsonl` | hypothesis / log |
| `results/error_registry.jsonl` | 跨轮错误追踪(case_hash → 出错轮次) | log | | `results/error_registry.jsonl` | log |
| `results/workflow<runDic>.md` | 每轮回归分析报告 | dist-analysis | | `results/workflow<runDic>.md` | dist-analysis |
| `output/relabel_candidates_<runDic>.csv` | **阶段一**:预计修改训练集候选清单(complex 翻转候选等) | **dist-analysis分层结果分析)** | | `output/relabel_candidates_<runDic>.csv` | dist-analysis阶段一:候选) |
| `results/data_clean_<runDic>/modified_samples.jsonl` | **阶段二**:确认修改最终落盘(用户审核 1/0 后实际生效的改动) | **augment(数据增强)** | | `results/data_clean_<runDic>/modified_samples.jsonl` | augment(阶段二:落盘) |
| `results/data_clean_<runDic>/deleted_samples.jsonl` | 旧数据清洗存档(删除项) | — | | `results/augment_raw/augment_<runDic>_raw.jsonl` | augment |
| `results/augment_raw/augment_<runDic>_raw.jsonl` | GPT-5.4 仿写原始产物 | augment | | `ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl` | augment |
| `ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl` | 本轮合入训练集的增量(H1 重标 + H2 仿写) | augment | | `results/gold_drift/drift_<runDic>.json` | gold-drift |
| `results/gold_drift/drift_<runDic>.json` | Gold drift 检测结果 | gold-drift |
| `results/label_rules.md` | 已确立的标签规则集(R1~RN) | — |
**两阶段训练集修改的展示约定**:候选阶段(量级判定 + 人审 1/0 之前)写在 `output/relabel_candidates_<runDic>.csv`,前端「分层结果分析」卡片读取展示供人审;确认阶段(人审后落盘)写在 `results/data_clean_<runDic>/modified_samples.jsonl`,前端「数据增强」卡片读取展示最终改动。详见 [references/program.md §4.0.1 Step A](references/program.md)。
+78 -648
View File
@@ -278,80 +278,11 @@ for k in prev:
3. **最终报告强制并列原文**:每条候选必须同时打印**原始 output 字符串前 80 字符**和**判别器返回值**,读者肉眼可对一遍——光报"近邻一致 N 条 / mislabel M 条"不够。 3. **最终报告强制并列原文**:每条候选必须同时打印**原始 output 字符串前 80 字符**和**判别器返回值**,读者肉眼可对一遍——光报"近邻一致 N 条 / mislabel M 条"不够。
4. **零计数兜底**mislabel 跑出 = 0 时**禁止**直接结论"训练集没问题",必须先回去验步骤 2 的端点是否全过——zero-count 默认是判别器 bug 信号、不是结论信号。 4. **零计数兜底**mislabel 跑出 = 0 时**禁止**直接结论"训练集没问题",必须先回去验步骤 2 的端点是否全过——zero-count 默认是判别器 bug 信号、不是结论信号。
⚠️ **算法选型**:错例集 O(几十)、训练集 O(几万) — 不要用复杂度高、要把训练集全量物化成稀疏矩阵或 embedding 的算法(如 `sklearn.TfidfVectorizer.fit_transform` 全量 + cosine — 实测 35k 行字符 n-gram 600s 仍 timeout)。 ⚠️ **近邻检索性能规则**(>1 万行训练集时强制):
- **禁止 O(M·N) 全量比对**——必须两段式:① 倒排索引粗筛(query 2-gram → posting list,每错例得 50-300 候选)② 仅对候选精排(Jaccard/char-overlap 取 top-3
⚠️ **>1 万行训练集做近邻必须先粗筛后精排,不许直接 O(M·N) 全量比对**——37 错例 × 30k 训练 = 1.1M 次精排,朴素写法实测 ~3 min(v32 R0 case:错例侧逐条扫训练集 + 每例剪枝仍 182s,且后续每加一步 mislabel 定位都要再跑一遍)。两段式: - **训练集 + 倒排索引 pickle 缓存到 `/tmp/`**key 用文件 mtime 自动失效),禁止每个脚本重建
- **stdout 只 print 聚合**(三档分布 + 每档 top-3 范例,目标 <2KB),明细落 `/tmp/<step>_<runDic>_detail.json`
1. **粗筛(倒排索引,全表只扫一次)**:训练集一次性建 `token → set(line_idx)` 倒排表,key 用 query 的 2-gram 字符 token30k 行 query 通常 3k5k 唯一 token,建表 O(N) 秒级)。 - **>10MB 文件先 bash 切片**`awk/grep/head` 取子集),禁止 `pd.read_csv` 整体加载
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。 ⚠️ **执行通道**:所有 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 函数本身就有问题。 3. **Reward 对齐检查**(全量失败 case,不抽样):用 `zk_reward_fn` 对"正确 label"和"实际输出"分别打分,验证 reward 方向是否和准确率一致。如果 reward 给错误输出的分更高 → reward 函数本身就有问题。
@@ -413,243 +344,95 @@ conflict_rate = len(conflicts) / len(query_to_golds)
#### 2.6 报告模板 #### 2.6 报告模板
> **首次评测使用下方"首次评测报告模板",后续迭代使用"迭代报告模板"。** 写入 `results/workflow<runDic>.md`。首次评测(new=old)省略"上轮回顾"和"跨轮追踪",只记绝对指标。
##### 首次评测报告模板
```markdown
# workflow<runDic> 基准模型评测报告
**版本**: v28 | **执行ID**: ... | **基准模型**: ...
> 首次评测:新旧模型相同,本报告只记录基准模型的绝对指标和错误分布,作为后续迭代的对照基准。
## 总览
- **需求集合 (<名称>)**: xxx% ⭐
- **大盘(车载)**: xxx%
- **Specific test**: xxx%
## 需求集合分析
### 子集表现
| 子集 | 准确率 | 总数 | 错误数 | 距 95% gap |
### 失败 case 与训练数据关联
- <子集>: N 条失败中 X 条**有近邻且 label 一致**、Y 条**无近邻**、Z 条**近邻 mislabeled** ⚠️ → 主要缺口: <分布外补数据 / SFT 学不动 / 训练集错标>
#### 训练集错标清单(逐条,对应 Z 条 mislabeled
| 失败 case query | 训练样本 file:line | hash | 当前 label | 应改 label | 相似度 | 违反规则 |
|---|---|---|---|---|---|---|
| <q1> | augment_<N>.jsonl:42 | <h1> | Agent | CT | 0.93 | R1(单 POI+多形容词) |
> 没有 mislabeled 时这张表省略;有则进入下一轮 Step 4 的修订清单。
## 设备维度(只分析车载)
| 设备 | 准确率 | 总数 | 错误数 |
全设备参考:
| 设备 | 准确率 | 总数 | 错误数 |
## Specific Test 子集表现
| 子集 | 准确率 | 总数 | 错误数 |
## 基准错误画像
### <错误 pattern>(数量)
**根因归类**: reward / data / 格式
**典型 case**:
query: ...
label: ...
模型输出: ...
归类: 分流/语义
训练集近邻: 有 label 一致 / 无 / ⚠️ mislabeledhash=xxx, 相似度 0.xx, 当前=Agent / 应改=CT
## 初始错误分布总结
| 根因类 | 错误数 | 占比 | 代表 pattern |
## 下一轮假设候选(按 ROI 排序)
1. <假设>: 基于基准错误画像 <xxx>,预期收益 <xxx>
2. ...
```
##### 迭代报告模板
```markdown ```markdown
# workflow<runDic> 回归分析报告 # workflow<runDic> 回归分析报告
**版本**: v27 | **执行ID**: ... | **新模型**: ... | **基线**: ... **版本**: v28 | **执行ID**: ... | **新模型**: ... | **基线**: ...
## 上轮假设回顾(若本轮是上轮训练结果 ## 上轮假设回顾(首次评测省略
- **假设**: <from iteration_log> - **假设/干预/预测/判定**: ✅ hit / ❌ miss / 🟡 partial
- **干预**: <type: reward/data/hparam> - <summary>
- **预测**: <...>
- **判定**: ✅ hit / ❌ miss / 🟡 partial — <一句话说明>
## 总览 ## 总览
- **需求集合 (<名称>)**: xxx%vs 基线 ±x.xx%vs 上轮 ±x.xx%)⭐ - **需求集合 (<名称>)**: xxx%vs 基线 ±x.xx%)⭐
- **大盘(车载)**: xxx%vs 基线 ±x.xx% - **大盘(车载)**: xxx%vs 基线 ±x.xx%
- **Specific test**: xxx%vs 基线 ±x.xx% - **Specific test**: xxx%vs 基线 ±x.xx%
## 跨轮追踪 ## 跨轮追踪(首次评测省略)
| 指标 | 本轮 | 上轮 | 基线 | | 指标 | 本轮 | 上轮 | 基线 |
|---|---|---|---|
| 需求集合 | | | |
| 持久错误数 | | | — |
| 新引入错误数 | ⚠️ | — | — |
| 修复错误数 ✅ | | — | — |
## 需求集合深度分析 ## 需求集合深度分析
### 子集表现 ### 子集表现
| 子集 | 基线准确率 | 新模型准确率 | 旧对新错 | 旧错新对 | 总数 | 分流错误 | 语义错误 | 变化 | | 子集 | 基线准确率 | 新模型准确率 | 旧对新错 | 旧错新对 | 总数 | 分流错误 | 语义错误 | 距95% gap |
> 准确率来自 lark_template.json(含 complex 门禁检查),B/G 数来自 CSV 的纯模型 GSB(不含 complex 门禁),两者口径不同。
### 失败 case 与训练数据关联 ### 失败 case 与训练数据关联
- <子集>: N 条失败中 X 条**有近邻 label 一致**、Y 条**无近邻**、Z 条**近邻 mislabeled** ⚠️ → 主要缺口: <分布外补数据 / SFT 学不动 / 训练集错标> - <子集>: N 条失败中 X 条有近邻 label 一致、Y 条无近邻、Z 条近邻 mislabeled ⚠️
- Reward 对齐(全量): N/N 条 reward 方向与准确率一致 - Reward 对齐(全量): N/N 条方向一致
#### 训练集错标清单(逐条,对应 Z 条 mislabeled #### 训练集错标清单(Z>0 时必写
| 失败 case query | 训练样本 file:line | hash | 当前 label | 应改 label | 相似度 | 违反规则 | | 失败 case query | 训练样本 file:line | hash | 当前 label | 应改 label | 相似度 | 违反规则 |
|---|---|---|---|---|---|---|
| <q1> | augment_<N>.jsonl:42 | <h1> | Agent | CT | 0.93 | R1(单 POI+多形容词) |
> 没有 mislabeled 时这张表省略;有则 Z 条样本自动进入下一轮 Step 4 的修订清单。 ## 设备维度(车载)
## 设备维度(只分析车载)
| 设备 | 基线准确率 | 新模型准确率 | 变化 | 旧对新错 | | 设备 | 基线准确率 | 新模型准确率 | 变化 | 旧对新错 |
全设备参考(准确率来自 lark_template含 complex 门禁检查): > B/G 数来自 CSV 纯模型 GSB(不含 complex 门禁),准确率来自 lark_template含 complex 门禁),口径不同。
| 设备 | 基线准确率 | 新模型准确率 | 变化 | 旧对新错(B) | 旧错新对(G) |
> 注:B/G 数来自 overrall_comparison.csv 的纯模型 GSB(不含 complex 门禁),准确率来自 lark_template.json(含 complex 门禁),两者口径不同。
## Specific Test 旧对新错 Top 子集(不含需求集合) ## Specific Test 旧对新错 Top 子集(不含需求集合)
| 子集 | 旧对新错 | 总数 | 率 | 旧错新对 | 相对基线 | | 子集 | 旧对新错 | 总数 | 率 | 旧错新对 | 相对基线 |
## 问题模式分析 ## 问题模式分析(先需求集合,再其他 Specific Test
> **分析顺序**:先分析本次需求迭代集合(如 icl_test)的问题,再分析其他 Specific Test 问题。 ### N. <问题标题>P0/P1/P2
> Specific Test 旧对新错 Top 子集表中也应去掉需求集合子集(已在需求集合深度分析中覆盖)。 **根因归类**: reward / data / 格式
**涉及集合**: `xxx.csv`B=x/xxx=x.xx%
### 一、需求集合问题分析
### 二、其他 Specific Test 问题分析
### N. <问题标题>(优先级 P0/P1/P2
**根因归类**: reward / data / 格式 — <一句话依据>
**涉及集合**:
- `data/specific_test_set/xxx/xxx.csv`B=x/xxx=x.xx%G=x,相对基线 ±x.xx%
**错误分布**: 分流 x / 语义 x(可重叠)
**跨轮**: 持久 x / 新引入 x ⚠️ / 修复 x ✅ **跨轮**: 持久 x / 新引入 x ⚠️ / 修复 x ✅
**典型 case**(必须含对话历史,从 input 的 `[对话历史]` 段提取):
**错误类型**:
- <类型1>x 条): 描述
- <类型2>x 条): 描述
**典型 case**(优先 new/regressed,必须包含对话历史):
``` ```
对话历史: 对话历史: ...(多轮列出,无则写"无")
用户: <之前的对话> query: ... | label: ... | 旧: ... | 新: ... | 归类: 分流/语义
小爱: <之前的回复> 训练集近邻: 有/无/⚠️ mislabeledhash, 相似度, 当前→应改)
...(多轮则列出所有轮次,无历史则写"无")
query: <用户最后一句(当前 query>
label: xxx
旧: xxx # origin_predict_base
新: xxx # origin_predict_dev(保留 complex= 前缀)
归类: 分流/语义/两者 | 跨轮: persistent/new/regressed
训练集近邻: 有 label 一致(hash=xxx, 相似度 0.xx/ 无 / ⚠️ mislabeledhash=xxx, 相似度 0.xx, 当前=Agent / 应改=CT
``` ```
**结论+建议**: 一句话
> **重要**:对话历史从 input 字段的 `[对话历史]` 段提取,不能省略。很多错误(如短句闲聊被误判为 Agent)只有在多轮上下文中才能理解根因。
**结论**: 一句话根因
**建议**: 一句话改动(对应 2.4 的动作)
## 优先级与下一轮假设 ## 优先级与下一轮假设
| 优先级 | 问题 | B 数 | 根因类 | 建议动作 | | 优先级 | 问题 | B 数 | 根因类 | 建议动作 |
**下一轮假设候选**(按 ROI 排序):
1. <假设>: 基于本轮发现 <xxx>,预期收益 <xxx>
2. ...
``` ```
#### 2.7 参考实现 #### 2.7 参考工具函数
```python ```python
import re, csv, ast, json, hashlib import re, ast, json, hashlib
from pathlib import Path from pathlib import Path
from collections import defaultdict from collections import defaultdict
def extract_query(text: str) -> str: def extract_query(text: str) -> str:
parts = text.rsplit('用户: ', 1) parts = text.rsplit('用户: ', 1)
if len(parts) >= 2: return parts[1].split('[function]', 1)[0].strip() if len(parts) >= 2 else (
return parts[1].split('[function]', 1)[0].strip() re.search(r'query:(.+?)(?:\n|context:|function:)', text) or type('',(),{'group':lambda s,n:'<未提取到>'})()
m = re.search(r'query:(.+?)(?:\n|context:|function:)', text) ).group(1).strip()
return m.group(1).strip() if m else '<未提取到>'
def get_label(row: dict) -> str: def get_label(row: dict) -> str:
if row.get('label', '').strip(): if row.get('label', '').strip(): return row['label'].strip()
return row['label'].strip()
clb = row.get('code_label_base', '').strip() clb = row.get('code_label_base', '').strip()
if not clb: if not clb: return ''
return ''
try: try:
parsed = ast.literal_eval(clb) parsed = ast.literal_eval(clb)
if isinstance(parsed, list) and parsed: if isinstance(parsed, list) and parsed: return '\n '.join(parsed)
return '\n '.join(parsed) except (ValueError, SyntaxError): pass
except (ValueError, SyntaxError):
pass
return clb.strip("[]'").replace('\\n', '\n ') return clb.strip("[]'").replace('\\n', '\n ')
def classify(row: dict) -> tuple[bool, bool]:
"""返回 (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 = ... # TODO: 见 docstring,按当前评测产出格式现写
is_semantic = cpb != cpd
return is_triage, is_semantic
def case_hash(query: str, label: str) -> str: def case_hash(query: str, label: str) -> str:
return hashlib.md5(f'{query}|||{label}'.encode()).hexdigest()[:16] return hashlib.md5(f'{query}|||{label}'.encode()).hexdigest()[:16]
def load_error_registry(path='results/error_registry.jsonl') -> dict:
"""返回 {case_hash: [runDic where it was wrong]}"""
reg = defaultdict(list)
if Path(path).exists():
with open(path) as f:
for line in f:
e = json.loads(line)
reg[e['case_hash']].append(e['runDic'])
return reg
def cross_iter_tag(case_h: str, last_runDic: int, registry: dict) -> str: def cross_iter_tag(case_h: str, last_runDic: int, registry: dict) -> str:
history = registry.get(case_h, []) history = registry.get(case_h, [])
if last_runDic in history: return 'persistent' if last_runDic in history: return 'persistent'
if history: return 'regressed' return 'regressed' if history else 'new'
return 'new'
# 主流程
registry = load_error_registry()
with open('specific_comparison.csv', encoding='utf-8-sig') as f:
b_rows = [r for r in csv.DictReader(f) if r['纯模型GSB'] == 'B']
stats = defaultdict(lambda: {'triage': 0, 'semantic': 0, 'total': 0,
'persistent': 0, 'new': 0, 'regressed': 0})
for r in b_rows:
sc = r['sub_cate']
q, l = extract_query(r['input']), get_label(r)
h = case_hash(q, l)
triage, semantic = classify(r)
tag = cross_iter_tag(h, last_runDic=LAST_RUN, registry=registry)
stats[sc]['total'] += 1
stats[sc]['triage'] += int(triage)
stats[sc]['semantic'] += int(semantic)
stats[sc][tag] += 1
``` ```
> `classify(row)` 无法通用——agent 需先 `head` 当前 `origin_predict_*` 实物,按真实格式现写 parser 抽出 complex 二值。
### 3. 本轮假设(基于 Step 1 的证据) ### 3. 本轮假设(基于 Step 1 的证据)
**只有证据充分时才往下走。** 在开始训练前,写入 `results/iteration_log.jsonl` 的 `hypothesis` 字段: **只有证据充分时才往下走。** 在开始训练前,写入 `results/iteration_log.jsonl` 的 `hypothesis` 字段:
@@ -746,30 +529,11 @@ with open(out_csv, 'w', encoding='utf-8-sig') as fp:
for c in candidates: w.writerow(list(c)+['','']) for c in candidates: w.writerow(list(c)+['',''])
``` ```
**两阶段 UI 展示约定(强制)** **两阶段产物路径**
- 阶段一(候选):`$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_<runDic>.csv` → 前端"分层结果分析"卡
- 阶段二(确认):`$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl` → 前端"数据增强"卡
| 阶段 | 产物 | 落盘路径 | 前端卡片 | > **runDic 规则**:所有产物的 `<runDic>` = `$SFT_RUNDIC`(从 `eval "$(./scripts/resolve_run_ids.sh)"` 取)。**只能在当轮 eval `lark_template.json` 落盘后调用**,禁止手算或提前调用。
|---|---|---|---|
| 阶段一:预计修改候选 | `relabel_candidates_<runDic>.csv` | `$AUTORESEARCH_CHAT_ROOT/output/`NFS | **分层结果分析(dist-analysis** |
| 阶段二:确认修改最终 | `modified_samples.jsonl` | `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/` | **数据增强(augment** |
候选阶段(246 条等量级)的 CSV 必须写到 `output/relabel_candidates_<runDic>.csv` 让分析卡片可读;确认修改阶段(用户审核后落盘的 135 条)写入 `results/data_clean_<runDic>/modified_samples.jsonl` 让数据增强卡片可读。**不要混落**——把候选 CSV 写到 `/tmp/` 或 `data_clean_/` 都会让 UI 看不到。
> **runDic 命名语义(backend 已对齐)**augment 类产物(`data_clean_<N>/`、`augment_raw/augment_<N>_raw.jsonl`、`zk_intent/augment_<N>.jsonl`、`label_master_review.jsonl`)的 `<N>` = `$SFT_RUNDIC` = **当轮 eval 落盘后 `resolve_run_ids.sh` 取到的 max(已落盘 workflow)**。由于 resolve 是在当轮 eval 的 `lark_template.json` 落盘之后调用的,此时 max 就是当轮 eval 自身的 workflow id。Backend step-detail 端点对 augment 也直接用本轮(`per_round[R{n}]`)的 runDic 去读。其他 stepcml / dist-analysis / hypothesis / gold-drift)的产物同理按本轮自己的 runDic 命名。
>
> ⚠️ **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 = max(已落盘 workflow) = 当轮 eval 的 workflow id
> # augment 落盘 / yaml / 归档全用这个
> # $EVAL_RUNDIC = SFT_RUNDIC + 1(仅下一轮 cml workflow run 提交时使用)
> ```
>
> **写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。
>
> 🚨 **调用时序(CRITICAL**`resolve_run_ids.sh` **只能在当轮 CML eval 的 `lark_template.json` 已落盘之后调用**。禁止在 eval 提交前或 watcher 等待期间预先调用——此时 max workflow 仍是上一轮的值,会导致 SFT_RUNDIC 偏移 -1(已踩坑:R0 workflow17823 评测中提前 resolve 得到 17822augment 文件全部错位)。正确时序:`cml step=complete` → `resolve_run_ids.sh` → augment/SFT。每次进入 augment/SFT 步骤前**必须重新调用**,不许复用之前缓存的值。
**Step A.5label-master 预审(量级判定之前必跑,强制)** **Step A.5label-master 预审(量级判定之前必跑,强制)**
@@ -830,43 +594,11 @@ for f, edits in edits_by_file.items():
with open(f, 'w') as fp: fp.writelines(rows) with open(f, 'w') as fp: fp.writelines(rows)
``` ```
**Step E:修改后回归验证(强制)** **Step E:修改后验证** — `prepare_and_train_sft.py prepare` 验数量 + 格式校验 4.1.1 + 逐条目视确认
改完不能直接训: **Step F:删除 vs 修改** — 标签错但 query 有价值→修改;query 噪声重/同 pattern 已充足/跨子集 gold 矛盾→删除
1. **数据 sanity**`prepare_and_train_sft.py prepare`,对比合并后总数(旧总数 - 删除数 = 新总数)
2. **全量人审**:所有改动条目逐条过一遍,看 query/label/instruction 符合预期
3. **格式校验**:用 4.1.1 字段约束扫一遍改后 output
**Step F:删除 vs 修改的选择** **Step G.bak 管理** — 每轮 `.before_r{N}.bak` 保留≥5 轮;回退:`cp xxx.before_r{N}.bak xxx`;禁止 bak 文件含 `_valid`/`_train`(会被 prepare 误读)
| 情况 | 删除 | 修改 |
|---|---|---|
| 标签错但 query 有价值 | | ✓ |
| 标签错且 query 噪声重(ASR 含糊) | ✓ | |
| 同 pattern 已有大量正确样本 | ✓ | |
| 同 pattern 仅此一条 | | ✓(保留多样性) |
| query 跨多子集 gold 矛盾 | ✓(避免训练矛盾信号) | |
**Step G:多轮 .bak 累积管理**
每轮 `.before_r{N}.bak` 至少保留 5 轮:
```
ai-planning/data/train_set/zk_intent/
├── augment_17729.jsonl # 当前
├── augment_17729.jsonl.before_r24.bak # R24 改前
├── augment_17729.jsonl.before_r25.bak # R25 改前
└── augment_17729.jsonl.before_r28.bak # R28 改前
```
回退:`cp augment_17729.jsonl.before_r{N}.bak augment_17729.jsonl`
**反模式**(违反必查):
- ❌ `sed -i 's/ComplexTask/Agent/g'` 全文替换
- ❌ 没备份就改文件
- ❌ >50 条改动跳过人审
- ❌ 修改后不验证直接训练
- ❌ backup 文件命名含 `_valid` / `_train`(会被 prepare 脚本误读)
#### 4.1 输入 #### 4.1 输入
@@ -1108,122 +840,22 @@ def check_label_rules(query: str, label: str, history: list = None) -> tuple[boo
超阈值必须人介入定边界,不允许自动放行。 超阈值必须人介入定边界,不允许自动放行。
#### 4.3.2 从评测集自动归纳新规则(替代旧版"新规则不明确"信号) #### 4.3.2 从评测集自动归纳新规则
**核心观察**测试集 CSV 已含 gold (`code_label` / `complex`),所谓"新规则"不是 gold 不明确,而是**模型尚未学到测试集已存在的 gold 规律**。这种情况自动归纳即可,不该让人 测试集 CSV 已含 gold`code_label`/`complex`),规则从错例自动归纳,不需人定
##### 触发时机 **触发**:每轮 Step 1 后,对未被已知规则覆盖的错例做归纳。
每轮 Step 1 错例分析后,对**所有未被 R1~R{N} 已知规则覆盖的错例**做自动归纳。 **接受条件**(两个同时满足):
- `c1 ≥ 0.80`:同 pattern 错例内 gold 主流占比(防错例巧合)
- `c2 ≥ 0.85`:候选正则扫全测试集后主流 gold 占比(防过拟合错例)
- 两者推出的主流 gold 必须一致,且不与现有规则重叠 >70%
##### Confidence 的定义(两个独立指标,必须同时满足) **归纳流程**:同 pattern 错例 ≥3 条 → 提取公共正则 → 算 c1/c2 → 过阈值则自动追加到 `results/label_rules.md`,下一轮 4.3.1 立即生效,不需人审。
新规则候选必须通过**两层置信度检查** **归纳失败 → HiTL**
- 信号 #2:同结构 query 在 ≥2 子集 gold 矛盾
**c1 = 错例内一致性**:候选 pattern 错例中 gold 标注的主流占比 - 信号 #6:同 pattern 错例 <3 条但多轮持续出现
```
c1 = max_gold_count(同 pattern 错例) / 总错例数
```
例:5 条同 pattern 错例,4 条 gold=CT、1 条 gold=Agent → c1 = 4/5 = **0.8**
**c2 = 全测试集验证准确率**:把候选正则扫**整个测试集**(含对例 + 错例),看主流 gold 占比
```
c2 = 主流 gold 数 / 命中正则的全部测试 case
```
例:扫整个测试集,符合该 pattern 共 12 条(4 错 + 8 对),9 条 gold=CT → c2 = 9/12 = **0.75**
**接受条件**`c1 ≥ 0.80 AND c2 ≥ 0.85`
为什么用两个?
- c1 防止"错例巧合":3 条错例都说 CT 但全测试集大多数对例其实是 Agent → c2 拦下
- c2 防止"过拟合错例":仅看错例可能学到模型当前的偏差而不是真规则
- 两个指标同时高才说明规则真实存在
为什么是 0.80 / 0.85 不是 1.0
- gold 标注本身有 ~5-10% 噪声(标注员失误 + 边界 case)
- 如果要求 100%,会丢失大部分有效规则
- 0.85 是经验阈值(参考 R28 实测:状态描述句规则 c2 ≈ 0.92,找+模糊 c2 ≈ 0.55 被拦下)
##### 自动归纳代码
```python
import re
from collections import Counter
def induce_rule(cluster_cases, all_test_rows, existing_rules):
"""
cluster_cases: 同 pattern 的错例(≥3 条)
all_test_rows: 所有测试集 rows(含对+错)
existing_rules: 已有 R1~R{N}
返回: 候选规则 dict 或 None
"""
if len(cluster_cases) < 3: return None
# c1: 错例内一致性
err_gold_dist = Counter((c['gold_complex'], c['gold_tag']) for c in cluster_cases)
majority_gold, majority_count = err_gold_dist.most_common(1)[0]
c1 = majority_count / len(cluster_cases)
if c1 < 0.80: return None
# 提取候选正则
regex = extract_common_regex(cluster_cases)
# c2: 全测试集验证
matched = [r for r in all_test_rows if re.search(regex, r['query'])]
if len(matched) < 5: return None # 命中样本太少不足以判定
full_gold_dist = Counter((r['complex_norm'], r['code_tag']) for r in matched)
full_majority, full_count = full_gold_dist.most_common(1)[0]
c2 = full_count / len(matched)
if c2 < 0.85: return None
# 主流 gold 必须一致(c1 和 c2 推出的 gold 不能矛盾)
if majority_gold != full_majority: return None
# 排除:是否已被现有规则覆盖
for rule in existing_rules:
if rule.regex_overlap(regex) > 0.7: return None
return {
'regex': regex,
'gold': majority_gold,
'c1': c1, # 错例一致性
'c2': c2, # 全集验证
'support_err': len(cluster_cases),
'support_total': len(matched),
'anchor_cases': cluster_cases[:3],
}
```
##### 归纳后自动追加到规则库
候选规则 c1 ≥ 0.80 + c2 ≥ 0.85 + 主流 gold 一致 + 不与现有规则冲突 → 自动追加到 `results/label_rules.md` + `check_label_rules()`
```markdown
### R{N} 自动归纳({runDic} 轮)
**规则**:含 pattern `{regex}` → {gold}
**置信度**c1={c1:.2f}(错例 {support_err}/{support_err_total} 一致), c2={c2:.2f}(全测试集 {full_majority}/{support_total} 一致)
**锚定 case**(列出该 pattern 在测试集命中的全部 case,不抽样):
- {case1}
- {case2}
- {case3}
- ...(共 {N} 条)
**自动检查正则**`{regex}`
```
新规则立即在下一轮 4.3.1 检查中生效,**不需要人审**。
##### 什么时候自动归纳失败 → 才触发人介入
仅当以下情况自动归纳算法**得不出结论**时,才转 H-i-T-L 信号:
- **触发信号 #2**:同结构 query 在 ≥2 个测试子集 gold 不同(自动归纳得到的规则会矛盾)
- **触发信号 #6**:同 pattern 错例 < 3 条(样本不足无法归纳)但持续多轮出现 → 让人决定是否手动定规则
> 这就是为什么删除了旧的"新规则不明确"信号——**99% 情况自动归纳就能解决**,1% 真矛盾的情况已被信号 #2/#6 覆盖。
#### 4.4 写入训练目录 #### 4.4 写入训练目录
@@ -1248,76 +880,18 @@ JSONL 行里的 `sub_cate` 字段仅用于内部路由/去重,归档时丢弃
#### 4.5 label-master 标签复核(落盘后、SFT 前,**强制**) #### 4.5 label-master 标签复核(落盘后、SFT 前,**强制**)
任何写入训练目录的修改/新增样本都必须经 [label-master skill](../../label-master/SKILL.md) 复核:格式 + 语义。**没过这关不许进 Step 5。** | 文件 | 层 1(格式) | 层 2(语义,`Skill(skill="label-master")` |
|---|---|---|
| H1 `modified_samples.jsonl` | ✅ `validate_label_output.py --field output_after` | ❌ 已在 §4.0.1 A.5 完成 |
| H2 `augment_<runDic>.jsonl` | ✅ `validate_label_output.py --field output` | ✅ 逐条调 label-master1:1 全量) |
**复核范围**(两份必须过格式层;语义层只对 H2): **层 2 禁止用正则/规则脚本替代**,必须调 `Skill(skill="label-master")``repeatable: true`)。每次只传一条 `(query, label)`,禁止批量。
| 文件 | 来源 | 层 1(格式) | 层 2(语义,Skill 调 label-master | **覆盖率硬规则**`label_master_review.jsonl` H2 行数 = `augment_<runDic>.jsonl` 行数(§5.0 `wc -l` 断言会拦)。不通过 > 0 则必须修正后重新 review 直到全 pass。verdict 文件不存在 → Step 5 拒绝启动。
|---|---|---|---|
| `$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:格式校验(确定性,自动跑)**
```bash
cd /home/mi/zk-data-agent-wsh # 或 wherever skills 包在的项目根
# H1 改标
python skills/label-master/scripts/validate_label_output.py \
--file "$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl" \
--field output_after
# H2 仿写
python skills/label-master/scripts/validate_label_output.py \
--file "$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl" \
--field output
```
任一行 failtag 不存在 / Agent 包装错 / function 引用错)→ **必须修,不许直接跳过**。修完原地重跑直到全过。
**层 2:语义复核(Skill 调用 label-master Agent,仅 H2**
**禁止用正则/规则脚本替代 LLM 语义判断**。层 2 不是格式校验,是"该 query 在 label-master 知识体系下是否应该标成这个 label"的语义问题。**唯一合法实现方式是调用 `Skill(skill="label-master")`**label-master 已标记 `repeatable: true`,可在同一 run 内多次调用)。已踩坑:R1 agent 写了个检查 hist 里有没有导航触发词的正则脚本充当"layer 2",完全没走语义判断,导致错误样本全量混入训练集。
H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里**不再重跑**。H2 仿写是 §4.2 GPT 新增的样本,没经过 Step A.5,必须在此补一次语义判定:
🚨 **覆盖率硬规则**`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: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`
- ⚠️ **禁止把多条 query 塞进同一次 Skill 调用**——批量调用会让 label-master 在多条之间相互锚定,分错率显著升高(已踩坑)
- 可在同一轮 assistant response 里并发多次 Skill 调用(建议 ≤ 8 路并发)以提升吞吐
3. 收集 verdict,写入 `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/label_master_review.jsonl`,每行一条 verdict(只含 H2 条目)
**verdict 处置规则(自动)**
| 比例 | 处置 |
|---|---|
| 不通过 = 0 | ✅ 全量通过,允许进入 Step 5 |
| 不通过 > 0 | ❌ **必须修正**H2 → 修正 augment 样本后重新 reviewH1 → 回滚或修正后重新 review),循环直到不通过 = 0 才能进 Step 5。禁止"自动丢弃不通过样本继续"——每条都必须过 |
**写完复核报告才允许进 Step 5。**verdict 文件不存在 / 没跑层 2 → Step 5 拒绝启动(5.0 的准入检查会查 `label_master_review.jsonl` 存在)。
🚨 **注意**label-master 是"知识 + Agent 推理"型 Skill,不是黑盒分类器。每一次调用都必须输出 `verdict + 推荐标签 + 排除理由` 三件套,便于人类回查;严禁要求它返回 1/0 标量。(批量调用本身已在第 2 步禁掉,这里再强调一次输出 schema。)
### 5. SFT 训练 ### 5. SFT 训练
🚨 **Step 4 → Step 5 硬连接(NEVER STOP,反惯性反复强调)** 🚨 **Step 4 → Step 5 硬连接**`augment=complete` 后同一轮**紧接着**:① 写 `sft=running` ② §4.5 复核 ③ 复核全过 → `submit_sft.sh` + 挂 watcher。禁止 turn 结束、禁止写简报、禁止等回调。评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起,不串进 SFT bg。
写完 `{"step":"augment","status":"complete",...}` 那一行**之后**,**同一轮 bash 不许结束、不许写"Step 4 完成"简报、不许等回调**。必须**紧接着**:
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.sh <SFT_RUNDIC> [PREV_RUNDIC]` 并立刻挂 watcher(§5.2)。训练 `_SUCCESS` 落盘后的下一轮 step 单独用 `bash scripts/submit_cml_eval.sh <EVAL_RUNDIC>` 起评测,**不要**把评测串到 SFT bg 任务里——跨 step bg 是禁区
**反模式**(被反复踩坑,永远禁止):
- ❌ 写完 `augment=complete` → 写一份漂亮的"Step 4 完成"进度简报 → turn 结束 → 等下一次触发
- ❌ "augment 已完成,下一步等回调后续做 SFT" — 不存在等回调,augment 完成的那一刻就是 SFT 启动的那一刻
- ❌ 把 H2 后台任务的 stdout 当成"用户消息"处理完就 turn 结束 — bg 任务回调不是 turn 结束信号,augment=complete 也不是
**模型本能会想停("任务段落感"),SKILL NEVER STOP 压不住这个本能**——所以这里写成硬命令清单,照清单第一条到第三条机械执行,做完才允许 turn 结束。
使用 `prepare_and_train_sft.py` 完成数据组装和训练。 使用 `prepare_and_train_sft.py` 完成数据组装和训练。
@@ -1390,93 +964,19 @@ cd "$AUTORESEARCH_CHAT_ROOT"
-`--model_path $AUTORESEARCH_CHAT_ROOT/sft_output_r17776`(从历史轮 ckpt -`--model_path $AUTORESEARCH_CHAT_ROOT/sft_output_r17776`(从历史轮 ckpt
- ❌ 任何形式的 "增量 SFT"(除非显式声明是为了验证"从 X ckpt 继续是否更好"的对照实验,且单次性,log 要明确标记) - ❌ 任何形式的 "增量 SFT"(除非显式声明是为了验证"从 X ckpt 继续是否更好"的对照实验,且单次性,log 要明确标记)
#### 5.0.1 训练产物备份R23-R28 经验沉淀) #### 5.0.1 训练产物备份
**训练前强制**
```bash ```bash
# 上一轮 sft_output 改名归档(保留至少 5 轮,方便快速回退评测)
[ -d sft_output ] && mv sft_output sft_output_r${PREV_RUNDIC} [ -d sft_output ] && mv sft_output sft_output_r${PREV_RUNDIC}
rm -f "$AUTORESEARCH_CHAT_ROOT/sft_output/_SUCCESS" # 防遗留误报
``` ```
R23-R28 期间累积了 `sft_output_r23` ~ `sft_output_r28`,多次需要回退到上一轮模型重评(如 gold drift 验证、副作用对照)。**没备份就丢失了一切对照能力。** **SFT watcher 流程**`submit_sft.sh` 提交后):
1. 写 program-state `sft=running` + watch 声明(target = `sft_output/_SUCCESS`
2.`bash(run_in_background=true, wait_for_completion=true)``_SUCCESS` 落盘
3. Watcher fire / bg 返回后**下一轮**:写 `sft=complete` → 写 `cml=running` → 调 `submit_cml_eval.sh` 起评测 + 挂评测 watcher
**自动评测 watcher** ⚠️ 训练和评测**不串接**——`submit_sft.sh` 只提 SFT,评测在 `_SUCCESS` 后新一轮单独起。
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` 在脚本内嵌 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**(一个都不能少):
**⚠️ 先 `rm -f sft_output/_SUCCESS` 再挂 Watcher 1**R17777 经验:上一轮遗留的 `_SUCCESS` 会让 watcher 瞬间误报 train done)。cml 容器内 `imageCommand``rm -rf sft_output` 要等到 `state=deploying→running` 之后才执行,若 watcher 只靠 `_SUCCESS` 存在与否判断,会在 deploy 阶段直接触发。
```bash
# 预清理:必须在挂 watcher 之前执行
rm -f "$AUTORESEARCH_CHAT_ROOT/sft_output/_SUCCESS"
# Watcher 1:训练阶段 —— cml state 为主判据,_SUCCESS 辅助
# 注意:heredoc 用 'EOF' 防止外层 shell 展开,$AUTORESEARCH_CHAT_ROOT
# 在 watcher 子进程里运行时再展开(远端 bash 会自动注入这个 env)。
cat > /tmp/wait_train_<RUNDIC>.sh <<'EOF'
#!/bin/bash
JOB_ID=<t-xxx-xxx>
SUCCESS="$AUTORESEARCH_CHAT_ROOT/sft_output/_SUCCESS"
export PATH=$HOME/.cloudml-cli/bin:$PATH
while true; do
sleep 60
STATE=$(cml custom_train describe "$JOB_ID" 2>/dev/null | grep -oE '"state": "[a-z]+"' | head -1 | sed 's/.*: "//;s/"//')
case "$STATE" in
succeed)
# state=succeed 后再校验 _SUCCESS 文件(有时 flush 慢,再等 30s)
[ -f "$SUCCESS" ] && { echo "TRAIN_DONE"; exit 0; }
sleep 30; [ -f "$SUCCESS" ] && { echo "TRAIN_DONE (delayed _SUCCESS)"; exit 0; }
echo "TRAIN_DONE (state=succeed, _SUCCESS missing)"; exit 0 ;;
failed|killed) echo "TRAIN_$STATE"; exit 1 ;;
running|deploying|queued|pending|"") ;; # 继续等
esac
done
EOF
# Watcher 2:评测阶段 —— 等 workflow<RUNDIC>/metric_diff/lark_template.json 出现
cat > /tmp/wait_eval_<RUNDIC>.sh <<'EOF'
TARGET=/mnt/xiaoai-zk-model-train-tj5/workflow5/workflow<RUNDIC>/metric_diff/lark_template.json
while [ ! -f "$TARGET" ]; do sleep 120; done
echo "EVAL_DONE"
EOF
```
两个 task 都要 `run_in_background: true`。Claude 会在两个事件各触发一次 task-notification
- Watcher 1 触发 → 进入"训练完成,评测已自动提交,等评测"阶段(通常不需要动作,报告进度即可)
- Watcher 2 触发 → 进入 Step 1/2 分析报告阶段
**反模式**
- ❌ 只挂 Watcher 2(R17775 犯的错) —— 训练完到评测完之间的窗口期完全失联
- ❌ 依赖 `tail -f /tmp/r<RUNDIC>_logs/cml_watcher.log` —— Claude 不会主动 tail
- ❌ 依赖用户看到别的信号来触发 —— 违反 "NEVER STOP" 自主循环原则
旧版 nohup 路径下的 watcher 模板(仅在 5.2 例外情况使用):
```bash
#!/bin/bash
# /tmp/auto_eval_after_train.sh —— 等本地训练 PID 结束 → 自动起 cml workflow
TRAIN_PID=$1
RUN_DIC=$2
MODEL_NEW=$AUTORESEARCH_CHAT_ROOT/sft_output
MODEL_OLD=<基线模型>
while kill -0 $TRAIN_PID 2>/dev/null; do sleep 60; done
sleep 30 # 等 checkpoint flush
[ ! -f "$MODEL_NEW/config.json" ] && exit 1
source ~/.cloudml-cli/.profile
cml workflow run --workflow_id f-... --version v28 \
--global_inputs runDic=$RUN_DIC \
--global_inputs model_path_new=$MODEL_NEW \
--global_inputs model_path_old=$MODEL_OLD
```
#### 5.1 数据组装 #### 5.1 数据组装
@@ -1510,90 +1010,20 @@ python prepare_and_train_sft.py prepare --regen_from_csv --output_dir ./sft_data
必须展示:(1) 参与训练的所有文件及行数 (2) 被排除的旧轮文件及排除原因(如果有)。 必须展示:(1) 参与训练的所有文件及行数 (2) 被排除的旧轮文件及排除原因(如果有)。
#### 5.2 启动训练(cml 任务,R29 起统一走这条路径 #### 5.2 启动训练(cml custom_train
R23-R28 期间用本地 `nohup python3 prepare_and_train_sft.py train ...` 启动,会因登出/网络/会话退出而中断,并占用本地工作机 8 卡 GPU。**R29 起统一改为 cml custom_train submit 提交训练任务**,由 CloudML 调度到 `bj-nlp` 队列的 h20-96g 8 卡,本地零占用。
##### 一键提交脚本
⚠️ **runDic 不许手敲**:先 `eval` 一下 `resolve_run_ids.sh` 拿到这一轮的 `$SFT_RUNDIC` / `$EVAL_RUNDIC`,再原样传给两个独立脚本。R2 踩过的坑就是手算 runDic 时把 SFT yaml 的命名也跟着 +1,导致 `data_clean_<EVAL_RUNDIC>/` 跳过了 R1 命名空间。
提交流程拆成两步(**两个独立脚本,两个 step**):
```bash ```bash
cd "$AUTORESEARCH_CHAT_ROOT" cd "$AUTORESEARCH_CHAT_ROOT"
eval "$(./scripts/resolve_run_ids.sh)" # 解析出 $SFT_RUNDIC / $EVAL_RUNDIC eval "$(./scripts/resolve_run_ids.sh)"
./scripts/submit_sft.sh "$SFT_RUNDIC" # 提交 SFT,立刻退出
# Step 5:提交 SFT 训练(不附带任何评测逻辑) # _SUCCESS 落盘后下一轮:
./scripts/submit_sft.sh "$SFT_RUNDIC" ./scripts/submit_cml_eval.sh "$EVAL_RUNDIC" # 起评测
# 训练 _SUCCESS 落盘后的下一轮 step 才起评测:
./scripts/submit_cml_eval.sh "$EVAL_RUNDIC"
``` ```
`submit_sft.sh <SFT_RUNDIC> [PREV_RUNDIC]` **`submit_sft.sh`**:渲染 `sft_train_job.yaml.tpl`(替换 `{RUNDIC}`)→ `cml custom_train submit` → 打印 JobID → 退出。
**`submit_cml_eval.sh`**:从 `config.yaml` 读 workflow_id/version → `cml workflow run` 起评测,runDic=EVAL_RUNDIC。
1. 渲染 `scripts/sft_train_job.yaml.tpl` 为本轮 yaml(替换 `{RUNDIC}``$SFT_RUNDIC``{PREV_RUNDIC}``$PREV_RUNDIC` 监控:`cml custom_train describe/logs/kill <JOB_ID>`
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`
| 字段 | 值 | 说明 |
|---|---|---|
| `imageUrl` | `micr.cloud.mioffice.cn/wsw/large-lm:1.0.15-2` | zk_trainer 默认训练镜像(torch 2.6 + accelerate 1.7.0 |
| `queueId` | `6052` (bj-nlp) | h20-96g 资源池 |
| `resourceName` | `cloudml.ng2h20-8-8.20-199` | 8 卡 H20 96G |
| `juiceFsMountConfigs` | wangsenhao + xiaoai-zk-model-train-tj5 | 训练数据 / 产出路径 |
| `imageCommand` | 内嵌 prepare → train → mark _SUCCESS | 训练成功才落标记 |
| `retryConfig` | enableRetry: true, NodeFailure | 节点级故障自动重试 2 次 |
| `alertConfig` | FAILED + SUCCEED 飞书 P2 告警 | 异常立即知道 |
##### 监控命令
```bash
# 查任务实时状态
cml custom_train describe <JOB_ID>
# 查训练实时日志(含 accelerate / trainer 输出)
cml custom_train logs <JOB_ID> --follow
# 停任务
cml custom_train kill <JOB_ID>
# SFT 不再内嵌 watcher,自动评测改成 agent watcher → 单独调 submit_cml_eval.sh
```
##### 与 nohup 路径的对比
| 维度 | nohup(旧) | cml submit(新) |
|---|---|---|
| 本地 GPU 占用 | 8 卡满载 | 0 |
| 网络/会话中断容忍 | 中断即停 | 任务持续跑 |
| 节点故障重试 | 无 | 自动 2 次 |
| 失败告警 | 无 | 飞书 P2 |
| 任务历史 | 仅本地 log | cml 平台可查 |
| 启动复杂度 | 单行 nohup | submit_sft.sh + watcher + submit_cml_eval.sh |
##### 何时仍用 nohup 本地训
只有以下情况例外使用本地训:
- cml 队列资源排队 > 30min(紧急复现验证)
- 调试新训练逻辑(频繁改代码)
- 实验性超小规模训练(< 1000 条数据,< 2 epoch
其余一律走 cml。
##### 兼容旧脚本
`prepare_and_train_sft.py` 不动,cml 任务的 `imageCommand` 内部仍调用它。`train` 子命令本身只负责训练,需先执行 5.1 组装数据。配置见 5.0。
### 6. 记录结果 ### 6. 记录结果