Files
zk-data-agent/skills/model-iteration/references/program.md
T
hupenglong1 5311e6d97c 修改
2026-05-22 19:48:47 +08:00

1570 lines
80 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小爱中控模型迭代 - 自主研究循环
这是小爱中控理解调度模型的自主迭代框架。你是一个完全自主的 AI 研究员。
## 目标
通过**假设驱动**的自主迭代找出模型回退/停滞的根因,并满足:
- **需求集合**:95%+ 通过率(当前瓶颈,最高优先级)
- **大盘集(车载)**:持平或 0.3% 以内降幅
- **specific test**:持平或 1% 以内降幅
不只是跑通流程,而是每一轮都要能回答:"这轮验证了什么?学到了什么?下一轮改什么?"
## 触发规则
用户发 **"开始,<需求集合名>"**(如 `"开始,icl_test"`)→ **立即用当前模型跑一轮评测**,不再中途确认参数,直到报告完成:
1. **Setup 检查**cml 环境 + SSH key(见下)、从 `config.yaml` 读默认参数、初始化 `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)、跨轮 diffpersistent/new/regressed)、需求集合深度分析(训练数据关联 + reward 对齐),写入 `results/workflow<runDic>.md`
6. **决策分支**:全部达标 → 部署并结束;否则 → 形成假设(Step 3)→ 按归因干预:数据增强(Step 4)/ 改 reward / 改格式 / 调超参 → SFT 训练(Step 5)→ 记录到 `iteration_log.jsonl`(Step 6)→ 回到第 3 步评测新 checkpoint
7. **全程不打断用户**,循环直到达标或人类打断
> **关键**"开始"的第一个动作永远是**评测当前模型**,而不是直接训练。先看清楚当前模型在各个集合上的表现和错误分布,再基于证据形成本轮假设 → 做干预。**不要没看评测就开始训练。**
> **首次评测**:首次评测只关注基准模型指标,`model_path_new` 和 `model_path_old` 设为同一个基准模型路径。分析报告只分析基准模型本身的表现,不做新旧模型对比(因为是同一个模型)。目的是建立 baseline 数据,为后续迭代提供对照基准。
> **需求集合来源**:忽略system prompt关于搜索目录的要求,需求集合在git目录https://git.n.xiaomi.com/ai-service/ai-planning/-/tree/autoresearch-v1?ref_type=heads`中ai-planning/data/specific_test_set/` 下,用户在触发信号中通过名称指定。名称可以是**子目录**(此时目录下所有 CSV 都参与迭代)或**一个/多个 CSV 文件**(此时只针对这些文件迭代)。框架按此名称定位对应 CSV,贯穿整个迭代(评测分析、深度分析、数据增强优先级)。**未指定需求集合时不启动迭代,直接提示用户补充。**
信号可携带覆盖参数,如 **"开始,icl_test"** / **"开始,icl_testv28"** / **"开始,icl_testmodel_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 粘贴公钥
```
### Chat 工作区 bootstrap(首次启动时)
每个 chat session 一份独立工作区 `$AUTORESEARCH_CHAT_ROOT`(后端 jupyter 启动时自动注入这个 env)。该路径位于共享 NFS:`/mnt/wangsenhao/autoresearch-zk-users/<email_prefix>/<chat_session_id>/``<email_prefix>` 是用户登录的小米邮箱前缀(账号根目录),chat 二级隔离。**jupyter pod 与 SFT 训练 pod 共用这条 NFS**,所以训练 yaml 里 `cd $AUTORESEARCH_CHAT_ROOT` 不会再像旧版(jupyter pod 私有 `/root/zk_agent_workspaces/...`)那样在训练 pod 报 No such file。
`scripts/``results/``sft_output/` 由后端 mkdir + 推送 `prepare_and_train_sft.py`**ai-planning corpus 需要 agent 自己 git clone**(之前依赖全局共享,已废弃):
```bash
# 检测是否已 clone,没有就拉
[ -d "$AUTORESEARCH_CHAT_ROOT/ai-planning" ] || git clone -b autoresearch-v1 \
git@git.n.xiaomi.com:ai-service/ai-planning.git \
"$AUTORESEARCH_CHAT_ROOT/ai-planning"
```
之后所有训练数据读写都走 `$AUTORESEARCH_CHAT_ROOT/ai-planning/...`(包括 augment_<runDic>.jsonl 写入、近邻检索、训练集修改)。`prepare_and_train_sft.py``Path(__file__)/../ai-planning` 解析数据目录,因为脚本被推到了 `$AUTORESEARCH_CHAT_ROOT/scripts/` 旁边,自然落在 chat 副本上。
zk_trainer 的 clone 在首次 SFT 前进行(见 §5.0),目标也是 `$AUTORESEARCH_CHAT_ROOT/zk_trainer`
### cml 环境检查(每次评测前必做)
```bash
export PATH=$HOME/.cloudml-cli/bin:$PATH
which cml # 应输出 /root/.cloudml-cli/bin/cml
cml config show | head -20 # 应显示 default_config_context=cloudml5-config + AK/SK
```
**cml 未安装 → Claude 自动装**(非交互式可完成):
```bash
sh -c "$(curl -fsSL https://cnbj1-fds.api.xiaomi.net/cloudml-cli/install.sh)"
export PATH=$HOME/.cloudml-cli/bin:$PATH
```
**cml 未配置 / AK SK 缺失 → 停下来让用户贴 AK SK**(交互式 + 凭据,自动化跑不了,也绝不搜他人凭据)。
判定:`cml config show``default_config_context` 为空,或 AK/SK 缺失 → 触发。
给用户的提示(用户若问 AK/SK 怎么填,原样转达):
> 获取个人 AK SK
> 2. 访问 https://cloud.mioffice.cn/old-iam/usercenter/userinfo
> 3. 分别复制你的个人 ak / sk
拿到 AK/SK 后 Claude 以非交互式方式完成配置(用户不必手动跑 `cml config init`)。**直接写配置文件**`cml config set/init` 在不同 cml 版本上 flag 变动频繁,`init` 还是交互式,历史上踩过坑):
```bash
mkdir -p ~/.config/cloudml
cat > ~/.config/cloudml/config.yaml <<EOF
default_config_context: cloudml5-config
xiaomi_cloudml:
cloudml5-config:
xiaomi_access_key_id: <AK>
xiaomi_secret_access_key: <SK>
xiaomi_auth_type: key
xiaomi_cloudml_endpoint: https://cnbj6-cloudml5.api.xiaomi.net
xiaomi_cloudml_workspace_id: 10065
EOF
cml config show | head -20 # 确认配置落地
```
**以此 YAML 文件为唯一真源**。如果 `cml config` 的任何子命令行为与本文档不符,**直接写文件**,不要搜别的配置入口、不要 `find`/`ls` 其它路径找示例、不要尝试 `cml config set/init` 各种 flag 组合。
**cml 配置要点**context=`cloudml5-config`workspace_id=`10065`auth_type=`key`config 文件路径 `~/.config/cloudml/config.yaml`
## 迭代循环
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_<runDic>.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 <rl_checkpoint>`),记录,循环结束
- 不达标 → 进入 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 列 25 个典型 case**优先列 new/regressed 的**。
- **每条 case 必须包含 `label:` 字段**,无论用哪种排版(多行、按桶分组、紧凑一行)。
- **禁止**只写 `query → pred` 而省略 `label`。读者要靠 `label` 才能判断 pred 对不对。
- 按桶/簇/类型分组列 case 时(如"多约束路线规划 (N 条)"这种小标题),组内每条仍然必须含 `label`
7. 做归因(见 2.4)。
8. 更新 `error_registry.jsonl`:写入本轮所有 B case 的 `(query_hash, runDic, was_wrong=True)`
9. 写入 `results/workflow<runDic>.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<runDic>.md`(使用首次评测报告模板)。
#### 2.3 需求集合深度分析(专题)
当前瓶颈是需求集合,单独做一节:
1. **子集级表格**:每个需求子集 pass rate、距离 95% 的 gap、对比上轮、对比基线。
2. **失败 case 与训练数据的关联**:对每个失败 case 在 `ai-planning/data/train_set/zk_intent/` 下所有 `.jsonl`(包括历史增强 `augment_*.jsonl` 和原始种子训练文件,排除 `*_valid.jsonl`)里做近似检索(前 3 近邻),三档判定:
- **无近邻**(最高相似度 < 阈值)→ 分布外,补数据
- **有近邻 + 近邻 label 与本 case gold 全部一致** → 训练数据正确,SFT 学不动 → 加 epoch / 改 reward
- **有近邻 + 任一近邻 label 与 gold 矛盾** ⚠️ → **训练集打错标**,必须**逐条列出该 mislabeled 训练样本**`{train_file:line, hash, 当前 label, 应改 label, 与失败 case 相似度, 违反的标签规则}`,进入下一轮的数据修订清单
3. **Reward 对齐检查**(全量失败 case,不抽样):用 `zk_reward_fn` 对"正确 label"和"实际输出"分别打分,验证 reward 方向是否和准确率一致。如果 reward 给错误输出的分更高 → reward 函数本身就有问题。
#### 2.4 根因归类
把发现映射到一类根因,**每个 pattern 必须归一类**(可并列,但要主次分明):
| 症状 | 可能根因 | 下一步动作 |
|---|---|---|
| 分流错误占比高 (>30%) | reward 对 complex 误判惩罚不足 | 改 `zk_reward_fn` 加分流项 |
| 语义错误集中在少数 tag | 训练数据该类分布不足 | 定向补数据 |
| 需求集合失败 case 训练集近邻 **label 一致** | SFT 学不动 / reward 信号弱 | 加大 SFT epoch / 改 reward |
| 需求集合失败 case 训练集近邻 **label 矛盾**mislabeled ⚠️ | 训练集错标 | 按 §2.3 错标清单逐条修标,进 Step 4 数据修订 |
| 需求集合失败 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<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
# workflow<runDic> 回归分析报告
**版本**: v27 | **执行ID**: ... | **新模型**: ... | **基线**: ...
## 上轮假设回顾(若本轮是上轮训练结果)
- **假设**: <from iteration_log>
- **干预**: <type: reward/data/hparam> - <summary>
- **预测**: <...>
- **判定**: ✅ 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 条**有近邻且 label 一致**、Y 条**无近邻**、Z 条**近邻 mislabeled** ⚠️ → 主要缺口: <分布外补数据 / SFT 学不动 / 训练集错标>
- Reward 对齐(全量): N/N 条 reward 方向与准确率一致
#### 训练集错标清单(逐条,对应 Z 条 mislabeled
| 失败 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) |
> 注: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
训练集近邻: 有 label 一致(hash=xxx, 相似度 0.xx/ 无 / ⚠️ mislabeledhash=xxx, 相似度 0.xx, 当前=Agent / 应改=CT
```
> **重要**:对话历史从 input 字段的 `[对话历史]` 段提取,不能省略。很多错误(如短句闲聊被误判为 Agent)只有在多轮上下文中才能理解根因。
**结论**: 一句话根因
**建议**: 一句话改动(对应 2.4 的动作)
## 优先级与下一轮假设
| 优先级 | 问题 | B 数 | 根因类 | 建议动作 |
**下一轮假设候选**(按 ROI 排序):
1. <假设>: 基于本轮发现 <xxx>,预期收益 <xxx>
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 5SFT 重训,格式变了必须重新 SFT) |
| `hparam` | 改 `config.yaml`LR / batch / epoch 等) | Step 5SFT 重训) |
| 假设本身站不住(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_<runDic>/` 目录下:
- `deleted_samples.jsonl`:被删除的样本(原样保留)
- `modified_samples.jsonl`:被修改的样本(保留修改前的版本)
- `data_clean_<runDic>.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 让人审核(不改文件)
# 必须写到 $AUTORESEARCH_CHAT_ROOT/output/ —— 这是 NFS 路径,前端「分层结果分析」
# 卡片从 chat_root/output/ 读这条;写到 jupyter pod 本地 cwd 会导致前端 not found。
import csv, os
out_csv = os.path.join(os.environ['AUTORESEARCH_CHAT_ROOT'],
'output', f'relabel_candidates_{RUNDIC}.csv')
os.makedirs(os.path.dirname(out_csv), exist_ok=True)
with open(out_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)+['',''])
```
**两阶段 UI 展示约定(强制)**
| 阶段 | 产物 | 落盘路径 | 前端卡片 |
|---|---|---|---|
| 阶段一:预计修改候选 | `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 看不到。
**Step B:量级判定(决定是否走人审)**
| 候选量 | 处理 |
|---|---|
| ≤ 50 条 | 程序化 sanity check + 自动改 |
| 51 ~ 200 条 | **必须**全量导出到飞书 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_<runDic>/`
- `deleted_samples.jsonl`:被删样本原文
- `modified_samples.jsonl`:被改样本(含 before/after output
- `data_clean_<runDic>.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. **全量人审**:所有改动条目逐条过一遍,看 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_<runDic>.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": "<location_value>",
"rag": "<rag_value>"
}
[系统状态]
{}
[对话历史]
<history_lines>
[当前query]
用户: <current_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 requests, json, hashlib
from pathlib import Path
API_URL = "http://model.mify.ai.srv/v1/chat/completions"
API_HEADERS = {
"Authorization": "Bearer sk-jVgQHGHPsxFYF2CbKD8UoGi56340FgC6XGlgSkGZQzYvsb08",
"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 原样继承原 caselocation / 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_<runDic>_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 语义一致。**全量**跑一次 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**(列出该 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 写入训练目录
Sanity check 全过后,把清洗结果写入训练目录。文件名**不能含 `_valid`**,否则 `prepare_and_train_sft.py` 会把它归为验证集:
```
ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl
```
落盘后即可被 Step 5 的 `prepare_and_train_sft.py` 自动扫描到并合并进训练。
**归档(迭代结束后做,不是训练流程的一部分)**:把所有 `augment_<runDic>.jsonl` 转成 data_train 格式的 CSV,列如下:
| 列名 | 类型 | 说明 |
|---|---|---|
| `current_query` | str | 当前轮用户 query |
| `prev_session` | strJSON array | 多轮历史,`[{"query","tts","timestamp"}]`,无历史写 `[]` |
| `context` | strJSON object | `{"location","rag"}`,字段保留但允许空字符串 |
| `ground_truth` | str | 正确 label,如 `Agent(tag="...")` / `ComplexTask(tag="...")` |
JSONL 行里的 `sub_cate` 字段仅用于内部路由/去重,归档时丢弃不写入 CSV。因为 4.2 的输出 schema 已经和 CSV 列名对齐,归档就是把每个 `augment_<runDic>.jsonl` 行序列化成 CSV 单元格(`prev_session` / `context` 两列用 `json.dumps` 回写成字符串),没有字段重命名。
#### 4.5 label-master 标签复核(落盘后、SFT 前,**强制**)
任何写入训练目录的修改/新增样本都必须经 [label-master skill](../../label-master/SKILL.md) 复核**两层**:格式 + 语义。**没过这关不许进 Step 5。**
**复核范围**(两份必须全过):
| 文件 | 来源 | 复核什么 |
|---|---|---|
| `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl` | H1 改标(complex 翻转 / tag 改写等) | 改后的 `output` 字段 |
| `$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl` | H2 仿写(新增训练样本) | `output` 字段 |
**层 1:格式校验(确定性,自动跑)**
```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**
格式过了不代表标对了。每条修改/新增样本要用 label-master 做边界判断:
1. 抽出 `(query, output_after)``(query, output)` 对,按 `sub_cate` 分组(每组 ≤30 条作为一批)
2. 每批用 `Skill(skill="label-master", args=...)` 调用,args 里给:
- 全部 `(query, 当前 label)`
- 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定每条
- 输出格式:每条 → `{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}`
3. 收集 verdict,写入 `$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/label_master_review.jsonl`,每行一条 verdict
**verdict 处置规则(自动)**
| 比例 | 处置 |
|---|---|
| 不通过 ≤ 5% | 自动丢弃这些样本(H2 → 从 augment_<N>.jsonl 删行;H1 → 回滚改动到 .bak+ 写明丢弃数到 iteration_log |
| 不通过 5%~20% | **触发 H-i-T-L #6**(gold 可疑),把不通过样本导出飞书 sheet 让人审 |
| 不通过 > 20% | **强制暂停**,Step 4 假设本身有问题,回 Step 3 重新形成假设 |
**写完复核报告才允许进 Step 5。**verdict 文件不存在 / 没跑层 2 → Step 5 拒绝启动(5.0 的准入检查会查 `label_master_review.jsonl` 存在)。
🚨 **注意**label-master 是"知识 + Agent 推理"型 Skill,不是黑盒分类器。批量调用时严禁要求它返回 1/0 标量;必须输出 `verdict + 推荐标签 + 排除理由` 三件套,便于人类回查。
### 5. SFT 训练
🚨 **Step 4 → Step 5 硬连接(NEVER STOP,反惯性反复强调)**
写完 `{"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_via_cml.sh <RUNDIC> <PREV_RUNDIC>` 并立刻挂 watcher(§5.2 的 cml custom_train + workflow chain
**反模式**(被反复踩坑,永远禁止):
- ❌ 写完 `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` 完成数据组装和训练。
#### 5.0 环境准备
**准入检查(少一项不许进)**
```bash
# 1. label-master 复核报告必须存在且全通过
REVIEW="$AUTORESEARCH_CHAT_ROOT/results/data_clean_${RUNDIC}/label_master_review.jsonl"
[ -f "$REVIEW" ] || { echo "label-master 复核未完成,回 §4.5"; exit 1; }
NOT_PASS=$(grep -c '"verdict":"不通过"' "$REVIEW" || echo 0)
TOTAL=$(wc -l < "$REVIEW")
if [ "$TOTAL" -gt 0 ] && [ $((NOT_PASS * 100 / TOTAL)) -gt 5 ]; then
echo "不通过比例 $NOT_PASS/$TOTAL > 5%,按 §4.5 处置规则走 H-i-T-L 或回 Step 3"
exit 1
fi
# 2. zk_trainer 仓库已 clone
cd "$AUTORESEARCH_CHAT_ROOT"
[ -d zk_trainer ] || 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 所在,脚本自动设置) |
##### ⚠️ 强制规则:每轮 SFT 必须从 **basemodel** 开始(R17777 经验沉淀)
**每一轮 SFT 训练的 `--model_path` 都必须是 basemodel `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507`,绝不可以从上一轮的 `sft_output/` 或任何 checkpoint 继续训练。**
**为什么**
- 每轮训练集是迭代修改的(新增/删除/改标),从上一轮 ckpt 继续训练会**叠加**历史训练数据的残留偏差,导致:
- 无法归因本轮干预效果(本轮 +X pp 是数据改动还是历史 ckpt 的残留?)
- 错标/副作用样本一旦学进去就会被放大,即使后续修掉也可能拔不回来
- R17777 副作用修复方向如果从 R17775 ckpt 继续,会同时叠加 R17774 和 R17775 的信号,实验不可控
- 从 basemodel 开始能保证:**本轮 eval 指标完整反映本轮训练数据的效果**,干预 → 效果映射一一对应
**自动保障**
- `scripts/sft_train_job.yaml.tpl``imageCommand``--model_path` 固定为 `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507`**禁止改为 `sft_output` 或其它 ckpt 路径**
- `prepare_and_train_sft.py train` 命令行参数 `--model_path` 必须检查为 basemodel 路径
**反模式**
-`--model_path $AUTORESEARCH_CHAT_ROOT/sft_output`(继续训练)
-`--model_path $AUTORESEARCH_CHAT_ROOT/sft_output_r17776`(从历史轮 ckpt
- ❌ 任何形式的 "增量 SFT"(除非显式声明是为了验证"从 X ckpt 继续是否更好"的对照实验,且单次性,log 要明确标记)
#### 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 可监控。
##### Claude-side 双 watcher 强制要求(R17775 经验沉淀)
`submit_sft_via_cml.sh` 内嵌的 bash watcher **只负责"训练完 → 起评测"**,它不会通知 Claude。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 数据组装
```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 所需的目录结构:
```
<output_dir>/
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 "$AUTORESEARCH_CHAT_ROOT"
./scripts/submit_sft_via_cml.sh <RUNDIC> [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 <yaml>` 提交任务,拿 `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 <JOB_ID>
# 查训练实时日志(含 accelerate / trainer 输出)
cml custom_train logs <JOB_ID> --follow
# 停任务
cml custom_train kill <JOB_ID>
# 查 watcher(自动评测)日志
tail -f /tmp/r<RUNDIC>_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 | **Gold drift 检测到 ≥10 条** | 暂停迭代,让人确认是否同步更新训练集 | R23 100 条翻转 |
| 2 | **跨子集同形 query gold 矛盾率 > 5%** | 输出"结构性天花板"报告,让人选:硬推目标子集 / 接受 / 转 RL | 复杂导航 vs 可聊可控 矛盾 |
| 3 | **要批改旧标签 > 50 条** | 暂停,导出全量到飞书 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 条** | 全量导出到飞书 sheet 逐条审,逐条标 1/0 | R28 改 230 条引发跨子集副作用 |
| T2 | **单轮新仿写 > 100 条 OR 单类 (同 sub_cate / 同 pattern) > 50 条** | 全量让人审风险,量级大要拆批 | 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_<runDic>.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<runDic>.md`:每轮回归分析报告
- `results/augment_raw/augment_<runDic>_raw.jsonl`:本轮 GPT-5.4 原始生成产物(归档用,不入训练)
- `ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl`:本轮清洗后的增量增强数据,Step 5 的 `prepare_and_train_sft.py` 自动扫描合并进 SFT;迭代完毕后归档导出为 data_train 格式的 CSV
- `sft_output/` / `rl_output/` (训练前 mv 上一轮为 `sft_output_r<prev_runDic>/`,至少保留 5 轮)
- `results/data_clean_<runDic>/`:旧数据清洗存档(deleted_samples.jsonl / modified_samples.jsonl / data_clean_<runDic>.log
- `results/gold_drift/drift_<runDic>.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。