# 小爱中控模型迭代 - 自主研究循环

这是小爱中控理解调度模型的自主迭代框架。你是一个完全自主的 AI 研究员。

## 目标

通过**假设驱动**的自主迭代找出模型回退/停滞的根因，并满足：
- **需求集合**：95%+ 通过率（当前瓶颈，最高优先级）
- **大盘集（车载）**：持平或 0.3% 以内降幅
- **specific test**：持平或 1% 以内降幅

不只是跑通流程，而是每一轮都要能回答："这轮验证了什么？学到了什么？下一轮改什么？"

## 触发规则

用户发 **"开始，<需求集合名>"**（如 `"开始，icl_test"`）→ **立即用当前模型跑一轮评测**，不再中途确认参数，直到报告完成：

1. **Setup 检查**：cml 环境（缺失则自动安装，见下）+ SSH key + **git clone ai-planning 仓库**（见「Chat 工作区 bootstrap」）+ 从 `config.yaml` 读默认参数、初始化 `error_registry.jsonl`
2. **准备评测参数**：确定 `model_path_new`（当前模型）/ `model_path_old`（基线），runDic 一律由 `eval "$(./scripts/resolve_run_ids.sh)"` 解析出来——**不要自己 `ls workflow5 \| tail -1` 心算 +1**；首次评测两者设为同一基准模型
3. **CML 评测**（Step 0）：执行 `cml workflow run`，后台轮询 `metric_diff/lark_template.json` 直到结果就绪
4. **分层结果分析 + 问题分析 & 报告**（Step 1）：按优先级逐层检查 需求集合（≥95%）→ 大盘车载（降幅≤0.3%）→ specific test（降幅≤1%）；做根因归类（reward/data/格式/hparam）、跨轮 diff（persistent/new/regressed）、需求集合深度分析（训练数据关联 + reward 对齐）、SFT 天花板诊断，写入 `results/workflow<runDic>.md`
5. **决策分支**：全部达标 → 部署并结束；否则 → 形成假设（Step 3）→ 按归因干预：数据增强（Step 4）/ 改 reward / 改格式 / 调超参 → SFT 训练（Step 5）→ 记录到 `iteration_log.jsonl`（Step 6）→ 回到第 3 步评测新 checkpoint
6. **全程不打断用户**，循环直到达标或人类打断

> **关键**："开始"的第一个动作永远是**评测当前模型**，而不是直接训练。先看清楚当前模型在各个集合上的表现和错误分布，再基于证据形成本轮假设 → 做干预。**不要没看评测就开始训练。**

> **首次评测**：首次评测只关注基准模型指标，`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，贯穿整个迭代（评测分析、深度分析、数据增强优先级）。**未指定需求集合时不启动迭代，直接提示用户补充。**
>
> ⛔ **禁止因"本地找不到评测集文件"而停下来问用户路径**。评测集在 git 仓库里，找不到说明还没 clone——立即执行 bootstrap 的 `git clone -b autoresearch-v1` 拉取仓库，然后在 `$AUTORESEARCH_CHAT_ROOT/ai-planning/data/specific_test_set/` 下定位。同理，`cml` 命令不存在时按下方「cml 环境检查」自动安装，**不要停下来问用户**。只有 AK/SK 缺失（凭据类）才允许暂停询问。

信号可携带覆盖参数，如 **"开始，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 粘贴公钥
```

### Chat 工作区 bootstrap（首次启动时）

每个 chat session 一份独立工作区 `$AUTORESEARCH_CHAT_ROOT`（后端 jupyter 启动时自动注入这个 env）。该路径位于共享 NFS：`/mnt/<email_prefix>/autoresearch-zk-users/<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` 在脚本顶部统一以 `CHAT_ROOT = Path(__file__).resolve().parent.parent` 解析 chat 工作区（脚本本身位于 `$AUTORESEARCH_CHAT_ROOT/scripts/`），`AI_PLANNING` 和 `zk_trainer_dir` 都基于此推导，**不要在 agent 侧手动 patch `Path(__file__)`**——以前需要改两处的坑已在脚本侧统一。

zk_trainer 的 clone 在首次 SFT 前进行（见 §5.0），目标也是 `$AUTORESEARCH_CHAT_ROOT/zk_trainer`。

### 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%（次要）

⚠️ **优先级铁律**：目标集上 95% 是主线任务。只要目标集还没到 95%，其他集合的轻微下降（大盘 ≤1%、specific ≤2%）**不构成回滚理由**，应继续迭代优化目标集。只有目标集已达 95% 后，才需要关注并修复其他集合的 regression。

**决策**：
- 全部达标 → 部署（`python modules/cloudml_deploy.py <rl_checkpoint>`），记录，循环结束
- 目标集未达标 → 继续迭代（即使其他集合有轻微下降也不回滚）
- 目标集已达标但其他集合超限 → 微调修复其他集合
- 分析顺序：**需求集合 → 车载大盘 → specific test**

> **首次评测差异**：由于新旧模型相同，"降幅"和"vs 基线"无意义（均为 0）。此轮只记录各集合的**绝对准确率**作为 baseline，不做达标/不达标判定，不触发部署。直接在 Step 1 内分析基准模型的错误分布，为后续迭代建立对照基准。

### 1.A 问题分析 & 报告（在 Step 1 同一 step 内继续做，不再单独开 step）

目标不是"列错误"，是回答三个问题：
- **根因属于哪一类？**（reward / data / 格式）
- **上轮干预是否按假设起效？**（若本轮是上轮训练结果，对照上轮 Step 3 的假设）
- **相比上轮，哪些错误是新引入的？**（跨轮 diff）

> **首次评测差异**：无上轮可对比，跳过"上轮干预回顾"和"跨轮 diff"。此轮只回答：**基准模型的错误分布是什么？各集合/子集的绝对准确率和错误 pattern 是什么？** 所有错误统一标记为 `baseline`（既不是 persistent 也不是 new），写入 `error_registry.jsonl` 作为后续跨轮追踪的起点。

#### 2.1 术语与字段

**GSB**：G=旧错新对，S=旧新同，B=旧对新错（本步骤只关注 B 和 G）。

| 术语 | 定义 |
| --- | --- |
| 旧对新错率 | B 数 / 总数 × 100% |
| 相对基线变化 | 新模型准确率 − 基线准确率（百分点） |
| 准确率 | 该子集准确率（`is_model_correct_dev` 为 True 的比例） |
| 分流错误 | base 模型和 dev 模型对该 case 的「是否复杂」二值判断不一致（一边判复杂、一边判不复杂），cleaned tag 是否相同不影响判定。具体怎么从两列原始输出里抽出「是否复杂」这个判断，由 agent 分析前先 head 当前评测产出反推，**不要照抄历史固定字符串** |
| 语义错误 | `origin_predict_base != origin_predict_dev`，意图/tag 本身判错 |
| 持久错误 | 该 case 在上一轮也错（查 `error_registry.jsonl`） |
| 新引入错误 | 该 case 在上一轮对，本轮错 —— **最危险的信号，说明上轮干预有副作用** |

一个 case 可同时属于分流错误和语义错误。

| 字段 | 含义 |
| --- | --- |
| `origin_predict_base` | 旧模型（baseline）原始输出。具体格式（是否含 `complex=` 前缀、tag 包装、自定义 class 名等）以当前评测产出为准，**分析前 head 一下实物** |
| `origin_predict_dev`  | 新模型（迭代模型）原始输出。同上，格式以当前产出为准 |
| `is_model_correct_dev` | 新模型预测是否正确（True/False） |
| `is_model_correct_base` | 旧模型预测是否正确（True/False） |
| `complex_dev` | 迭代模型的 complex 标签 |
| `complex_base` | baseline 模型的 complex 标签 |

| `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 的**。
   - **每条 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 矛盾** ⚠️ → **训练集打错标**，必须**落盘到 `$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_<runDic>.csv`**，列固定为 `file,line,query,old_label,是否改(1/0),建议新label`（注意：CSV 落 `output/`，**不是** `results/`；前端「分层结果分析」卡片就读这一份）。**禁止**只把 mislabeled 列表打印到 stdout 或仅写进 workflow.md——卡片读不到 CSV 等同于本步漏交。本轮无 mislabel 也要写一份只含表头的空 CSV，明示"已检查、无候选"

   ⚠️ **判别器自检（写完任何"把 raw output 抽成可比对值"的函数后、跑全量前必做，跳过=本步无效）**：

   适用范围：凡是从原始模型输出里抽出某个判定值供后续比对/统计的函数都算判别器，无论抽的是什么——
   - 二值（如 complex true/false、是否为 Agent）
   - 字符串/枚举（如 tag 名、function 名、code label）
   - 结构化字段（如某个 slot 的值）

   1. **采端点**（覆盖**所有要区分的类**，每类训练侧 ≥1 条 + eval 侧 ≥1 条；二分类→4 条，N 分类→≥2N 条）：
      - 训练侧：`head` 几条 `train_set/zk_intent/*.jsonl`，靠 cleaned label / 现成 gold 字段挑出每类各 ≥1 条**已知真值**的样本。
      - eval 侧：`head` 几条评测 CSV 的 `origin_predict_base` / `origin_predict_dev` 列实物，肉眼判一下属于哪类，每类各 ≥1 条。
      - **禁止**只 head 一条就开写代码——一条样本不能验证判别器对所有类都能分对。
   2. **跑判别器在端点上**（用真实抓到的字符串当输入，**不要**用想象出来的格式）：
      ```
      判别器(已知 X 类的训练样本 output)   == "X"     # 对每个类都跑一遍
      判别器(已知 X 类的 eval baseline 输出) == "X"   # 训练侧+eval 侧两侧都过
      ```
      任一不对 → 判别器没对齐当前数据格式（最常见死法：把历史文档/旧版本里的 class 名/字符串当成 hardcoded substring 写进了 `'XXX' in out`，结果当前数据里根本没出现过这个字面量），**禁止跑全量**，回去重写。
   3. **最终报告强制并列原文**：每条候选必须同时打印**原始 output 字符串前 80 字符**和**判别器返回值**，读者肉眼可对一遍——光报"近邻一致 N 条 / mislabel M 条"不够。
   4. **零计数兜底**：mislabel 跑出 = 0 时**禁止**直接结论"训练集没问题"，必须先回去验步骤 2 的端点是否全过——zero-count 默认是判别器 bug 信号、不是结论信号。

   ⚠️ **近邻检索性能规则**（>1 万行训练集时强制）：
   - **禁止 O(M·N) 全量比对**——必须两段式：① 倒排索引粗筛（query 2-gram → posting list，每错例得 50-300 候选）② 仅对候选精排（Jaccard/char-overlap 取 top-3）
   - **训练集 + 倒排索引 pickle 缓存到 `/tmp/`**（key 用文件 mtime 自动失效），禁止每个脚本重建
   - **stdout 只 print 聚合**（三档分布 + 每档 top-3 范例，目标 <2KB），明细落 `/tmp/<step>_<runDic>_detail.json`
   - **>10MB 文件先 bash 切片**（`awk/grep/head` 取子集），禁止 `pd.read_csv` 整体加载

   ⚠️ **执行通道**：所有 python 一律走 `bash` + `/tmp/xxx.py` + `python3 -u … | tee /tmp/xxx.log`，禁用 `python_exec`。`python_exec` stdout 全缓冲、turn 结束才 flush，超时被 kill 时 buffer 直接丢光（实测 600s/1800s 撞墙后 `tool_result` 没有任何 stdout 字段）。bash + tee 模式实时落盘，超时也能 `tail /tmp/xxx.log` 看到死在哪个 phase。
3. **Reward 对齐检查**（全量失败 case，不抽样）：用 `zk_reward_fn` 对"正确 label"和"实际输出"分别打分，验证 reward 方向是否和准确率一致。如果 reward 给错误输出的分更高 → reward 函数本身就有问题。
#### 2.4 根因归类

把发现映射到一类根因，**每个 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 报告模板

写入 `results/workflow<runDic>.md`。首次评测（new=old）省略"上轮回顾"和"跨轮追踪"，只记绝对指标。

```markdown
# workflow<runDic> 回归分析报告

**版本**: v28 | **执行ID**: ... | **新模型**: ... | **基线**: ...

## 上轮假设回顾（首次评测省略）
- **假设/干预/预测/判定**: ✅ hit / ❌ miss / 🟡 partial

## 总览
- **需求集合 (<名称>)**: xxx%（vs 基线 ±x.xx%）⭐
- **大盘（车载）**: xxx%（vs 基线 ±x.xx%）
- **Specific test**: xxx%（vs 基线 ±x.xx%）

## 跨轮追踪（首次评测省略）
| 指标 | 本轮 | 上轮 | 基线 |

## 需求集合深度分析
### 子集表现
| 子集 | 基线准确率 | 新模型准确率 | 旧对新错 | 旧错新对 | 总数 | 分流错误 | 语义错误 | 距95% gap |

### 失败 case 与训练数据关联
- <子集>: N 条失败中 X 条有近邻 label 一致、Y 条无近邻、Z 条近邻 mislabeled ⚠️
- Reward 对齐（全量）: N/N 条方向一致

#### 训练集错标清单（Z>0 时必写）
| 失败 case query | 训练样本 file:line | hash | 当前 label | 应改 label | 相似度 | 违反规则 |

## 设备维度（车载）
| 设备 | 基线准确率 | 新模型准确率 | 变化 | 旧对新错 |

> B/G 数来自 CSV 纯模型 GSB（不含 complex 门禁），准确率来自 lark_template（含 complex 门禁），口径不同。

## Specific Test 旧对新错 Top 子集（不含需求集合）
| 子集 | 旧对新错 | 总数 | 率 | 旧错新对 | 相对基线 |

## 问题模式分析（先需求集合，再其他 Specific Test）

### N. <问题标题>（P0/P1/P2）
**根因归类**: reward / data / 格式
**涉及集合**: `xxx.csv`（B=x/xxx=x.xx%）
**跨轮**: 持久 x / 新引入 x ⚠️ / 修复 x ✅
**典型 case**（必须含对话历史，从 input 的 `[对话历史]` 段提取）:
```
对话历史: ...（多轮列出，无则写"无"）
query: ... | label: ... | 旧: ... | 新: ... | 归类: 分流/语义
训练集近邻: 有/无/⚠️ mislabeled（hash, 相似度, 当前→应改）
```
**结论+建议**: 一句话

## 优先级与下一轮假设
| 优先级 | 问题 | B 数 | 根因类 | 建议动作 |
```

#### 2.7 参考工具函数

```python
import re, ast, json, hashlib
from pathlib import Path
from collections import defaultdict

def extract_query(text: str) -> str:
    parts = text.rsplit('用户: ', 1)
    return parts[1].split('[function]', 1)[0].strip() if len(parts) >= 2 else (
        re.search(r'query:(.+?)(?:\n|context:|function:)', text) or type('',(),{'group':lambda s,n:'<未提取到>'})()
    ).group(1).strip()

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 case_hash(query: str, label: str) -> str:
    return hashlib.md5(f'{query}|||{label}'.encode()).hexdigest()[:16]

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'
    return 'regressed' if history else 'new'
```

> `classify(row)` 无法通用——agent 需先 `head` 当前 `origin_predict_*` 实物，按真实格式现写 parser 抽出 complex 二值。

### 3. 本轮假设（基于 Step 1 的证据）

**只有证据充分时才往下走。** 在开始训练前，写入 `results/iteration_log.jsonl` 的 `hypothesis` 字段：
- **假设**：本轮要验证什么？（例："分流错误主因是 reward 对 complex 误判的惩罚太弱"）
- **干预**：具体改了什么？类型限一类：`reward` / `data` / `hparam` / `格式`
- **预测**：期望指标如何变化？（例："需求集合车载子集 ≥ 93%，分流错误率 < 15%"）

没有假设就开始训练 = 随机游走。**如果本轮纯粹是跑一次稳定性复现，也要显式写 "replication"。**

> 若 Step 1 显示全部达标 → 跳过 Step 3–5，直接部署。

> ⛔ **CRITICAL: hypothesis=complete 后禁止空手结束 turn**。写完 hypothesis 到 program-state.jsonl 之后，当前 turn 内必须做下面两件之一（不能都不做就结束）：
> - 命中 HiTL → 写 `human-check` gate entry (running) → chat reply 给用户选项
> - 不命中 HiTL → 写 `augment=running` → 开始 Step 4
>
> **"turn 预算不够"不是合法理由**——没有自动 wake 机制，turn 结束 = session 永久停止。已踩坑：agent 在 R0 hypothesis=complete 后误判"turn 用满"而空手结束，用户看到 pipeline 卡在 hypothesis 不动、无任何后续（session `qf1ni1jh`，25/50 turns 实际只用一半）。

### 4. 数据生成（GPT-5.4 从 badcase 增强）

**只在 §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 1 证据不支持 Step 3 的假设） | 不做任何训练 | **回 Step 3**，重写假设 |

换句话说：Step 4 是"data 归因专属"的干预入口，其他归因各有各的干预点，往下找对应的 Step 就行。

#### 4.0 原始训练数据清洗（增强前必做）

> 🚨 **强制执行：每轮 augment 必须先跑 §4.0→§4.0.1，不得跳过直接做 H2 仿写。**
> 即使 hypothesis 归因为"训练集缺数据"，也必须先逐 pattern 检索训练集确认是否存在 mislabel。只有当 §4.0.1 Step A 扫描结果候选 = 0 时，才能判定"无需 H1 修改"并跳到 H2。**"这轮只需要加数据"不是跳过 H1 检查的合法理由**——上一轮加的新数据可能引入了新的标签冲突，必须每轮重新检索确认。
>
> 缺少 H1 检查的 augment 视为不完整：`modified_samples.jsonl` 可以为空（代表确认无需修改），但 `data_clean_<runDic>.log` 必须记录"H1 扫描完成，0 候选"的结论，否则 §5.0 准入检查拒绝启动 SFT。

数据增强不仅仅是加数据，还必须对原数据集中的错误/不一致标签进行清洗，否则新增数据和旧数据矛盾，模型学不好。

**清洗原则（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)+['',''])
```

> ⛔ **`relabel_candidates` CSV 中的 `file` 列必须指向 `train_set/` 下的训练文件，禁止把测试集（`specific_test_set/`）的行写入候选**。测试集是只读评测参照，修改目标永远是训练集。流程是：测试集错例 → 归类 pattern → 检索训练集同类 query → 训练集中命中的条目才是候选。如果候选 CSV 里出现 `specific_test_set/` 路径，说明流程搞反了，必须重做。

**两阶段产物路径**：
- 阶段一（候选）：`$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_<runDic>.csv` → 前端"分层结果分析"卡
- 阶段二（确认）：`$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl` → 前端"数据增强"卡

> **runDic 规则**：所有产物的 `<runDic>` = `$SFT_RUNDIC`（从 `eval "$(./scripts/resolve_run_ids.sh)"` 取）。**只能在当轮 eval `lark_template.json` 落盘后调用**，禁止手算或提前调用。

~~**Step A.5：label-master 预审**~~ — **已禁用，跳过此步**。候选直接进入 Step B 量级判定。

**Step B：量级判定（基于 Step A 产出的候选量）**

| 候选量 | 处理 |
|---|---|
| ≤ 50 条 | 程序化 sanity check + 按 suspected_new_label 自动改 |
| 51 ~ 200 条 | **必须**全量导出到飞书 sheet 让人逐条审 1/0（不抽样）；导出时带 `query / old_label / suspected_new_label` 三列 |
| > 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`：被改样本，**必须包含以下字段**：
  - `line_idx`：原文件行号
  - `file`：原文件路径
  - `query`：**完整 query**（用 `extract_query(instruction)` 从原始 instruction 提取，**禁止截断**）
  - `old_output`：修改前 output
  - `new_output`：修改后 output
  - 可选：`instruction_query_excerpt`（仅供人工快速浏览，允许截断，但**不得作为 §4.5 复核的 query 来源**）
- `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：修改后验证** — `prepare_and_train_sft.py prepare` 验数量 + 格式校验 4.1.1 + 逐条目视确认

**Step F：删除 vs 修改** — 标签错但 query 有价值→修改；query 噪声重/同 pattern 已充足/跨子集 gold 矛盾→删除

**Step G：.bak 管理** — 每轮 `.before_r{N}.bak` 保留≥5 轮；回退：`cp xxx.before_r{N}.bak xxx`；禁止 bak 文件含 `_valid`/`_train`（会被 prepare 误读）

#### 4.1 输入

- **Badcase 来源**：Step 1 里 `纯模型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 原样继承原 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_<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`），规则从错例自动归纳，不需人定。

**触发**：每轮 Step 1 后，对未被已知规则覆盖的错例做归纳。

**接受条件**（两个同时满足）：
- `c1 ≥ 0.80`：同 pattern 错例内 gold 主流占比（防错例巧合）
- `c2 ≥ 0.85`：候选正则扫全测试集后主流 gold 占比（防过拟合错例）
- 两者推出的主流 gold 必须一致，且不与现有规则重叠 >70%

**归纳流程**：同 pattern 错例 ≥3 条 → 提取公共正则 → 算 c1/c2 → 过阈值则自动追加到 `results/label_rules.md`，下一轮 4.3.1 立即生效，不需人审。

**归纳失败 → HiTL**：
- 信号 #2：同结构 query 在 ≥2 子集 gold 矛盾
- 信号 #6：同 pattern 错例 <3 条但多轮持续出现

#### 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` | 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_<runDic>.jsonl` 行序列化成 CSV 单元格（`prev_session` / `context` 两列用 `json.dumps` 回写成字符串），没有字段重命名。

#### ~~4.5 label-master 标签复核~~ — **已禁用，跳过此步**

仅保留层 1 格式校验（`validate_label_output.py`），不再调用 label-master 做语义复核。augment 完成后直接进入 Step 5 SFT。

### 5. SFT 训练

🚨 **Step 4 → Step 5 硬连接**：`augment=complete` 后同一轮**紧接着**：① 写 `sft=running` ② 层 1 格式校验（`validate_label_output.py`） ③ → `submit_sft.sh` + 挂 watcher。禁止 turn 结束、禁止写简报、禁止等回调。评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起，不串进 SFT bg。

⚠️ **写 `augment=complete` 时必须附带 `"count"` 字段**，值为本轮最终写入 `augment_<runDic>.jsonl` 的样本行数（经 dedup + 格式校验后的实际数）。示例：
```jsonl
{"step":"augment","status":"complete","run_id":"R5","count":49,"ts":"2026-05-26T21:51:32+08:00"}
```
Pipeline panel 用此字段展示增强条数；缺失则只显示文件名。

使用 `prepare_and_train_sft.py` 完成数据组装和训练。

#### 5.0 环境准备

**准入检查（少一项不许进）**：

```bash
# 1. H1 检查日志必须存在（§4.0 强制要求，即使无修改也要记录扫描结论）
CLEAN_LOG="$AUTORESEARCH_CHAT_ROOT/results/data_clean_${RUNDIC}/data_clean_${RUNDIC}.log"
[ -f "$CLEAN_LOG" ] || { echo "data_clean 日志不存在，说明 §4.0 H1 检查未执行，回 §4.0"; exit 1; }

# 2. zk_trainer 仓库已 clone（URL 写死，不要换命名空间，clone 失败先看 ssh key）
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 训练产物备份

```bash
[ -d sft_output ] && mv sft_output sft_output_r${PREV_RUNDIC}
rm -f "$AUTORESEARCH_CHAT_ROOT/sft_output/_SUCCESS"   # 防遗留误报
```

**SFT watcher 流程**（`submit_sft.sh` 提交后）：
1. 写 program-state `sft=running` + watch 声明（target = `sft_output/_SUCCESS`）
2. 用 `bash(run_in_background=true, wait_for_completion=true)` 等 `_SUCCESS` 落盘
3. Watcher fire / bg 返回后**下一轮**：写 `sft=complete` → 写 `cml=running` → 调 `submit_cml_eval.sh` 起评测 + 挂评测 watcher

⚠️ 训练和评测**不串接**——`submit_sft.sh` 只提 SFT，评测在 `_SUCCESS` 后新一轮单独起。

#### 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`。

⚠️ **每轮 SFT 前必须在 log 中展示训练数据组成**

`prepare_and_train_sft.py` 会 glob 目录下**所有** `augment_*.jsonl`。agent 在每轮 SFT 前必须在 program-state log 中列出最终参与训练的文件及行数：

```jsonl
{"log":{"ts":"...","iter":"R3","text":"SFT 数据组成: all_train.jsonl(35379) + augment_17829.jsonl(210) = 35589 条 | 排除: augment_17828.jsonl(150) rename .pre_R3（R2 pattern 导致导航 bvt 劣化）"}}
```

必须展示：(1) 参与训练的所有文件及行数 (2) 被排除的旧轮文件及排除原因（如果有）。

#### 5.2 启动训练（cml custom_train）

```bash
cd "$AUTORESEARCH_CHAT_ROOT"
eval "$(./scripts/resolve_run_ids.sh)"
./scripts/submit_sft.sh "$SFT_RUNDIC"              # 提交 SFT，立刻退出
# _SUCCESS 落盘后下一轮：
./scripts/submit_cml_eval.sh "$EVAL_RUNDIC"        # 起评测
```

**`submit_sft.sh`**：渲染 `sft_train_job.yaml.tpl`（替换 `{RUNDIC}`）→ `cml custom_train submit` → 打印 JobID → 退出。
**`submit_cml_eval.sh`**：从 `config.yaml` 读 workflow_id/version → `cml workflow run` 起评测，runDic=EVAL_RUNDIC。

监控：`cml custom_train describe/logs/kill <JOB_ID>`

### 6. 记录结果

每轮写入 `results/iteration_log.jsonl` 一行，schema：

> 🚨 **`results` 字段必须包含以下固定指标（每轮都要，不可遗漏）**：
> - `specific_test`：Specific Test 准确率
> - `dapan_car`：大盘车载准确率
> - `target_subset`：目标集合准确率（= 当前需求子集）
> - `icl_test`：ICL Test 准确率
>
> 这些指标从 `lark_template.json` 的 metric_diff 中提取。如果某个指标在评测结果中确实不存在（如首次评测缺少某子集），写 `null` 而不是省略 key——前端 metrics 折线图依赖每轮 key 的一致性，缺 key 会导致数据点丢失。

```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": {
    "specific_test": 95.10, "dapan_car": 96.30,
    "target_subset": 92.1, "icl_test": 86.11,
    "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. **全量 case 直接铺进对话**（凡是"该不该改 / 该不该删 / 该不该新增 N 条"型决策都适用，**包括但不限于** H-i-T-L #1/#2/#3/#6 与 T1/T2/T3/T5）：
   - 必须把全部候选按 pattern 分组、每条一行（紧凑表）贴到**同一条** ask 消息体里，不是只给 CSV 路径或飞书链接，**也不要分段连发**——一段全铺，让用户一次滚完
   - **不允许抽样、不允许"前 N 条样例"、不允许"代表 case"**——抽样让用户看不到边界外的长尾，决策无意义
   - 每条至少含：`query / old_label / 推荐标签 / 理由（≤40字）`；H1（改标）类必含 `file:line`，H2（仿写）类必含 `pattern_id`
   - **高置信 + 低置信（潜在影响半径 / 类似 case）都得铺，缺一不可**：当 ask 里出现「确认要改 X 条 + 还有 Y 条类似的 / 潜在影响 Y 条 / 同 pattern 还有 Y 条疑似」这种二段叙述时，**Y 条也必须全量铺进同一条对话消息**（同样按 pattern 分组、紧凑表），并对每条标 `confidence=high / low`。理由：用户的决策本身就是「只改 X」vs「扩到 X+Y」vs「再收窄 pattern」，看不到 Y 就只能瞎选。**只展示高置信 X 条、把 Y 条藏在数字背后**视为违反 skill。
4. **2-4 个选项**（用 AskUserQuestion 工具）：每个选项含预期收益 + 风险
5. **推荐选项**：基于经验给出推荐（标 "(推荐)"）
6. **本地产物**：全量错例 csv / 飞书 sheet 链接（作为对话铺陈的备份和事后回查渠道，**不替代**第 3 项）

### 介入后的恢复

人决策后立即恢复全自动循环，**不再二次确认**当前轮的细节。除非人显式说"再问我"。

### 反模式（不应触发介入）

- ❌ 训练前问"要训吗？" — 应直接训
- ❌ 评测前问"要评吗？" — 应直接评
- ❌ 改 ≤ 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。