diff --git a/skills/model-iteration/README.md b/skills/model-iteration/README.md new file mode 100644 index 0000000..0e49f81 --- /dev/null +++ b/skills/model-iteration/README.md @@ -0,0 +1,66 @@ +# Skill: model-iteration + +本目录是小爱中控模型迭代 skill,用于评测、错误分析、数据增强、SFT 训练和 CML workflow 追踪。 + +## 目录结构 + +``` +model-iteration/ +├── config.yaml # 评估阈值 + CML 工作流配置 +├── knowledge/ # 分流知识库 +│ ├── navigation_routing_rules.md # 地图导航 Agent/ComplexTask 分流 +│ ├── travel_routing_rules.md # 旅游 Agent/ComplexTask 分流 +│ └── multi_command_rules.md # 多指令 vs ComplexTask 判定 +├── scripts/ +│ ├── prepare_and_train_sft.py # 数据组装 + 本地训练 +│ ├── submit_sft_via_cml.sh # CML 云端训练提交(R29 起统一用此) +│ └── sft_train_job.yaml.tpl # CML 训练 job YAML 模板 +├── SKILL.md # 完整迭代协议(触发规则、Step 0-6、数据增强/清洗规范) +└── README.md # 本文件 +``` + +## 快速上手 + +### 1. 评测当前模型 + +```bash +cml workflow run \ + --workflow_id f-20260408161444-wu3pz --version v28 \ + --global_inputs runDic= \ + --global_inputs model_path_new=<新模型路径> \ + --global_inputs model_path_old=<基线模型路径> +``` + +### 2. SFT 训练(CML 云端) + +```bash +AUTORESEARCH_ROOT=/mnt/wangsenhao/autoresearch-zk \ + ./scripts/submit_sft_via_cml.sh [PREV_RUNDIC] +``` + +训练完成后自动起评测 workflow。 + +### 3. SFT 训练(本地) + +```bash +AUTORESEARCH_ROOT=/mnt/wangsenhao/autoresearch-zk \ + python scripts/prepare_and_train_sft.py prepare --output_dir ./sft_data + +AUTORESEARCH_ROOT=/mnt/wangsenhao/autoresearch-zk \ + python scripts/prepare_and_train_sft.py train \ + --data_dir ./sft_data \ + --model_path /mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507 \ + --model_type qwen3 \ + --train_output ./sft_output \ + --epochs 3 --lr 1e-5 +``` + +## 关键规则 + +- **达标标准**:需求集合 ≥ 95%,大盘车载降幅 ≤ 0.3%,specific test 降幅 ≤ 1% +- **数据清洗**:case 驱动、禁止批量、必须备份、只改 output 不动 instruction +- **训练环境**:FSDP2 + torch 2.6 需要 accelerate==1.7.0,不要改训练代码去适配 + +## 完整协议 + +详见 [SKILL.md](SKILL.md) diff --git a/skills/model-iteration/SKILL.md b/skills/model-iteration/SKILL.md new file mode 100644 index 0000000..85f78a2 --- /dev/null +++ b/skills/model-iteration/SKILL.md @@ -0,0 +1,1379 @@ +--- +name: model-iteration +description: 评测、分析并迭代小爱中控理解调度模型,覆盖需求集合回归、错误归因、数据增强、SFT/CML 训练和 workflow 指标报告。 +when_to_use: 当用户要求开始某个需求集合的模型迭代、评测当前模型、分析模型回退或停滞、追踪 CML workflow、准备/提交 SFT 训练、做中控模型 AutoResearch 时使用。 +aliases: zk-model, autoresearch, model-research, sft-iteration +allowed_tools: read_file, write_file, edit_file, grep_search, glob_search, bash, python_exec, python_package, ask_user_question +--- + +# 小爱中控模型迭代 + +使用这个 skill 处理“小爱中控理解调度模型”的评测、错误分析、归因、数据增强、SFT 训练和 CML workflow 追踪。 + +## 平台适配约束 + +- 本 skill 面向外部模型迭代工程,不要修改本平台源码目录,除非用户明确要求开发平台本身。 +- 对话内临时报告、分析摘要和中间产物优先写当前会话 `output/`;外部训练仓库、CML 目录、`/mnt/...` 路径只有在用户明确指定或确认后才写入。 +- 修改训练数据、提交 CML 训练、部署模型、覆盖外部结果目录都属于高影响操作;执行前必须展示将要改动/提交的对象并等待用户确认。用户明确说“开始,<需求集合名>”时,可以先执行一次当前模型评测和只读分析,不要直接训练。 +- 不要硬编码或输出 API key。调用模型接口时从环境变量读取 `OPENAI_API_KEY`、`OPENAI_BASE_URL`、`OPENAI_MODEL` 或用户当前配置。 +- 优先使用 `python_exec` 做结构化分析和报表汇总;只有需要调用 CML、git、系统命令或已有脚本时才使用 `bash`。 + +这是小爱中控理解调度模型的证据驱动迭代框架。你是模型迭代助手,目标是先评测、再分析、再提出可验证干预,避免没有证据就直接训练。 + +## 目标 + +通过**假设驱动**的自主迭代找出模型回退/停滞的根因,并满足: +- **需求集合**:95%+ 通过率(当前瓶颈,最高优先级) +- **大盘集(车载)**:持平或 0.3% 以内降幅 +- **specific test**:持平或 1% 以内降幅 + +不只是跑通流程,而是每一轮都要能回答:"这轮验证了什么?学到了什么?下一轮改什么?" + +## Setup + +1. 读取 `config.yaml` +2. 读取 `modules/` 下所有 Python 文件 +3. 验证 `verl_zk`、`ai-planning` 等目录 +4. 确认基线:若 `results/iteration_log.jsonl` 不存在,先跑基线 +5. 确认 `results/error_registry.jsonl` 存在(无则初始化为空)——用于跨轮错误追踪 +6. 确认 需求集合,需求集合在https://git.n.xiaomi.com/ai-service/ai-planning/-/tree/autoresearch-v1?ref_type=heads的data/specific_test_set下,具体由用户指定 + +## 触发规则 + +用户发 **"开始,<需求集合名>"**(如 `"开始,icl_test"`)→ **立即用当前模型跑一轮评测**,不再中途确认参数,直到报告完成: + +1. **Setup 检查**:cml 环境 + SSH key(见下)、从 `config.yaml` 读默认参数、验证 `verl_zk`/`ai-planning` 目录、初始化 `error_registry.jsonl` +2. **准备评测参数**:确定 `model_path_new`(当前模型)/ `model_path_old`(基线),自动分配 runDic(扫描 `run_history_dir` 最大值 +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)、跨轮 diff(persistent/new/regressed)、需求集合深度分析(训练数据关联 + reward 对齐),写入 `results/workflow.md` +6. **决策分支**:全部达标 → 部署并结束;否则 → 形成假设(Step 3)→ 按归因干预:数据增强(Step 4)/ 改 reward / 改格式 / 调超参 → SFT 训练(Step 5)→ 记录到 `iteration_log.jsonl`(Step 6)→ 回到第 3 步评测新 checkpoint +7. **只读评测和分析可以连续执行**;涉及训练、部署、批量改训练数据或覆盖外部目录前,必须先展示计划并等待用户确认。 + +> **关键**:"开始"的第一个动作永远是**评测当前模型**,而不是直接训练。先看清楚当前模型在各个集合上的表现和错误分布,再基于证据形成本轮假设 → 做干预。**不要没看评测就开始训练。** + +> **首次评测**:首次评测只关注基准模型指标,`model_path_new` 和 `model_path_old` 设为同一个基准模型路径。分析报告只分析基准模型本身的表现,不做新旧模型对比(因为是同一个模型)。目的是建立 baseline 数据,为后续迭代提供对照基准。 + +> **需求集合来源**:需求集合在git目录https://git.n.xiaomi.com/ai-service/ai-planning/-/tree/autoresearch-v1?ref_type=heads`中ai-planning/data/specific_test_set/` 下,用户在触发信号中通过名称指定。框架按此名称定位对应子目录及其 CSV 文件,贯穿整个迭代(评测分析、深度分析、数据增强优先级)。**未指定需求集合时不启动迭代,直接提示用户补充。** + +信号可携带覆盖参数,如 **"开始,icl_test"** / **"开始,icl_test,v28"** / **"开始,icl_test,model_path_new=/xxx/"**。其中第一个非 key=value、非版本号的参数即为需求集合名。 + +### SSH Key 检查(首次 / git 操作失败时) + +```bash +# 检查是否存在 SSH 密钥 +ls ~/.ssh/id_ed25519.pub 2>/dev/null || ls ~/.ssh/id_rsa.pub 2>/dev/null + +# 不存在则生成 +ssh-keygen -t ed25519 -C "autoresearch-zk" -f ~/.ssh/id_ed25519 -N "" + +# 添加 git.n.xiaomi.com 到 known_hosts +ssh-keyscan git.n.xiaomi.com >> ~/.ssh/known_hosts 2>/dev/null + +# 测试连通性 +ssh -T git@git.n.xiaomi.com + +# 若 Permission denied → 需要把公钥添加到 GitLab: +# cat ~/.ssh/id_ed25519.pub +# 打开 https://git.n.xiaomi.com/-/profile/keys 粘贴公钥 +``` + +### cml 环境检查(每次评测前必做) + +```bash +source ~/.cloudml-cli/.profile +which cml # 应输出 /root/.cloudml-cli/bin/cml + +# 不存在则安装 +sh -c "$(curl -fsSL https://cnbj1-fds.api.xiaomi.net/cloudml-cli/install.sh)" \ + && export PATH=$HOME/.cloudml-cli/bin:$PATH + +cml config show # 确认 default_config_context / AK / SK / workspace_id +# 缺失则 cml config init +``` + +**cml 配置**:context=`cloudml5-config`,workspace_id=`10065`,auth_type=`key` + +## 迭代循环 + +LOOP FOREVER: + +### 0. CML 评测(当前模型,先定位再动手) + +```bash +cml workflow run \ + --workflow_id f-20260408161444-wu3pz --version v27 \ + --global_inputs runDic=<编号> \ + --global_inputs model_path_new=<当前模型路径> \ + --global_inputs model_path_old=<基线模型路径> +``` + +历史记录:`/mnt/xiaoai-zk-model-train-tj5/workflow5/` + +> **注**:第一轮"当前模型"通常就是基线或最近一次训练的产物;后续轮次是 Step 5 刚训出来的 SFT checkpoint。 + +> **首次评测**:`model_path_new` 和 `model_path_old` 均设为同一个基准模型路径。此轮目的是建立 baseline 数据,GSB 对比结果中 B=0、G=0(自比无差异),重点关注基准模型在各集合上的**绝对准确率**。 + +#### 0.1 Gold drift 检查(R23 经验沉淀) + +测试集 gold 标注会随时间更新(业务规则演进),但本地 CSV 不一定同步。**每轮评测后立即对比上一轮的 `code_label` / `complex` 字段,发现差异立即处理**。 + +R23 实例:workflow17746→17749 之间,`导航过召回附近记忆` 子集 gold 翻转 100/240 条(`complex=True`→`False`),但本地 CSV 完全没动。如果不检查就当成模型回退,会用错误数据继续仿写。 + +**自动检查**(写到 Step 0 末尾): + +```python +import csv, ast +csv.field_size_limit(2**30) +def parse_code(v): + try: + x = ast.literal_eval(str(v)) + return x[0] if isinstance(x, list) and x else str(x) + except: return str(v).strip() + +def norm_complex(v): return str(v).strip().lower()=='true' + +# 取上一轮 + 本轮 specific_test_results.csv,按 (sub_cate, rid) 索引 +prev = {(r['sub_cate'], r['rid']): r for r in csv.DictReader(open(PREV_RESULT))} +cur = {(r['sub_cate'], r['rid']): r for r in csv.DictReader(open(CUR_RESULT))} +drifts = [] +for k in prev: + if k not in cur: continue + a, b = prev[k], cur[k] + if (parse_code(a['code_label']) != parse_code(b['code_label']) or + norm_complex(a['complex']) != norm_complex(b['complex'])): + drifts.append((k, a, b)) +``` + +**判定与动作**: +- `len(drifts) < 10` → 个例修正,记录到 `results/gold_drift/drift_.json`,继续 +- `len(drifts) >= 10` → **触发 H-i-T-L #1**,暂停迭代让人确认是否同步训练集 +- 任何 drift 发生时,对应 query 在训练集里的同 pattern 样本都要按新 gold 重检 + +### 1. 结果分析(分层,按优先级) + +**对比维度**(按优先级,一旦高层不达标就深入): + +1. **需求集合**(重点,当前瓶颈):`config.yaml` 的 `test_sets.requirements` +2. **大盘集(车载)**:只看车载子集 +3. **specific test** + +**达标条件**: +- 需求集合 ≥ 95% +- 大盘集(车载)降幅 ≤ 0.3% +- specific test 降幅 ≤ 1% + +**决策**: +- 全部达标 → 部署(`python modules/cloudml_deploy.py `),记录,循环结束 +- 不达标 → 进入 Step 2,**分析顺序:需求集合 → 车载大盘 → specific test** + +> **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接进入 Step 2 分析基准模型的错误分布,为后续迭代建立对照基准。 + +### 2. 问题分析 & 报告 + +目标不是"列错误",是回答三个问题: +- **根因属于哪一类?**(reward / data / 格式) +- **上轮干预是否按假设起效?**(若本轮是上轮训练结果,对照上轮 Step 3 的假设) +- **相比上轮,哪些错误是新引入的?**(跨轮 diff) + +> **首次评测差异**:无上轮可对比,跳过"上轮干预回顾"和"跨轮 diff"。此轮只回答:**基准模型的错误分布是什么?各集合/子集的绝对准确率和错误 pattern 是什么?** 所有错误统一标记为 `baseline`(既不是 persistent 也不是 new),写入 `error_registry.jsonl` 作为后续跨轮追踪的起点。 + +#### 2.1 术语与字段 + +**GSB**:G=旧错新对,S=旧新同,B=旧对新错(本步骤只关注 B 和 G)。 + +| 术语 | 定义 | +| --- | --- | +| 旧对新错率 | B 数 / 总数 × 100% | +| 相对基线变化 | 新模型准确率 − 基线准确率(百分点) | +| 准确率 | 该子集 GSB 统一准确率(`cleaned_predict` vs `label`) | +| 分流错误 | `origin_predict_base` 含 `ComplexTask` 且 `origin_predict_dev` 以 `complex=false` 前缀开头。complex 判断本身错了,**即使 cleaned tag 相同也计为错** | +| 语义错误 | `cleaned_predict_base != cleaned_predict_dev`,意图/tag 本身判错 | +| 持久错误 | 该 case 在上一轮也错(查 `error_registry.jsonl`) | +| 新引入错误 | 该 case 在上一轮对,本轮错 —— **最危险的信号,说明上轮干预有副作用** | + +一个 case 可同时属于分流错误和语义错误。 + +| 字段 | 含义 | +| --- | --- | +| `origin_predict_base` | 旧模型原始输出,可能是 `ComplexTask(tag="...")` | +| `origin_predict_dev` | 新模型原始输出,始终以 `complex=true/false` 前缀开头 | +| `cleaned_predict_*` | 清洗后输出,用于 GSB 对比 | +| `label` / `code_label_base` | ground truth;`label` 为空时回退解析 `code_label_base` | + +#### 2.2 分析步骤 + +1. 读 `metric_diff/specific_comparison.csv` 和需求集合对应 CSV,`utf-8-sig`。 +2. 筛 `纯模型GSB == 'B'`,按 `sub_cate` 统计 B/总数/率,B 降序。 +3. 对每个子集统计分流错误 / 语义错误数。 +4. **跨轮 diff**:对每个 B case 查 `error_registry.jsonl`,标记 `persistent` / `new` / `regressed`。 +5. 按 query 内容归类 pattern:过召、丢失、误判、噪声、意图漂移等。 +6. 每个 pattern 列 2–5 个典型 case,**优先列 new/regressed 的**。 +7. 做归因(见 2.4)。 +8. 更新 `error_registry.jsonl`:写入本轮所有 B case 的 `(query_hash, runDic, was_wrong=True)`。 +9. 写入 `results/workflow.md`。 + +> **首次评测差异**:由于新旧模型相同,B=0、G=0,步骤 2–4 无数据可分析。改为: +> 1. 从 lark_template.json 提取各集合/子集/设备维度的**绝对准确率**。 +> 2. 统计模型预测 vs label 不一致的 case(即基准模型本身的错误),按 `sub_cate` 聚类。 +> 3. 跳过跨轮 diff(步骤 4),所有错误标记为 `baseline` 写入 `error_registry.jsonl`。 +> 4. 按 pattern 归类错误并做归因,建立初始错误画像。 +> 5. 写入 `results/workflow.md`(使用首次评测报告模板)。 + +#### 2.3 需求集合深度分析(专题) + +当前瓶颈是需求集合,单独做一节: + +1. **子集级表格**:每个需求子集 pass rate、距离 95% 的 gap、对比上轮、对比基线。 +2. **失败 case 与训练数据的关联**:对每个失败 case 在 `ai-planning/data/train_set/zk_intent/` 下所有 `.jsonl`(包括历史增强 `augment_*.jsonl` 和原始种子训练文件,排除 `*_valid.jsonl`)里做近似检索(前 3 近邻),判断: + - **数据缺失**:训练集中没有类似样本 → 补数据 + - **数据充足但仍错**:训练集中有类似样本 → 数据质量 or 学不会,不是加数据的问题 +3. **Reward 对齐检查**(抽样 10 条失败 case):用 `zk_reward_fn` 对"正确 label"和"实际输出"分别打分,验证 reward 方向是否和准确率一致。如果 reward 给错误输出的分更高 → reward 函数本身就有问题。 +#### 2.4 根因归类 + +把发现映射到一类根因,**每个 pattern 必须归一类**(可并列,但要主次分明): + +| 症状 | 可能根因 | 下一步动作 | +|---|---|---| +| 分流错误占比高 (>30%) | reward 对 complex 误判惩罚不足 | 改 `zk_reward_fn` 加分流项 | +| 语义错误集中在少数 tag | 训练数据该类分布不足 | 定向补数据 | +| 需求集合失败 case 在训练集有近邻 | 数据质量 / 标签噪声 / SFT 学不动 | 审核数据 + 加大 SFT epoch | +| 需求集合失败 case 在训练集无近邻 | 分布外 | 补数据(最直接) | +| new/regressed case 多 | 上轮干预有副作用 | 回退 or 缩小干预范围 | +| 训练 prompt ≠ eval prompt | 格式不匹配(silent bug) | 对齐模板,常常能"白捡"几个点 | +| 错误 query 含状态描述句但训练集标 Agent | 标签规则违反(R3) | 改训练集对应样本为 CT,按 4.3.1 规则检查 | +| 错误 query 是单 POI + 多形容词但训练集标 CT | 标签规则违反(R1) | 改训练集对应样本为 Agent | +| 错误 query 含真实动作(吃/喝/买)但训练集标 Agent | 标签规则违反(R2) | 改训练集对应样本为 CT | +| 同结构 query 在多个测试子集 gold 不同 | 子集 gold 矛盾(结构性天花板)| 不能靠 SFT 解,进入 2.5 天花板诊断 | + +#### 2.5 SFT 天花板诊断(R28 经验沉淀) + +当多个测试子集对**同结构 query** 的 gold 标注相反时,SFT 模型只能靠 query 文本预测,无法做到双向准确。**先识别天花板再决定是否继续投入**。 + +**实例**: +- 复杂导航 gold "第一个" / "继续导航" / "选择最顺路的那个" → Agent +- 可聊可控 gold "第一个" 在闲聊语境 → Chat +- 模型只看 query 必有一个子集错。R27 时复杂导航 +2.86pp 同时可聊可控 -2.35pp,几乎抵消。 + +**自动诊断**: + +```python +# 跨子集同形 query 矛盾检查 +from collections import defaultdict +query_to_golds = defaultdict(set) +for r in all_test_rows: # 所有 specific test 子集 + q = extract_query(r['input']).strip() + g_ct = norm_complex(r['complex']) + g_code = parse_code(r['code_label']) + query_to_golds[q].add((r['sub_cate'], g_ct, g_code)) + +conflicts = {q: gs for q, gs in query_to_golds.items() if len({(g[1], g[2]) for g in gs}) > 1} +conflict_rate = len(conflicts) / len(query_to_golds) +``` + +**判定**: +- `conflict_rate ≤ 5%` → 视作可接受噪声,继续 SFT 迭代 +- `conflict_rate > 5%` → **触发 H-i-T-L #2**,输出"结构性天花板"报告,让人选: + - 接受当前指标 + - 牺牲某一子集硬推目标子集 + - 转 RL(更细粒度 reward 可表达跨子集差异) + - 返回业务方修标 + +**报告字段**: +- 矛盾 query 数 / 占比 +- 每对矛盾的子集 gold 列表 +- 估算 SFT 理论上限(按子集大小加权) + +#### 2.6 报告模板 + +> **首次评测使用下方"首次评测报告模板",后续迭代使用"迭代报告模板"。** + +##### 首次评测报告模板 + +```markdown +# workflow 基准模型评测报告 + +**版本**: v28 | **执行ID**: ... | **基准模型**: ... + +> 首次评测:新旧模型相同,本报告只记录基准模型的绝对指标和错误分布,作为后续迭代的对照基准。 + +## 总览 +- **需求集合 (<名称>)**: xxx% ⭐ +- **大盘(车载)**: xxx% +- **Specific test**: xxx% + +## 需求集合分析 +### 子集表现 +| 子集 | 准确率 | 总数 | 错误数 | 距 95% gap | + +### 失败 case 与训练数据关联 +- <子集>: N 条失败中 X 条在训练集有近邻,Y 条无近邻 → 主要缺口: <数据缺失/质量> + +## 设备维度(只分析车载) +| 设备 | 准确率 | 总数 | 错误数 | + +全设备参考: +| 设备 | 准确率 | 总数 | 错误数 | + +## Specific Test 子集表现 +| 子集 | 准确率 | 总数 | 错误数 | + +## 基准错误画像 +### <错误 pattern>(数量) +**根因归类**: reward / data / 格式 +**典型 case**: +query: ... +label: ... +模型输出: ... +归类: 分流/语义 +训练集近邻: 有/无 + +## 初始错误分布总结 +| 根因类 | 错误数 | 占比 | 代表 pattern | + +## 下一轮假设候选(按 ROI 排序) +1. <假设>: 基于基准错误画像 ,预期收益 +2. ... +``` + +##### 迭代报告模板 + +```markdown +# workflow 回归分析报告 + +**版本**: v27 | **执行ID**: ... | **新模型**: ... | **基线**: ... + +## 上轮假设回顾(若本轮是上轮训练结果) +- **假设**: +- **干预**: - +- **预测**: <...> +- **判定**: ✅ hit / ❌ miss / 🟡 partial — <一句话说明> + +## 总览 +- **需求集合 (<名称>)**: xxx%(vs 基线 ±x.xx%,vs 上轮 ±x.xx%)⭐ +- **大盘(车载)**: xxx%(vs 基线 ±x.xx%) +- **Specific test**: xxx%(vs 基线 ±x.xx%) + +## 跨轮追踪 +| 指标 | 本轮 | 上轮 | 基线 | +|---|---|---|---| +| 需求集合 | | | | +| 持久错误数 | | | — | +| 新引入错误数 | ⚠️ | — | — | +| 修复错误数 ✅ | | — | — | + +## 需求集合深度分析 +### 子集表现 +| 子集 | 基线准确率 | 新模型准确率 | 旧对新错 | 旧错新对 | 总数 | 分流错误 | 语义错误 | 变化 | + +> 准确率来自 lark_template.json(含 complex 门禁检查),B/G 数来自 CSV 的纯模型 GSB(不含 complex 门禁),两者口径不同。 + +### 失败 case 与训练数据关联 +- <子集>: N 条失败中 X 条在训练集有近邻,Y 条无近邻 → 主要缺口: <数据缺失/质量> +- Reward 对齐抽样: N/10 条 reward 方向与准确率一致 + +## 设备维度(只分析车载) +| 设备 | 基线准确率 | 新模型准确率 | 变化 | 旧对新错 | + +全设备参考(准确率来自 lark_template,含 complex 门禁检查): +| 设备 | 基线准确率 | 新模型准确率 | 变化 | 旧对新错(B) | 旧错新对(G) | + +> 注:B/G 数来自 overrall_comparison.csv 的纯模型 GSB(不含 complex 门禁),准确率来自 lark_template.json(含 complex 门禁),两者口径不同。 + +## Specific Test 旧对新错 Top 子集(不含需求集合) +| 子集 | 旧对新错 | 总数 | 率 | 旧错新对 | 相对基线 | + +## 问题模式分析 + +> **分析顺序**:先分析本次需求迭代集合(如 icl_test)的问题,再分析其他 Specific Test 问题。 +> Specific Test 旧对新错 Top 子集表中也应去掉需求集合子集(已在需求集合深度分析中覆盖)。 + +### 一、需求集合问题分析 + +### 二、其他 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 ✅ + +**错误类型**: +- <类型1>(x 条): 描述 +- <类型2>(x 条): 描述 + +**典型 case**(优先 new/regressed,必须包含对话历史): +``` +对话历史: + 用户: <之前的对话> + 小爱: <之前的回复> + ...(多轮则列出所有轮次,无历史则写"无") +query: <用户最后一句(当前 query)> +label: xxx +旧: xxx # origin_predict_base +新: xxx # origin_predict_dev(保留 complex= 前缀) +归类: 分流/语义/两者 | 跨轮: persistent/new/regressed +训练集近邻: 有/无(hash=xxx, 相似度 0.xx) +``` + +> **重要**:对话历史从 input 字段的 `[对话历史]` 段提取,不能省略。很多错误(如短句闲聊被误判为 Agent)只有在多轮上下文中才能理解根因。 + +**结论**: 一句话根因 +**建议**: 一句话改动(对应 2.4 的动作) + +## 优先级与下一轮假设 +| 优先级 | 问题 | B 数 | 根因类 | 建议动作 | + +**下一轮假设候选**(按 ROI 排序): +1. <假设>: 基于本轮发现 ,预期收益 +2. ... +``` + +#### 2.7 参考实现 + +```python +import re, csv, ast, json, hashlib +from pathlib import Path +from collections import defaultdict + +def extract_query(text: str) -> str: + parts = text.rsplit('用户: ', 1) + if len(parts) >= 2: + return parts[1].split('[function]', 1)[0].strip() + m = re.search(r'query:(.+?)(?:\n|context:|function:)', text) + return m.group(1).strip() if m else '<未提取到>' + +def get_label(row: dict) -> str: + if row.get('label', '').strip(): + return row['label'].strip() + clb = row.get('code_label_base', '').strip() + if not clb: + return '' + try: + parsed = ast.literal_eval(clb) + if isinstance(parsed, list) and parsed: + return '\n '.join(parsed) + except (ValueError, SyntaxError): + pass + 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'] + cpb, cpd = row['cleaned_predict_base'], row['cleaned_predict_dev'] + is_triage = 'ComplexTask' in opb and opd.startswith('complex=false') + is_semantic = cpb != cpd + return is_triage, is_semantic + +def case_hash(query: str, label: str) -> str: + 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: + history = registry.get(case_h, []) + if last_runDic in history: return 'persistent' + if history: return 'regressed' + 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 +``` + +### 3. 本轮假设(基于 Step 1–2 的证据) + +**只有证据充分时才往下走。** 在开始训练前,写入 `results/iteration_log.jsonl` 的 `hypothesis` 字段: +- **假设**:本轮要验证什么?(例:"分流错误主因是 reward 对 complex 误判的惩罚太弱") +- **干预**:具体改了什么?类型限一类:`reward` / `data` / `hparam` / `格式` +- **预测**:期望指标如何变化?(例:"需求集合车载子集 ≥ 93%,分流错误率 < 15%") + +没有假设就开始训练 = 随机游走。**如果本轮纯粹是跑一次稳定性复现,也要显式写 "replication"。** + +> 若 Step 1 显示全部达标 → 跳过 Step 3–5,直接部署。 + +### 4. 数据生成(GPT-5.4 从 badcase 增强) + +**只在 Step 2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由: + +| 归因 | 本步动作 | 跳到哪步 | +|---|---|---| +| `data` | 做 Step 4 数据增强(在已有数据基础上增删改) | Step 5(SFT 重训) | +| `格式`(prompt / label schema 不对齐) | 改 prompt 模板或 label 渲染逻辑 | Step 5(SFT 重训,格式变了必须重新 SFT) | +| `hparam` | 改 `config.yaml`(LR / batch / epoch 等) | Step 5(SFT 重训) | +| 假设本身站不住(Step 2 证据不支持 Step 3 的假设) | 不做任何训练 | **回 Step 3**,重写假设 | + +换句话说:Step 4 是"data 归因专属"的干预入口,其他归因各有各的干预点,往下找对应的 Step 就行。 + +#### 4.0 原始训练数据清洗(增强前必做) + +数据增强不仅仅是加数据,还必须对原数据集中的错误/不一致标签进行清洗,否则新增数据和旧数据矛盾,模型学不好。 + +**清洗原则(case 驱动,逐类分析)**: + +> **核心方法**:从测试集错误 case 出发,归类出具体的 query 类型/pattern,然后逐个 pattern 去训练集中检索同类 query,根据检索结果决定动作。**不做批量关键词匹配式的清洗**(已验证会导致不可控的副作用)。 + +1. **归类错误 case 的 query 类型**:对每个测试集子集的错误 case,按 query 语义归类(如"找附近充电桩"、"导航到xxx"、"停车费"、"短句闲聊"等),得到若干具体 pattern。 +2. **逐 pattern 检索训练集**:对每个 pattern,在训练集中搜索同类 query(语义近似检索或关键词匹配),判断: + - **训练集有同类 query 但标签错了** → **改标签**(只改这几条,不批量改同 tag 的所有数据) + - **训练集没有同类 query** → **加数据**(通过 GPT 增强生成) + - **训练集有同类 query 且标签正确但数量少** → **加数据**(同类样本太少模型学不到,需要增强该 pattern 的样本量) + - **训练集有同类 query 且标签正确且数量充足** → 不动(说明问题不在数据,可能是 reward / 格式 / 超参) +3. **标签缺失补全**:如果评测中出现训练集完全没有的 tag,需要补数据。 +4. **去除噪声标签**:标签和 query 明显不匹配的样本直接删除。 + +> **禁止批量清洗**:不要按关键词或 tag 批量修改训练数据。SFT 会从批量修改中学到过度泛化的模式(如"不要输出 ComplexTask"),导致不可预见的副作用。每次修改必须是针对具体 case 的精准操作。 + +**清洗存档(必须)**:任何对训练数据的删除、修改操作,都必须在覆盖原文件之前,把被删改的原始数据备份到 `results/data_clean_/` 目录下: +- `deleted_samples.jsonl`:被删除的样本(原样保留) +- `modified_samples.jsonl`:被修改的样本(保留修改前的版本) +- `data_clean_.log`:清洗摘要(改了什么、为什么改、影响多少条) + +这样任何一轮的清洗都可以回溯和回退。**不留存档就不允许覆盖原文件。** + +#### 4.0.1 旧数据删改的标准操作(SOP,R23-R28 经验沉淀) + +针对训练集 `ai-planning/data/train_set/zk_intent/*.jsonl` 的修改/删除,**必须**按这个流程,禁止脚本一把梭。 + +**Step A:候选定位(程序化扫描,不直接改)** + +```python +import json, re, glob +def extract_q(inst): + m = re.search(r'\[当前query\]\s*\n用户:\s*(.*?)\n\[function\]', inst, re.DOTALL) + return m.group(1).strip() if m else '' + +# 1. 定义 pattern(基于错例归类) +target_pat = re.compile(r'^(嗯添加|添加个)(一个|个)?途经点') +multi_pat = re.compile(r'(然后|接着|顺便|再帮|完了再)') + +# 2. 扫训练集 +candidates = [] +for f in glob.glob('ai-planning/data/train_set/zk_intent/*.jsonl'): + if '_valid' in f or '.bak' in f: continue + with open(f) as fp: + for ln, line in enumerate(fp): + d = json.loads(line) + q = extract_q(d['instruction']) + if target_pat.match(q) and not multi_pat.search(q) and 'ComplexTask' in d['output']: + candidates.append((f, ln, q, d['output'])) + +# 3. 输出 csv 让人审核(不改文件) +import csv +with open(f'/tmp/candidates_r{RUNDIC}.csv', 'w', encoding='utf-8-sig') as fp: + w = csv.writer(fp) + w.writerow(['file','line','query','old_label','是否改(1/0)','建议新label']) + for c in candidates: w.writerow(list(c)+['','']) +``` + +**Step B:量级判定(决定是否走人审)** + +| 候选量 | 处理 | +|---|---| +| ≤ 50 条 | 程序化 sanity check + 自动改 | +| 51 ~ 200 条 | **必须**抽样 30~50 条到飞书 sheet 让人审 1/0,按抽样比例外推到全量 | +| > 200 条 | **强制 H-i-T-L 介入**(触发条件 #3),让人定更精细 pattern 收窄 | + +**Step C:备份(强制,覆盖前必做)** + +```bash +cd ai-planning/data/train_set/zk_intent +for f in <要改的文件列表>; do + [ ! -f "$f.before_r${RUNDIC}.bak" ] && cp "$f" "$f.before_r${RUNDIC}.bak" +done +``` + +同时落归档到 `results/data_clean_/`: +- `deleted_samples.jsonl`:被删样本原文 +- `modified_samples.jsonl`:被改样本(含 before/after output) +- `data_clean_.log`:摘要 + 影响 pattern + 样本数 + +**Step D:修改执行(按文件批量,避免重复读写)** + +```python +edits_by_file = {} +for f, ln, q, old, new_label in confirmed_changes: + edits_by_file.setdefault(f, []).append((ln, new_label)) + +for f, edits in edits_by_file.items(): + with open(f) as fp: rows = fp.readlines() + for ln, new_label in edits: + d = json.loads(rows[ln]) + old_out = d['output'] + if new_label == 'Agent': + d['output'] = old_out.replace('ComplexTask(', 'Agent(') + elif new_label == 'CT': + d['output'] = old_out.replace('Agent(', 'ComplexTask(', 1) + rows[ln] = json.dumps(d, ensure_ascii=False) + '\n' + with open(f, 'w') as fp: fp.writelines(rows) +``` + +**Step E:修改后回归验证(强制)** + +改完不能直接训: +1. **数据 sanity**:`prepare_and_train_sft.py prepare`,对比合并后总数(旧总数 - 删除数 = 新总数) +2. **抽样人审**:随机抽 5 条,看 query/label/instruction 符合预期 +3. **格式校验**:用 4.1.1 字段约束扫一遍改后 output + +**Step F:删除 vs 修改的选择** + +| 情况 | 删除 | 修改 | +|---|---|---| +| 标签错但 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 输入 + +- **Badcase 来源**:Step 2 里 `纯模型GSB == 'B'` 的 case。按两个维度排优先级: + 1. **测试集维度**:本次需求集合 > 其他 specific test / 大盘(需求集合是当前瓶颈,优先补) + 2. **跨轮维度**:`new` / `regressed` > `persistent`(新引入/回退的先处理,持久错误兜底) + + 组合优先级 P0→P3:需求+new/regressed → 需求+persistent → 其他+new/regressed → 其他+persistent。 +- **按子集聚类**:每个需求子集/specific 子集分别处理,保证增强数据在该子集上的覆盖度。 +- **每条 badcase 的字段**:`query`、`对话历史`(从 input 提取的 `[对话历史]` 段)、`label`、`origin_predict_dev`(错误输出)、`sub_cate`。 + +#### 4.1.1 SFT 训练数据格式规范(生成前必读) + +**所有**生成的训练数据最终写入 `augment_.jsonl` 时,必须是如下 3 字段 JSONL 格式(与 `all_train.jsonl` 完全一致): + +```json +{ + "system": "你是小爱同学,中文智能语音助手。", + "instruction": "<见下方模板>", + "output": "Agent(tag=\"xxx\") 或 ComplexTask(tag=\"xxx\") 或 QA() 等" +} +``` + +**instruction 字段必须严格按如下模板**(逐字符对齐,不得自由发挥): + +``` +请参考用户的[当前query]、[对话历史]、[知识注入]、[系统状态]识别出[当前query]的[function]结果,[function]是python的code形式。 +[知识注入] +{ +"location": "", +"rag": "" +} +[系统状态] +{} +[对话历史] + +[当前query] +用户: +[function] +``` + +**字段格式约束**: + +| 字段 | 格式 | 示例 | 注意 | +|---|---|---|---| +| `"location"` | `"城市(市)区(区)位于中国(国家)省(省)"` 或 `""` | `"北京(市)海淀(区)位于中国(国家)"` | key 必须是 `"location"`,用标准双引号,不是反斜杠 | +| `"rag"` | `"实体1是类型\t实体2是类型"` 或 `""` | `"高德地图是APP\t百度地图是APP"` | key 必须是 `"rag"`(不是 `"tag"`),多实体用 `\t` 分隔 | +| `[对话历史]` | `用户: xxx\n小爱: xxx\n` | 见下方 | 无历史时为空(直接接 `[当前query]`) | +| `[系统状态]` | 固定 `{}` | `{}` | | + +**正确示例**: + +```json +{"system": "你是小爱同学,中文智能语音助手。", "instruction": "请参考用户的[当前query]、[对话历史]、[知识注入]、[系统状态]识别出[当前query]的[function]结果,[function]是python的code形式。\n[知识注入]\n{\n\"location\": \"北京(市)位于中国(国家)\",\n\"rag\": \"高德地图是APP\\t百度地图是APP\"\n}\n[系统状态]\n{}\n[对话历史]\n用户: 帮我导航去最近的加油站\n小爱: 好的,已经找到附近3个加油站\n[当前query]\n用户: 加完油再去机场接人,然后一起去三里屯吃饭\n[function]\n", "output": "ComplexTask(tag=\"地图导航\")"} +``` + +**常见错误(GPT 高发,必须在 sanity check 中拦截)**: + +| 错误 | 正确 | +|---|---| +| `\location\: \xxx\` | `"location": "xxx"` | +| `\tag\: \图片问答\` | `"rag": "图片问答是视频"` | +| key 用反斜杠转义 | key 用标准双引号 | +| `"tag"` 作为知识注入 key | `"rag"` 是唯一正确的 key | +| system = "你是一名Python程序员..." | system 固定为 "你是小爱同学,中文智能语音助手。" | + +> **为什么不用 Python程序员 prompt 格式?** `all_train.jsonl` 中 35379 条全部是"小爱同学"格式,不存在 Python程序员格式。混入不同 system prompt 会让模型困惑,必须统一。 + +#### 4.2 调用 GPT-5.4 生成同义训练样本 + +生成字段与归档 CSV(data_train 格式)的列名保持一致,避免归档时再做字段映射。 + +```python +import os +import requests, json, hashlib +from pathlib import Path + +API_URL = "http://model.mify.ai.srv/v1/chat/completions" +API_KEY = os.environ.get("OPENAI_API_KEY") or os.environ.get("MODEL_API_KEY") +if not API_KEY: + raise RuntimeError("缺少 OPENAI_API_KEY 或 MODEL_API_KEY") +API_HEADERS = { + "Authorization": f"Bearer {API_KEY}", + "X-Model-Provider-Id": "azure_openai", + "X-Model-Request-Id": "augment-gen", + "Content-Type": "application/json", +} +MODEL = "gpt-5.4" + +SYSTEM = """你是小爱同学中控理解训练数据生成助手。给定一条错误 case,生成若干条与其**意图相同**、**ground_truth 相同**、**表述多样**的训练样本。 + +要求: +1. ground_truth 严格照抄原 case,不得改写(包括 Agent(tag=...) / ComplexTask(...) / QA() / Chat() 等格式)。 +2. current_query 表述要多样化:口语/书面、长/短、有/无填充词、方言化等,但意图不能漂移。 +3. prev_session 保留原 case 的设备/场景线索,结构和原 case 一致(JSON 数组,每项含 query / tts / timestamp)。无历史时用 []。允许在合理范围内改写历史文本,但轮数、设备、场景不变。 +4. context 原样继承原 case(location / rag),不要改写、不要新造地点或 RAG 实体。 +5. 禁止生成与已知测试集 current_query 精确或近似重复的样本。 +6. 严格输出 JSONL,一行一条样本,不要解释、不要 markdown。每行格式(字段名与 data_train CSV 对齐): + {"current_query": "...", "prev_session": [...], "context": {"location": "...", "rag": "..."}, "ground_truth": "...", "sub_cate": "..."} + 其中 sub_cate 仅用于内部路由/去重,归档时会被丢弃,不写入 CSV。""" + +USER_TEMPLATE = """错误 case: +- sub_cate: {sub_cate} +- prev_session: {prev_session} +- context: {context} +- current_query: {current_query} +- 正确 ground_truth: {ground_truth} +- 模型错误输出: {predict} + +生成 {n} 条意图/ground_truth 一致、表述多样的训练样本。""" + +REQUIRED_KEYS = {"current_query", "prev_session", "context", "ground_truth"} + +def gen_augment(badcase: dict, n: int = 8) -> list[dict]: + payload = { + "model": MODEL, + "messages": [ + {"role": "system", "content": SYSTEM}, + {"role": "user", "content": USER_TEMPLATE.format(n=n, **badcase)}, + ], + } + resp = requests.post(API_URL, headers=API_HEADERS, json=payload, timeout=120) + resp.raise_for_status() + text = resp.json()["choices"][0]["message"]["content"].strip() + out = [] + for line in text.splitlines(): + line = line.strip().lstrip("```json").rstrip("```").strip() + if not line: + continue + try: + sample = json.loads(line) + if REQUIRED_KEYS.issubset(sample.keys()): + out.append(sample) + except json.JSONDecodeError: + continue + return out +``` + +**调用策略**: +- 每条 badcase 生成 6–10 条(依据子集缺口决定,缺口越大给越多)。 +- **每类(每个 tag/意图)增强数据上限 50 条**。要克制,不要一次加太多。如果某类需要更多数据,应在下一轮迭代中逐步追加。 +- 按子集做速率限制;失败重试 3 次,指数退避。 +- **原始输出落到 `results/augment_raw/augment__raw.jsonl`**(跟本轮报告/log 一起归档,不是训练目录)。下一步 4.3 过完 sanity check 才写入训练目录。 + +#### 4.3 Sanity check & 后处理(silent bug 高发区) + +**生成完立即跑,否则 silent bug 会直接进训练:** + +- **instruction 格式校验(最高优先级)**:逐条检查 `instruction` 字段是否符合 4.1.1 模板: + 1. 必须包含 `"location"` 和 `"rag"` 两个 key(标准双引号,不是反斜杠) + 2. 不得出现 `\location\`、`\tag\`、`\rag\` 等反斜杠转义的 key + 3. `system` 必须为 `"你是小爱同学,中文智能语音助手。"`(不是 Python程序员) + 4. 必须包含 `[知识注入]`、`[系统状态]`、`[对话历史]`、`[当前query]`、`[function]` 段 + 5. 不符合的**整条丢弃**,不要尝试修复(GPT 格式错误通常是系统性的,修一个字段其他字段也不可信) +- **ground_truth 格式校验**:`Agent(tag=...)` / `ComplexTask(...)` / `QA()` / `Chat()` 是否和测试集一致?GPT-5.4 偶尔会把 tag 改写或加空格,必须用 `zk_reward_fn` 里的 parser 过一遍,parse 失败的扔掉。 +- **context 完整性**:`context` 必须是 dict 且含 `location` / `rag` 两键(可为空字符串但不能缺);`prev_session` 必须是 list。结构不符的丢弃。 +- **泄漏检测**:生成 `current_query` 与所有测试集(需求集合 + 大盘 + specific)做**精确匹配 + 近似匹配**(MinHash or embedding cos > 0.9)。命中则整条丢弃。 +- **Label 自洽**:生成的 `(current_query, prev_session, context) → ground_truth` 必须和原 badcase 的 ground_truth 语义一致。抽 10% 跑一次 GPT-5.4 自检("下面这条 current_query 的正确 ground_truth 是什么?"),和声明 ground_truth 不一致的丢弃。 +- **Prompt 模板一致性**:并入训练前,把 `(prev_session, context, current_query)` 渲染成最终 SFT prompt,和 eval prompt 逐字段比对(`complex=true/false` 前缀、system prompt、function schema),不一致就对齐模板——常常能白捡几个点。 +- **去重**:与 `ai-planning/data/train_set/zk_intent/` 下所有已存在的 `*_train.jsonl`(历史增强 + 原始种子训练文件)去重(`current_query` 精确 + 近似)。 + +#### 4.3.1 标签规则约束(R28 经验沉淀) + +4.3 只检查格式 / 泄漏 / label 自洽,**不检查标签语义规则**。R23-R28 多次出现"格式正确但 label 违反业务规则"的样本进入训练(augment_17749 30 条「从X到Y」反向 / augment_17752 23 条短延续 / augment_17754 30 条 模糊属性 → CT 等)。 + +**生成完每条样本必须过以下规则检查,不通过整条丢弃**: + +##### 已确立的规则(按优先级) + +| ID | 规则 | 例 | +|---|---|---| +| R1 | 单 POI + 任意数量形容词修饰 → Agent | "帮我找最近的咖啡店" → Agent | +| R2 | 单 POI + 真实可执行动作(吃/喝/买/卖/看/玩/接/送)→ ComplexTask | "去海底捞吃饭" → CT | +| R3 | 用户状态描述句("我XX有问题/累/赶时间/电量低/感冒了")+ POI → ComplexTask | "我轮胎有点问题,找个补胎店" → CT | +| R4 | 多动作复合("X然后Y" / "先X再Y" / "加途经点 X 然后到 Y")→ ComplexTask | "先去加油再去机场" → CT | +| R5 | 上下文延续短 query(依赖前轮)→ 跟随前轮分类 | 前轮 CT,"第一个" → 跟前轮 | +| R6 | 路线偏好(走高速/走国道/走主路)+ POI → ComplexTask | "导航回家走高速" → CT | +| R7 | 含问句词(什么/哪个/哪里/在哪)+ 导航请求 → ComplexTask | "厦门有什么好吃的给我导航" → CT | + +> 规则随每轮新发现持续追加。新规则确立必须经人审核(4.3.2 自动归纳,必要时触发 H-i-T-L #2/#6),归档到 `results/label_rules.md`。 + +##### 自动检查代码模板 + +```python +def check_label_rules(query: str, label: str, history: list = None) -> tuple[bool, str]: + """返回 (是否通过, 不通过原因)""" + is_ct = 'ComplexTask' in label + + # R3: 状态描述句必须 CT + state_pat = re.compile( + r'(我.{0,5}(轮胎|车|手机).{0,5}(扎|坏|爆|漏|有问题|出问题))|' + r'(我.{0,5}(感冒|累|饿|渴|赶时间|快迟到|怕迟到|生病))|' + r'(我.{0,5}电量.{0,5}(只剩|不多|快没|没了))|' + r'(车.{0,5}(没油|快没油|没电|爆胎|坏了))' + ) + if state_pat.search(query) and not is_ct: + return False, 'R3 violation: 状态描述句应 CT' + + # R2: 真实可执行动作 + POI 应 CT + action_pat = re.compile(r'(去|要|来).{0,5}(吃|喝|买|卖|看一看|看一下|打卡|拍照|玩|逛|接|送)') + if action_pat.search(query) and not is_ct: + return False, 'R2 violation: 真实动作应 CT' + + # R6: 路线偏好 + POI 应 CT + route_pat = re.compile(r'(走高速|走国道|走主路|走快速路|走小路|走高架)') + has_poi = bool(re.search(r'[一-龥]{2,8}(路|街|站|广场|中心|公园|医院|餐厅|店|馆|区|城|湖|大厦)', query)) + if route_pat.search(query) and has_poi and not is_ct: + return False, 'R6 violation: 路线偏好+POI 应 CT' + + # R7: 问句 + 导航 应 CT + if re.search(r'(什么|哪个|哪里|在哪).*(导航|去|到)', query) and not is_ct: + return False, 'R7 violation: 问句导航应 CT' + + # R4: 多动作复合 应 CT + multi_pat = re.compile(r'(然后|接着|顺便|再帮|完了再|然后再|顺路|再去|.{0,5}先.{0,8}再)') + if multi_pat.search(query) and not is_ct: + return False, 'R4 violation: 多动作应 CT' + + return True, '' +``` + +##### 跨子集冲突检查(R28 经验沉淀) + +每条仿写 query 在所有测试集 csv 里搜: +- 若同 query 或高度相似(cos > 0.9)出现在 ≥2 个子集且 gold 不同 → **整条丢弃**(避免训练矛盾信号) + +##### 过通用度检查(防 R26 教训) + +仿写 query 满足以下全部 → 整条丢弃: +- 长度 ≤ 6 字 +- 不含具体地名/技能词 +- 与训练集已有 query 重复 + +> R26 augment_17752 加了 23 条"第一个 / 选第3 / 继续往前导航"等过通用短词,污染了可聊可控子集 -2.35pp。这种短 query 必须依赖对话历史才有意义,单独作训练样本会污染跨子集判定。 + +##### 仿写量级硬阈值 + +| 类型 | 单轮上限 | +|---|---| +| 旧标签批改 | 50 条(>50 触发 H-i-T-L #3) | +| 新仿写 | 100 条(按 sub_cate 分配,每类 ≤ 50) | +| 单轮总变更 | 150 条 | + +超阈值必须人介入定边界,不允许自动放行。 + +#### 4.3.2 从评测集自动归纳新规则(替代旧版"新规则不明确"信号) + +**核心观察**:测试集 CSV 已含 gold (`code_label` / `complex`),所谓"新规则"不是 gold 不明确,而是**模型尚未学到测试集已存在的 gold 规律**。这种情况自动归纳即可,不该让人。 + +##### 触发时机 + +每轮 Step 2 错例分析后,对**所有未被 R1~R{N} 已知规则覆盖的错例**做自动归纳。 + +##### Confidence 的定义(两个独立指标,必须同时满足) + +新规则候选必须通过**两层置信度检查**: + +**c1 = 错例内一致性**:候选 pattern 在错例中 gold 标注的主流占比 + +``` +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**(自动从测试集抽 3 条): +- {case1} +- {case2} +- {case3} +**自动检查正则**:`{regex}` +``` + +新规则立即在下一轮 4.3.1 检查中生效,**不需要人审**。 + +##### 什么时候自动归纳失败 → 才触发人介入 + +仅当以下情况自动归纳算法**得不出结论**时,才转 H-i-T-L 信号: + +- **触发信号 #2**:同结构 query 在 ≥2 个测试子集 gold 不同(自动归纳得到的规则会矛盾) +- **触发信号 #6**:同 pattern 错例 < 3 条(样本不足无法归纳)但持续多轮出现 → 让人决定是否手动定规则 + +> 这就是为什么删除了旧的"新规则不明确"信号——**99% 情况自动归纳就能解决**,1% 真矛盾的情况已被信号 #2/#6 覆盖。 + +#### 4.4 写入训练目录 + +Sanity check 全过后,把清洗结果写入训练目录。文件名**不能含 `_valid`**,否则 `prepare_and_train_sft.py` 会把它归为验证集: + +``` +ai-planning/data/train_set/zk_intent/augment_.jsonl +``` + +落盘后即可被 Step 5 的 `prepare_and_train_sft.py` 自动扫描到并合并进训练。 + +**归档(迭代结束后做,不是训练流程的一部分)**:把所有 `augment_.jsonl` 转成 data_train 格式的 CSV,列如下: + +| 列名 | 类型 | 说明 | +|---|---|---| +| `current_query` | str | 当前轮用户 query | +| `prev_session` | str(JSON array) | 多轮历史,`[{"query","tts","timestamp"}]`,无历史写 `[]` | +| `context` | str(JSON object) | `{"location","rag"}`,字段保留但允许空字符串 | +| `ground_truth` | str | 正确 label,如 `Agent(tag="...")` / `ComplexTask(tag="...")` | + +JSONL 行里的 `sub_cate` 字段仅用于内部路由/去重,归档时丢弃不写入 CSV。因为 4.2 的输出 schema 已经和 CSV 列名对齐,归档就是把每个 `augment_.jsonl` 行序列化成 CSV 单元格(`prev_session` / `context` 两列用 `json.dumps` 回写成字符串),没有字段重命名。 + +### 5. SFT 训练 + +使用 `prepare_and_train_sft.py` 完成数据组装和训练。 + +#### 5.0 环境准备 + +```bash +# 克隆 zk_trainer(仅首次) +cd /mnt/wangsenhao/autoresearch-zk +git clone git@git.n.xiaomi.com:wangsenhao/zk_trainer.git +``` + +**训练配置**: + +| 参数 | 值 | +|---|---| +| 基模 | `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507` | +| model_type | `qwen3` | +| accelerate 配置 | `/mnt/xiaoai-zk-model-train-tj5/common/accelerate_config_0.yaml` | +| FSDP | stage 3, bf16 | +| epochs | 3 | +| learning_rate | 1e-5 | +| max_seq_length | 1024 | +| lr_scheduler | cosine, warmup_ratio=0.1 | +| optimizer | Adam (β1=0.9, β2=0.95, ε=1e-9) | +| dataset_type | `zk_sft` | +| save_strategy | no(训练完直接保存最终 checkpoint) | +| VOLUME_PREFIX | `/mnt/xiaoai-zk-model-train-tj5`(labelref.json / tagref.json 所在,脚本自动设置) | + +#### 5.0.1 训练产物备份(R23-R28 经验沉淀) + +**训练前强制**: + +```bash +# 上一轮 sft_output 改名归档(保留至少 5 轮,方便快速回退评测) +[ -d sft_output ] && mv sft_output sft_output_r${PREV_RUNDIC} +``` + +R23-R28 期间累积了 `sft_output_r23` ~ `sft_output_r28`,多次需要回退到上一轮模型重评(如 gold drift 验证、副作用对照)。**没备份就丢失了一切对照能力。** + +**自动评测 watcher**: + +R29 起 SFT 走 cml custom_train submit(见 5.2),watcher 直接复用 `submit_sft_via_cml.sh` 内嵌的轮询逻辑——`cml custom_train describe` 检测 succeed → 验产出 → 自动起 `cml workflow run` 评测。本地无 PID 可监控。 + +旧版 nohup 路径下的 watcher 模板(仅在 5.2 例外情况使用): + +```bash +#!/bin/bash +# /tmp/auto_eval_after_train.sh —— 等本地训练 PID 结束 → 自动起 cml workflow +TRAIN_PID=$1 +RUN_DIC=$2 +MODEL_NEW=/mnt/wangsenhao/autoresearch-zk/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 数据组装 + +```bash +python prepare_and_train_sft.py prepare --output_dir ./sft_data + +# 如果 data_train/ 下的 CSV 有改动,先从 CSV 重新生成 JSONL +python prepare_and_train_sft.py prepare --regen_from_csv --output_dir ./sft_data +``` + +脚本行为: +1. 扫描 `ai-planning/data/train_set/*/` 下所有 `.jsonl` 文件 +2. 文件名含 `_valid` → 验证集,其余 → 训练集(包括 `all_train.jsonl`、`augment_*.jsonl` 等) +3. 合并输出到 zk_trainer 所需的目录结构: + +``` +/ + train/zk_sft_new_structure/merged_train.jsonl # 合并后的训练数据 + validation/zk_sft_new_structure/part-0.jsonl # 合并后的验证数据 +``` + +> 验证集文件名不能含 `valid`/`test`/`eval`(HF datasets 会推断错误的 split),脚本自动重命名为 `part-0.jsonl`。 + +#### 5.2 启动训练(cml 任务,R29 起统一走这条路径) + +R23-R28 期间用本地 `nohup python3 prepare_and_train_sft.py train ...` 启动,会因登出/网络/会话退出而中断,并占用本地工作机 8 卡 GPU。**R29 起统一改为 cml custom_train submit 提交训练任务**,由 CloudML 调度到 `bj-nlp` 队列的 h20-96g 8 卡,本地零占用。 + +##### 一键提交脚本 + +```bash +cd /mnt/wangsenhao/autoresearch-zk +./scripts/submit_sft_via_cml.sh [PREV_RUNDIC] + +# 例:R29 训练(上一轮 R28) +./scripts/submit_sft_via_cml.sh 17756 17755 +``` + +脚本做的事: +1. 渲染 `scripts/sft_train_job.yaml.tpl` 为本轮 yaml(替换 RUNDIC / PREV_RUNDIC) +2. `cml custom_train submit --filename ` 提交任务,拿 `JobID` +3. 后台 watcher 进程:每 60s `cml custom_train describe` 轮询任务状态 +4. 任务 succeed → 验证 `sft_output/_SUCCESS` 存在 → 自动起 cml workflow run 评测 +5. 任务 failed/killed → 写日志报警 + +##### 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 + +# 查训练实时日志(含 accelerate / trainer 输出) +cml custom_train logs --follow + +# 停任务 +cml custom_train kill + +# 查 watcher(自动评测)日志 +tail -f /tmp/r_logs/cml_watcher.log +``` + +##### 与 nohup 路径的对比 + +| 维度 | nohup(旧) | cml submit(新) | +|---|---|---| +| 本地 GPU 占用 | 8 卡满载 | 0 | +| 网络/会话中断容忍 | 中断即停 | 任务持续跑 | +| 节点故障重试 | 无 | 自动 2 次 | +| 失败告警 | 无 | 飞书 P2 | +| 任务历史 | 仅本地 log | cml 平台可查 | +| 启动复杂度 | 单行 nohup | 单行 submit_sft_via_cml.sh | + +##### 何时仍用 nohup 本地训 + +只有以下情况例外使用本地训: +- cml 队列资源排队 > 30min(紧急复现验证) +- 调试新训练逻辑(频繁改代码) +- 实验性超小规模训练(< 1000 条数据,< 2 epoch) + +其余一律走 cml。 + +##### 兼容旧脚本 + +`prepare_and_train_sft.py` 不动,cml 任务的 `imageCommand` 内部仍调用它。`train` 子命令本身只负责训练,需先执行 5.1 组装数据。配置见 5.0。 + +### 6. 记录结果 + +每轮写入 `results/iteration_log.jsonl` 一行,schema: + +```json +{ + "iteration": 12, + "runDic": 45, + "timestamp": "2026-04-23T...", + "hypothesis": "加分流 reward 项能降分流错误率到 15% 以下", + "intervention": { + "type": "reward", + "summary": "zk_reward_fn: complex 误判额外 -0.3" + }, + "prediction": {"req_set_car": 93.0, "triage_err_rate": 0.15}, + "results": { + "req_set_car": 92.1, "dapan_car": 96.30, + "specific_test": 95.10, "triage_err_rate": 0.18 + }, + "verdict": "partial", + "root_cause_findings": [ + {"pattern": "Chat 误激活 Agent", "class": "data", "cases": 14}, + {"pattern": "导航→旅游", "class": "reward", "cases": 8} + ], + "error_delta": {"persistent": 32, "new": 9, "fixed": 17}, + "next_hypothesis": "Chat 过召主要是训练集 Chat 样本太少;下轮补 500 条" +} +``` + +同时追加 `error_registry.jsonl`:每个 B case 一条 `{case_hash, runDic, sub_cate, query, label}`。 + +### 7. 回到 Step 0(评测刚训出来的 SFT checkpoint) + +## 决策规则 + +| 情况 | 处理 | +|------|------| +| 本轮 verdict = miss 且 new error 多 | 回退干预,缩小修改范围 | +| 连续 2 轮 miss 同一假设 | 假设错了,换思路(见 2.4)| +| SFT loss > 2.0 | 数据质量,不是训练问题 | +| 持久错误占 B 的 >70% | 单靠当前数据解不了,改 prompt 或回头审视 reward / 格式 | +| 新引入错误占 B 的 >30% | 上轮干预有副作用,**必须**先回退再推进 | +| 大盘降 > 0.3% | 回退上版,分析原因 | +| specific 降 > 1% | 定向补该类数据 | +| 需求集合 < 95% | 按 2.3 做深度分析,按 2.4 归因再动手 | +| OOM | 减 batch size | +| 连续 3 轮无改善 | 强制换策略:换数据比例 / 换超参 | +| **跨子集净退步:目标子集 +X / 其他子集合计 -Y, 净 < 0.3pp** | **回退或缩小干预范围**(R27 经验:单看目标子集涨容易自欺)| +| **连续 3 轮目标子集净提升 ≤ 0.5pp** | **进入瓶颈期**:输出 SFT 天花板报告,触发 H-i-T-L #6 | +| **跨子集 gold 矛盾率 > 5%** | **结构性天花板**:触发 H-i-T-L #2,停 SFT 转 RL 或返工标注 | +| **要批改旧标签 > 50 条** | **触发 H-i-T-L #3**,抽样人审,按比例外推 | +| **Gold drift ≥ 10 条** | **触发 H-i-T-L #1**,暂停迭代确认是否同步训练集 | +| 训练前未备份 sft_output | **强制 mv sft_output sft_output_r{prev}**,不允许覆盖 | + +## Human-in-the-Loop 时机(R23-R28 经验沉淀) + +迭代默认全自动跑(评测→分析→数据→训练→评测...)。**只在以下信号出现时停下问人**,其他情况自主推进。 + +### 启动前的基础项(迭代开始之前必须确认,独立于 7 类信号) + +启动一个新需求集合的迭代之前,必须先和人确认: + +1. **需求集合定义**:哪些子集属于本次需求(如 icl_test 的 8 个子集),明确目标 +2. **badcase 来源**:测试集 CSV 路径、版本,本地是否已同步 +3. **超参大幅调整**:当用户主动要求改 `epochs` / `lr` / `batch_size` 等核心超参时,先确认再改 + +迭代开始后,进入下面的 7 类信号触发判定。 + +### 触发人介入的 7 类信号(迭代级) + +| # | 触发条件 | 应对动作 | 经验来源 | +|---|---|---|---| +| 1 | **Gold drift 检测到 ≥10 条** | 暂停迭代,让人确认是否同步更新训练集 | R23 100 条翻转 | +| 2 | **跨子集同形 query gold 矛盾率 > 5%** | 输出"结构性天花板"报告,让人选:硬推目标子集 / 接受 / 转 RL | 复杂导航 vs 可聊可控 矛盾 | +| 3 | **要批改旧标签 > 50 条** | 暂停,导出抽样 ≥30 条到飞书 sheet 让人逐条确认 | R28 改 230 条引发副作用 | +| 4 | **跨子集净退步**(目标 +X / 其他合计 -Y, 净 < 0.3pp) | 暂停,让人决策回退 / 接受 / 换策略 | R27 可聊可控 -1.34pp | +| 5 | **连续 3 轮目标子集净提升 ≤ 0.5pp** | 输出 SFT 天花板报告,让人选继续 SFT / 转 RL / 接受 / 换基模 | R25-R28 复杂导航 +0.7~3pp 递减 | +| 6 | **测试集错例里 gold 可疑(自相矛盾的同 pattern)** | 列出可疑 gold 让人确认 / 反馈标注团队 | R28「找+模糊属性」双向标注 | +| 7 | **达标但有副作用**(需求集合达 95% 但 specific 降 ≥ 0.5pp)| 暂停部署,让人决策:部署 / 微调修复 | 预防性 | + +### 训练集调整专项 H-i-T-L 信号 + +任何对 `ai-planning/data/train_set/zk_intent/*.jsonl` 的修改/删除/新增动作,**先在程序内做下面的 10 项判定**,命中任一项就暂停问人;都不命中才能自动执行。 + +| # | 触发条件 | 应对动作 | 经验来源 | +|---|---|---|---| +| T1 | **单轮批改旧标签 > 50 条** | 抽样 30 条到飞书 sheet 逐条审;按抽样的"改/不改"比例外推全量 | R28 改 230 条引发跨子集副作用 | +| T2 | **单轮新仿写 > 100 条 OR 单类 (同 sub_cate / 同 pattern) > 50 条** | 抽样 20 条让人审风险,量级大要拆批 | R23 一次 670 条仿写过载,规则错全反 | +| T3 | **删除训练样本 > 30 条** | 列删除清单 + 删除原因,人确认后才执行(删比改更不可逆) | R26 删 23 P0 通用短词产生副作用 | +| T4 | **修改 `all_train.jsonl` 主集 > 20 条** | 核心训练集动一行都贵;列改动给人确认 | 主集影响所有 sub_cate,副作用范围最大 | +| T5 | **跨子集冲突 query:同结构 query 在 ≥3 条训练样本里 gold 不一致** | 让人定边界规则(如「沿途搜 X」是 Agent 还是 CT),不许两边都加 | R26 augment_17752 同 pattern 矛盾仿写 | +| T6 | **仿写新 pattern 在测试集找不到锚定 case** | 让人确认这个 pattern 是否真存在,避免凭空生成对模型有害的样本 | R28 augment_17754 B 类 30 条无测试集锚定 | +| T7 | **仿写 query 含"过通用"特征**(长度 ≤6 字 + 不含具体地名/技能词) | 整批暂停,让人确认是否丢弃;这种短 query 必依赖上下文,单独训会污染跨子集判定 | R26 "第一个/选第3/继续往前" 23 条 → 可聊可控 -2.35pp | +| T8 | **本轮修改的 pattern 在前 3 轮曾导致回退(查 iteration_log.jsonl)** | 让人决定是同方向加大力度还是换 pattern;不许重蹈覆辙 | 防止反复在同 pattern 上来回拉锯 | +| T9 | **备份失败 / `.bak` 文件已存在但内容与当前文件相同** | 立即停,让人手动检查;**绝不允许覆盖原文件** | SOP 强制要求 (4.0.1 Step C) | +| T10 | **单轮跨文件批改 > 5 个 `*.jsonl` 文件** | 列影响文件清单 + 每个文件改动数;让人确认范围合理 | R28 改 11 个文件 262 条,影响面失控 | + +> **执行顺序**:T9 (备份) > T1-T4 (量级) > T5-T8 (语义/历史) > T10 (范围)。任一命中即停。 + +> 注:旧版本曾把"新发现 gold 规则不确定"作为人介入信号,已废弃。**测试集 CSV 已含 gold (`code_label`/`complex`),规则可从评测集错例自动归纳**,不需要人定(详见 4.3.2)。 +> +> **审核交付物**:触发任一信号时,输出到 `/tmp/train_audit_.csv` 或写入飞书 sheet(视量决定),含 `(file, line, query, history, old_label, new_label, reason)` 7 列。 + +### 不触发人介入(全自动) + +**A. 评测/分析层**:评测正常完成 / 大盘 ±在阈值内 / 目标子集 +0.5pp 以上 / 上轮假设 hit/partial / new error ≤ 30% / persistent < 70% + +**B. 数据生成 / 修改层**:改旧标签 ≤ 50 条 / 仿写 ≤ 100 条 / 单轮总变更 ≤ 150 条 / sanity check 失败的样本自动丢 / 训练集近邻匹配高(自动归因 reward/格式)/ gold drift < 10 条 / 跨子集矛盾率 ≤ 5% + +**C. 训练 / 评测调度层**:prepare 数据 / 启训练 / 训练成功后自动启评测 / 备份 sft_output / 写 iteration_log + +**D. 假设 / 归因层**:根因明确归类按 2.4 路由 / 假设 hit/partial 同方向继续 / 假设 miss 但 new error 占比小重新形成假设 + +**E. 单轮目标达成层**:所有阈值满足且无副作用 → **自动部署 + 结束循环**;阶段性目标达成 → 写 milestone log,继续推下一目标 + +### 介入时的交付物(让人快速决策) + +每次触发介入,必须**主动**输出(不等人问): + +1. **触发原因**:哪一条信号 + 具体数字 +2. **现状量化**:目标子集 +X / 其他子集 -Y / 大盘 ±Z +3. **2-4 个选项**(用 AskUserQuestion 工具):每个选项含预期收益 + 风险 +4. **推荐选项**:基于经验给出推荐(标 "(推荐)") +5. **本地产物**:抽样 / 错例 csv / 飞书 sheet 链接 + +### 介入后的恢复 + +人决策后立即恢复全自动循环,**不再二次确认**当前轮的细节。除非人显式说"再问我"。 + +### 反模式(不应触发介入) + +- ❌ 训练前问"要训吗?" — 应直接训 +- ❌ 评测前问"要评吗?" — 应直接评 +- ❌ 改 ≤ 50 条标签前逐条问 — 应批量改后报告 +- ❌ 仿写 ≤ 100 条前预审 — 应生成后做 sanity 自动过滤 +- ❌ 单轮无副作用且达标 → 部署前问 — 直接部署 + +## 结果文件 + +- `results/iteration_log.jsonl`:每轮完整记录(含假设/干预/判定) +- `results/error_registry.jsonl`:跨轮错误追踪 +- `results/workflow.md`:每轮回归分析报告 +- `results/augment_raw/augment__raw.jsonl`:本轮 GPT-5.4 原始生成产物(归档用,不入训练) +- `ai-planning/data/train_set/zk_intent/augment_.jsonl`:本轮清洗后的增量增强数据,Step 5 的 `prepare_and_train_sft.py` 自动扫描合并进 SFT;迭代完毕后归档导出为 data_train 格式的 CSV +- `sft_output/` / `rl_output/` (训练前 mv 上一轮为 `sft_output_r/`,至少保留 5 轮) +- `results/data_clean_/`:旧数据清洗存档(deleted_samples.jsonl / modified_samples.jsonl / data_clean_.log) +- `results/gold_drift/drift_.json`:gold drift 检测结果(每轮强制写入,方便回溯) +- `results/label_rules.md`:已确立的标签规则集(R1~R7+,新规则 H-i-T-L 确认后追加) + +## NEVER STOP + +一旦开始,不要停下问人类。每轮必须: +1. **先评测当前模型** +2. 分析结果、归因 +3. 写假设 +4. 做干预 +5. 判定 hit/miss +6. 更新 error_registry +7. 写下轮假设 + +如果没思路了: +- 回读最近 3 轮 `iteration_log`,看有没有忽略的模式 +- 跑 SFT-only eval,判断是 SFT 就不行还是 RL 破坏了 +- 重新读 `zk_reward_fn` 和训练数据采样,找 silent bug + +循环直到人类打断,period。 diff --git a/skills/model-iteration/config.yaml b/skills/model-iteration/config.yaml new file mode 100644 index 0000000..85bae8d --- /dev/null +++ b/skills/model-iteration/config.yaml @@ -0,0 +1,13 @@ +# 小爱中控 autoresearch 迭代框架配置 + +# 评估阈值 +thresholds: + overall_max_drop: 0.003 + specific_max_drop: 0.01 + requirement_pass_rate: 0.95 + +# CML 评测工作流配置 +cml_eval: + workflow_id: f-20260408161444-wu3pz + version: v28 + diff --git a/skills/model-iteration/knowledge/multi_command_rules.md b/skills/model-iteration/knowledge/multi_command_rules.md new file mode 100644 index 0000000..192a242 --- /dev/null +++ b/skills/model-iteration/knowledge/multi_command_rules.md @@ -0,0 +1,21 @@ +# 多指令 vs ComplexTask 分流规则 + +## 核心判定 + +| 场景 | 输出 | 示例 | +|------|------|------| +| 多个独立命令拼在一句话里 | 多个 `Agent(...)` | "导航到大姚县打开微信播放音乐" → 3个Agent | +| 单个任务涉及多步骤/多目的地 | `ComplexTask(tag="...")` | "先去加油站再去机场" → 1个ComplexTask | + +## 判定要点 + +- **多指令(Multi-command)≠ ComplexTask**:用户在一句话中说了多个独立的命令(导航+播放+打电话+控制),每个命令应该独立解析为各自的 Agent,不是一个 ComplexTask +- **ComplexTask 是单任务多步**:只有当一个任务本身需要多步规划时才是 ComplexTask(如多目的地导航、带筛选条件的旅游规划) +- **"然后"不一定是 ComplexTask 信号**:如果"然后"连接的是不同领域的独立命令(导航+控制+播放),那是多指令;只有"然后"连接同一任务的多个步骤(先去A再去B)才是 ComplexTask + +## 识别方法 + +- output 中有多个 `Agent(...)` 调用 → 多指令,保持不变 +- output 中只有一个调用但 query 含多目的地 → 可能应该是 ComplexTask +- query 中"然后/再/接着"连接不同 tag 的任务 → 多指令 +- query 中"然后/再/接着"连接同 tag 的多个地点 → ComplexTask diff --git a/skills/model-iteration/knowledge/navigation_routing_rules.md b/skills/model-iteration/knowledge/navigation_routing_rules.md new file mode 100644 index 0000000..67fd10d --- /dev/null +++ b/skills/model-iteration/knowledge/navigation_routing_rules.md @@ -0,0 +1,21 @@ +# 地图导航 Agent / ComplexTask 分流规则 + +## 核心判定逻辑 + +| 场景 | 输出 | 示例 | +|------|------|------| +| 单POI + 任意数量形容词 | `Agent(tag="地图导航")` | 导航去最近的加油站、去附近最便宜的停车场 | +| 多POI(多个目的地) | `ComplexTask(tag="地图导航")` | 先去加油站再去机场接人 | +| 单POI + 一句话描述当前状态 | `ComplexTask(tag="地图导航")` | 加完油再去机场("加完油"描述当前状态) | + +## 判定要点 + +- **形容词不影响分流**:不管加多少形容词修饰(最近的、便宜的、大的),只要是单POI就是 Agent +- **多目的地 = Complex**:只要 query 中出现多个地点/动作序列,就是 ComplexTask +- **状态描述 = Complex**:单POI 但前面带了一句描述当前状态的话(如"加完油"、"吃完饭"),说明用户在做多步规划,属于 ComplexTask + +## 应用场景 + +- 数据增强时:按此规则标注 ground_truth +- Badcase 分析时:以此为标准区分"分流错误"和"语义错误" +- Reward 函数:分流违反此规则的应被惩罚 diff --git a/skills/model-iteration/knowledge/travel_routing_rules.md b/skills/model-iteration/knowledge/travel_routing_rules.md new file mode 100644 index 0000000..45d937b --- /dev/null +++ b/skills/model-iteration/knowledge/travel_routing_rules.md @@ -0,0 +1,26 @@ +# 旅游 Agent / ComplexTask 分流规则 + +## 核心判定逻辑 + +| 场景 | 输出 | 示例 | +|------|------|------| +| 规划路线,无筛选条件 | `Agent(tag="旅游出行")` | 规划一个从北京到上海的路线 | +| 规划路线,有筛选条件 | `ComplexTask(tag="旅游出行")` | 规划一个从北京到上海的路线,人少风景好、三天两夜、3000预算 | + +## 判定要点 + +- **无条件纯路线规划 = Agent**:只要求从A到B的路线,没有额外约束 +- **带筛选/约束条件 = Complex**:路线规划附带了时间、预算、偏好、天数等任何筛选条件,说明需要多维度规划,属于 ComplexTask + +## 筛选条件举例 + +- 时间约束:三天两夜、五一假期、周末 +- 预算约束:3000预算、经济型 +- 偏好约束:人少、风景好、适合亲子、有美食 +- 交通约束:自驾、高铁优先 + +## 应用场景 + +- 数据增强时:按此规则标注 ground_truth +- Badcase 分析时:以此为标准区分"分流错误"和"语义错误" +- Reward 函数:分流违反此规则的应被惩罚 diff --git a/skills/model-iteration/scripts/prepare_and_train_sft.py b/skills/model-iteration/scripts/prepare_and_train_sft.py new file mode 100644 index 0000000..d9a901d --- /dev/null +++ b/skills/model-iteration/scripts/prepare_and_train_sft.py @@ -0,0 +1,317 @@ +#!/usr/bin/env python3 +""" +从 ai-planning 组装 SFT 训练/验证集,并启动 zk_trainer SFT 训练。 + +用法: + # 仅组装数据 + AUTORESEARCH_ROOT=/mnt/wangsenhao/autoresearch-zk \ + python prepare_and_train_sft.py prepare --output_dir /mnt/wangsenhao/autoresearch-zk/sft_data + + # 启动训练(需先 prepare) + AUTORESEARCH_ROOT=/mnt/wangsenhao/autoresearch-zk \ + python prepare_and_train_sft.py train \ + --data_dir /mnt/wangsenhao/autoresearch-zk/sft_data \ + --model_path /mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507 \ + --model_type qwen3 \ + --train_output /mnt/wangsenhao/autoresearch-zk/sft_output +""" + +import argparse +import json +import logging +import os +import shutil +import subprocess +import sys +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") +log = logging.getLogger(__name__) + + +def _path_from_env(name: str, fallback: Path) -> Path: + return Path(os.environ.get(name, str(fallback))).expanduser().resolve() + + +AUTORESEARCH_ROOT = _path_from_env("AUTORESEARCH_ROOT", Path("/mnt/wangsenhao/autoresearch-zk")) +AI_PLANNING = _path_from_env("AI_PLANNING_DIR", AUTORESEARCH_ROOT / "ai-planning") +ZK_TRAINER_DIR = _path_from_env("ZK_TRAINER_DIR", AUTORESEARCH_ROOT / "zk_trainer") +TRAIN_SET_DIR = AI_PLANNING / "data" / "train_set" +DATA_TRAIN_DIR = AI_PLANNING / "data_train" +GENERATE_TRAIN_SCRIPT = DATA_TRAIN_DIR / "generate_train.py" + + +# ────────────────────────────────────────────── +# 1. 数据组装 +# ────────────────────────────────────────────── + +def regen_jsonl_from_csv(): + """调用 data_train/generate_train.py 从 CSV 重新生成 all_train.jsonl。""" + if not GENERATE_TRAIN_SCRIPT.exists(): + log.error(f"generate_train.py 不存在: {GENERATE_TRAIN_SCRIPT}") + return False + log.info("从 CSV 重新生成 JSONL ...") + result = subprocess.run( + [sys.executable, str(GENERATE_TRAIN_SCRIPT)], + cwd=str(DATA_TRAIN_DIR), + ) + if result.returncode != 0: + log.error("generate_train.py 执行失败") + return False + log.info("CSV → JSONL 完成") + return True + + +def scan_train_set(): + """扫描 train_set/ 下所有子目录,返回 (train_files, valid_files)。""" + train_files, valid_files = [], [] + if not TRAIN_SET_DIR.exists(): + log.error(f"train_set 目录不存在: {TRAIN_SET_DIR}") + return train_files, valid_files + + for subdir in sorted(TRAIN_SET_DIR.iterdir()): + if not subdir.is_dir() or subdir.name.startswith("."): + continue + for f in sorted(subdir.iterdir()): + if not f.is_file() or f.suffix != ".jsonl": + continue + if "_valid" in f.name: + valid_files.append(f) + else: + train_files.append(f) + return train_files, valid_files + + +def count_lines(path): + with open(path, "r", encoding="utf-8") as f: + return sum(1 for _ in f) + + +def prepare_data(output_dir: str, regen: bool = False, dataset_name: str = "zk_sft_new_structure"): + """ + 组装训练集和验证集到 output_dir,生成 zk_trainer 所需的目录结构: + output_dir/ + train//merged_train.jsonl + validation//part-0.jsonl + """ + output = Path(output_dir).resolve() + train_dir = output / "train" / dataset_name + valid_dir = output / "validation" / dataset_name + + if regen: + regen_jsonl_from_csv() + + train_files, valid_files = scan_train_set() + if not train_files: + log.error("未找到训练文件") + return None + + log.info(f"训练文件 ({len(train_files)}):") + for f in train_files: + log.info(f" {f.relative_to(AI_PLANNING)} ({count_lines(f)} 条)") + log.info(f"验证文件 ({len(valid_files)}):") + for f in valid_files: + log.info(f" {f.relative_to(AI_PLANNING)} ({count_lines(f)} 条)") + + # 清理旧数据 + for d in [train_dir, valid_dir]: + if d.exists(): + shutil.rmtree(d) + d.mkdir(parents=True) + + # 合并训练文件 + merged_path = train_dir / "merged_train.jsonl" + total_train = 0 + with open(merged_path, "w", encoding="utf-8") as out: + for fp in train_files: + with open(fp, "r", encoding="utf-8") as inp: + for line in inp: + out.write(line) + total_train += 1 + log.info(f"合并训练集: {total_train} 条 → {merged_path}") + + # 合并验证文件 + merged_valid_path = valid_dir / "part-0.jsonl" + total_valid = 0 + with open(merged_valid_path, "w", encoding="utf-8") as out: + for fp in valid_files: + with open(fp, "r", encoding="utf-8") as inp: + for line in inp: + out.write(line) + total_valid += 1 + log.info(f"验证集: {total_valid} 条 → {merged_valid_path}") + + log.info("=" * 50) + log.info(f"数据目录: {output}") + log.info(f" train: {total_train} 条") + log.info(f" validation: {total_valid} 条") + + return str(output) + + +# ────────────────────────────────────────────── +# 2. SFT 训练 +# ────────────────────────────────────────────── + +def run_sft_training(data_dir: str, model_path: str, model_type: str, train_output: str, + 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(ZK_TRAINER_DIR) + if not os.path.isdir(zk_trainer_dir): + log.error(f"zk_trainer 目录不存在: {zk_trainer_dir}") + return None + + data_dir = str(Path(data_dir).resolve()) + train_output = str(Path(train_output).resolve()) + os.makedirs(train_output, exist_ok=True) + + run_name = f"{model_type}-zk" + + env = os.environ.copy() + env["PYTHONPATH"] = f"{zk_trainer_dir}:{env.get('PYTHONPATH', '')}" + env["WANDB_DISABLED"] = "true" + env["WANDB_MODE"] = "offline" + env.setdefault("VOLUME_PREFIX", "/mnt/xiaoai-zk-model-train-tj5") + + # Step 1: 用 get_accelerate_config.py 动态生成 accelerate config + num_gpu_result = subprocess.run( + ["nvidia-smi", "--query-gpu=gpu_name", "--format=csv,noheader"], + capture_output=True, text=True + ) + num_processes = len(num_gpu_result.stdout.strip().split("\n")) if num_gpu_result.returncode == 0 else 1 + log.info(f"检测到 {num_processes} 张 GPU") + + acc_config_dir = zk_trainer_dir + gen_config_cmd = [ + sys.executable, f"{zk_trainer_dir}/llm/train/get_accelerate_config.py", + "--dt_framework=fsdp", + "--fsdp_sharding_strategy=1", + "--machine_rank=0", + "--num_machines=1", + f"--num_processes={num_processes}", + f"--output_dir={acc_config_dir}", + "--main_process_ip=127.0.0.1", + "--main_process_port=56390", + "--mixed_precision=bf16", + f"--model_type={model_type}", + "--ds_stage=3", + "--fsdp_version=2", + "--fsdp_wrap_cls=", + ] + log.info("生成 accelerate config ...") + result = subprocess.run(gen_config_cmd, env=env, cwd=zk_trainer_dir) + if result.returncode != 0: + log.error("生成 accelerate config 失败") + return None + + acc_config = os.path.join(acc_config_dir, "accelerate_config_0.yaml") + if not os.path.exists(acc_config): + log.error(f"accelerate config 未生成: {acc_config}") + return None + log.info(f"accelerate config: {acc_config}") + + # Step 2: 启动训练 + train_cmd = [ + "accelerate", "launch", + "--main_process_port", "12345", + "--config_file", acc_config, + f"{zk_trainer_dir}/llm/train/trainer_general.py", + "--model_name_or_path", model_path, + "--model_type", model_type, + "--use_auto_class", "true", + "--output_dir", train_output, + "--dataset_type", "zk_sft", + "--train_data_dir", os.path.join(data_dir, "train"), + "--valid_data_dir", os.path.join(data_dir, "validation"), + "--do_eval", + "--fp16_full_eval", + "--per_device_eval_batch_size", "1", + "--include_for_metrics", "inputs", + "--batch_eval_metrics", + "--concat_samples", "False", + "--mix_at_eval", "False", + "--num_train_epochs", str(epochs), + "--learning_rate", str(lr), + "--gradient_accumulation_steps", str(grad_accum), + "--ignore_data_skip", "False", + "--resume_train_if_ckpt_exists", "false", + "--save_strategy", "no", + "--logging_steps", "5", + "--save_total_limit", "0", + "--max_seq_length", str(max_seq_length), + "--adam_beta1", "0.9", + "--adam_beta2", "0.95", + "--adam_epsilon", "1e-9", + "--warmup_ratio", "0.1", + "--lr_scheduler_type", "cosine", + "--use_shuffle", "true", + "--batch_eval_metrics", + "--run_name", run_name, + "--report_to", "none", + "--bf16", + ] + + log.info("=" * 50) + log.info("启动 SFT 训练") + log.info(f" 模型: {model_path}") + log.info(f" 数据: {data_dir}") + log.info(f" 输出: {train_output}") + log.info(f" GPU: {num_processes}") + log.info(f" epochs: {epochs}, lr: {lr}, max_seq: {max_seq_length}") + + result = subprocess.run(train_cmd, env=env, cwd=zk_trainer_dir) + + if result.returncode != 0: + log.error(f"训练失败,返回码: {result.returncode}") + return None + + log.info(f"训练完成,输出: {train_output}") + return train_output + + +# ────────────────────────────────────────────── +# CLI +# ────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser(description="ai-planning SFT 数据组装 & 训练") + subparsers = parser.add_subparsers(dest="command") + + # prepare + p_prepare = subparsers.add_parser("prepare", help="仅组装数据") + p_prepare.add_argument("--output_dir", required=True, help="数据输出目录") + p_prepare.add_argument("--regen_from_csv", action="store_true", help="从 CSV 重新生成 JSONL") + p_prepare.add_argument("--dataset_name", default="zk_sft_new_structure") + + # train + p_train = subparsers.add_parser("train", help="启动训练(需先 prepare)") + p_train.add_argument("--data_dir", required=True, help="已组装的数据目录(prepare 的 output_dir)") + p_train.add_argument("--model_path", required=True, help="基模路径") + p_train.add_argument("--model_type", default="qwen3", help="模型类型") + p_train.add_argument("--train_output", required=True, help="训练输出目录") + p_train.add_argument("--epochs", type=int, default=3) + p_train.add_argument("--lr", type=float, default=1e-5) + p_train.add_argument("--max_seq_length", type=int, default=1024) + + args = parser.parse_args() + + if args.command == "prepare": + prepare_data(args.output_dir, regen=args.regen_from_csv, dataset_name=args.dataset_name) + + elif args.command == "train": + run_sft_training( + data_dir=args.data_dir, + model_path=args.model_path, + model_type=args.model_type, + train_output=args.train_output, + epochs=args.epochs, + lr=args.lr, + max_seq_length=args.max_seq_length, + ) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/skills/model-iteration/scripts/sft_train_job.yaml.tpl b/skills/model-iteration/scripts/sft_train_job.yaml.tpl new file mode 100644 index 0000000..6b926f6 --- /dev/null +++ b/skills/model-iteration/scripts/sft_train_job.yaml.tpl @@ -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 可能是 juicefs,mmap 易 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 diff --git a/skills/model-iteration/scripts/submit_sft_via_cml.sh b/skills/model-iteration/scripts/submit_sft_via_cml.sh new file mode 100644 index 0000000..154b464 --- /dev/null +++ b/skills/model-iteration/scripts/submit_sft_via_cml.sh @@ -0,0 +1,104 @@ +#!/bin/bash +# 用 cml custom_train submit 提交 SFT 训练,训练完成后自动起 cml workflow run 评测 +# 用法: ./submit_sft_via_cml.sh +# 例: ./submit_sft_via_cml.sh 17756 + +set -euo pipefail + +RUNDIC=${1:?usage: $0 } +PREV_RUNDIC=${2:-$((RUNDIC-1))} +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${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 模板 +sed \ + -e "s|{RUNDIC}|${RUNDIC}|g" \ + -e "s|{PREV_RUNDIC}|${PREV_RUNDIC}|g" \ + -e "s|{AUTORESEARCH_ROOT}|${ROOT}|g" \ + "$TPL" > "$YAML" +echo "[cml-sft] yaml: $YAML" + +# 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:等训练成功 → 自动起评测 +WATCHER_LOG=/tmp/r${RUNDIC}_logs/cml_watcher.log +mkdir -p "$(dirname "$WATCHER_LOG")" + +nohup bash -c ' +JOB_ID='"$JOB_ID"' +RUNDIC='"$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 +} + +# 起评测 +echo "[$(date)] 启动 CML 评测 runDic=$RUNDIC" >> "$LOG" +cml workflow run \ + --workflow_id $EVAL_WORKFLOW_ID --version $EVAL_VERSION \ + --global_inputs runDic=$RUNDIC \ + --global_inputs model_path_new=$MODEL_NEW \ + --global_inputs model_path_old=$MODEL_OLD >> "$LOG" 2>&1 +echo "[$(date)] cml workflow run 提交完成" >> "$LOG" +' > /tmp/r${RUNDIC}_logs/watcher_runner.log 2>&1 & + +WATCHER_PID=$! +echo "[cml-sft] watcher PID: $WATCHER_PID(log: $WATCHER_LOG)" + +# 4. 立即输出可查看命令 +cat <