---
name: model-iteration
description: 小爱中控模型自主迭代框架（autoresearch-zk）。用于对小爱同学中控理解调度模型进行假设驱动的自主 SFT+评测迭代循环。
when_to_use: |
  仅在用户通过 UI 明确点击使用 model-iteration skill 时触发。不要从自然语言推断意图自动启用。
---

# autoresearch-zk — 小爱中控模型自主迭代框架

## ⛔ 第一步：读 program.md（强制，任何操作之前）

收到"开始"触发信号后，**第一个动作**必须是读取 `references/program.md`。本文件（SKILL.md）只是概要和 UI 约定，**完整操作规范全在 program.md 里**——包括 bootstrap 流程（clone ai-planning 仓库）、cml 自动安装、评测集定位逻辑等关键步骤。

**不读 program.md 就开始工作 = 必然跑偏。** 已踩坑：agent 只看 SKILL.md 就开始环境检查，发现评测集文件不在本地后停下来问用户路径——而 program.md 明确写了"文件在 git 仓库里，先 clone"。

## 🚫 严禁问用户的事（违反任一条 = 违反 skill）

每次问用户都让用户烦。program.md 已经把"什么时候停"写得很死（17 类 HiTL 信号 + 用户主动打断），**这之外一律不准停下来等确认**。下面是被反复踩坑的"擅自暂停"模式，**全部禁止**：

| ❌ 禁止说的话 | ✅ 正确做法 |
|---------|---------|
| "要不要我做训练集近邻检索？" | 这是 §2.3 分析必做项，**直接做**，做完把结果落 dist-analysis 报告（`workflow<runDic>.md`） |
| "下一步可以继续吗？" | 永远不问。看 program.md 流程图自己判断 |
| "我先把控制权交回，等你来问跑完了吗" | watcher 接手 UI 同步，长任务用 `bash(run_in_background=true, wait_for_completion=true)` 提交后本轮主动结束；后端会在产物落盘后自动起新一轮把结果送回，**直接进 Step 1** |
| "这一步是关键决策点，需要你拍板" | program.md 没写就不是。**自主决策 + 落 iteration_log** |
| "我把现状停在这里，把决策摘要给你 review" | 不准。摘要可以写，但不准停 |
| "Step 4 augment 完成 ✅，等回调后续做 SFT" | augment=complete 那一刻就是 SFT 启动那一刻——**同一轮 bash** 紧接着跑层1格式校验 + 提交 SFT，不许写简报、不许等回调（详见 program.md "Step 4 → Step 5 硬连接"）|
| "R1 评测发现 regression，先把诊断给你看，等你拍板再决定要不要回滚" | dist-analysis 发现 regression **也算 dist-analysis 完成**——**同轮 bash** 紧接着写 `results/workflow<runDic>.md`（包含完整 delta 表 + new_fail/new_fix 子集 + 病灶定位 + 回滚建议）+ append `iteration_log.jsonl` R{n} entry。文件落完了再用 chat reply 给人提回滚选项。**不许把诊断只写在 chat 里、不落盘**——前端「分层结果分析」卡片读的是 `results/workflow<runDic>.md`，你不写卡片永远停在上轮。 |
| "SFT 用哪个 basemodel？(a) 用户 eval 给的路径 (b) 默认 Qwen3-4B" | program.md §5.0 写死 `--model_path = /mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507`，**每轮强制 basemodel**——这是规则不是选项。eval 阶段的 `model_path_new` 跟 SFT 的 `--model_path` 是两件事，不要混。直接用 Qwen3-4B-Instruct-2507。 |
| "zk_trainer 仓库 URL 是什么？" | program.md §5.0 line 1323 写死 `git clone git@git.n.xiaomi.com:wangsenhao/zk_trainer.git`，**直接用**。clone 失败先看 ssh key（program.md 「SSH Key 检查」），**不要**自己脑补 `nlp/`、`xiaoai/`、`autoresearch/` 等命名空间问用户。 |
| "格式校验 5 条 pass，按比例外推 100 条 OK" | **抽样外推禁用**——`validate_label_output.py` 必须对全量文件跑，不许抽样 |

**合法暂停只有**：

1. program.md `## Human-in-the-Loop 时机` 章节的 7 类迭代级信号
2. `### 训练集调整专项 H-i-T-L 信号` 的 T1-T10
3. **§4.0.1 Step B 量级判定的 HiTL 分支**：
   - 候选量 51-200 条 → **必须**全量导出到飞书 sheet 让人逐条审 1/0（**这就是一次合法暂停**，不是擅自停；**不再抽样外推**）
   - 候选量 >200 条 → 命中触发条件 #3，强制 HiTL 介入让人定更精细 pattern 收窄（仍然全量交付）
4. 用户主动发消息打断

这之外**所有**"我觉得这事大、我先停"的本能都要压下来。命中合法暂停时，**不要只是说"等你确认"**——按 §4.0.1 把**全量候选清单**（不是抽样）/精细 pattern 的具体输出 dump 到飞书 / scratchpad 给人具体可审的东西，再停。

## 📋 每个 Step 的强制准入条件（少一项不准进下一步）

按 Step 编号，**进入下一 Step 前**必须把当前 Step 的准入清单全部完成并落到 state file / iteration_log，否则视为跳步违规。

### Step 0 准入
- [ ] cml 环境检查通过（`which cml` + `cml config show`）
- [ ] runDic 已分配（扫 `run_history_dir` 取 max+1）
- [ ] CML workflow 已提交（拿到 Execution ID）
- [ ] watcher 声明已写入 program-state.jsonl
- [ ] cml workflow + 等 metric_diff 落盘已用 `bash(run_in_background=true, wait_for_completion=true)` 提交（**不要**在 bash 内 sleep+poll）

### Step 1 准入（`dist-analysis`，含原 Step 2 报告产出）
- [ ] 读 `metric_diff/lark_template.json` 提分层指标（specific / 大盘车载 / 目标专项）
- [ ] 读 `metric_diff/specific_comparison.csv` 提取 baseline 错误分布
- [ ] **§2.3 失败 case 与训练数据的关联**：每条错例在 `train_set/zk_intent/*.jsonl` 做近邻检索（前 3 近邻），输出"有近邻 / 无近邻"分类
- [ ] **§2.3 mislabel 命中必须落盘 CSV**：列固定为 `file,line,query,old_label,是否改(1/0),建议新label`，写到 `$AUTORESEARCH_CHAT_ROOT/output/relabel_candidates_<runDic>.csv`（前端「分层结果分析」卡片就读这一份）。本轮无 mislabel 也写一份只含表头的空 CSV，明示"已检查、无候选"。**禁止**只 print 到 stdout 或只写进 workflow.md——卡片读不到 CSV 等同漏交
- [ ] **§2.3 判别器自检**：写完任何"把 raw output 抽成可比对值"的函数后（complex 二值 / tag / function / code label / slot 等都算）、跑全量前——端点验证（每类训练侧 ≥1 条 + eval 侧 ≥1 条；二分类 4 条、N 分类 ≥2N 条）+ 报告并列原文 + 零计数兜底（详见 program.md §2.3 自检小节）
- [ ] **§2.3 Reward 对齐检查**：全量失败 case 用 `zk_reward_fn` 验 reward 方向（不抽样）
- [ ] §0.1 Gold drift 检查（如未做）
- [ ] §2.4 根因归类表（每个 pattern 必归一类，可并列但要主次）
- [ ] 写入 `results/workflow<runDic>.md`（根因 / 跨轮 diff / 需求集合深度分析 / 天花板诊断）
- [ ] `output/relabel_candidates_<runDic>.csv` 已落盘（无候选则空表头 CSV）
- [ ] error_registry 追加本轮错误

### Step 3 准入（`hypothesis`） — analysis 收尾，不是 train 开头
- [ ] 写本轮假设到 iteration_log.jsonl 的 hypothesis 字段
- [ ] 假设必须有依据（指向 §2.3 / §2.4 的具体发现）
- [ ] **`step:"hypothesis"` entry 写在当前 round（R{n}）名下**——不要写成 R{n+1}。后端把 hypothesis 归类为 analysis 类，是 R{n}·Baseline / R{n}·Analysis 的最后一张卡，不是 R{n+1}·Train 的开头。这样 gate（Human Check / Review）会插在 hypothesis 卡之后、R{n+1}·Train 之前，**用户看到假设内容再拍板是否进 train**。
- [ ] **`hypothesis=complete` 之前，必须 append `iteration_log.jsonl` 一条 R{n} entry**，schema 至少包含 `iteration:n / runDic:<本轮编号> / timestamp / results:{<指标 key>:<float>...} / hypothesis`。**前端 metrics 折线图唯一数据源就是这个文件**，没写 → 图表彻底空白（state file 写得再全也不行）。已踩坑：R0 走完 dist-analysis + hypothesis 但漏了 iteration_log，前端 metrics 卡看不到任何点，指标空了一整轮。`results` 必须用 program.md 「§iteration_log schema」里固定的 metric key（`req_set_car / dapan_car / specific_test / triage_err_rate / bvt_nav / talkable_controllable` 等），key 拼错前端按缺失处理。

### Step 3 → Step 4 / gate 边界（**CRITICAL：hypothesis=complete 后禁止空手结束 turn**）
- [ ] **`hypothesis=complete` 写入 program-state.jsonl 之后，当前 turn 绝对不允许结束**——必须在同一轮里做下面两件之一（二选一，不能都不做）：
  - (A) 如果命中 HiTL 信号（§HiTL 阈值）：写 `human-check` gate entry（running）→ chat reply 给用户选项 → 然后才可以让 turn 自然结束（gate=running 会让 UI 显示 "waiting" 卡片）
  - (B) 如果不命中 HiTL（候选量 ≤ 50 全自动路径）：直接写 `augment=running` → 开始执行增强
- [ ] **"turn 预算不够"不是合法理由**——50 turns 里跑完 hypothesis 最多用 15~20 turns，剩余空间远够写一条 gate entry 或启动 augment。如果你真的接近上限，也必须**先写 gate entry 再结束**，不能空手结束。
- [ ] **"下一轮 wake 继续"不存在**——没有自动 wake 机制。你结束了就是结束了，session 不会被再次调起，用户只看到 pipeline 卡在 hypothesis=complete 不动。

### Step 1 → Step 3 边界（**NEVER STOP 硬连接**，R0 / R1+ regression 都适用）
- [ ] dist-analysis 算完 delta（new_fail / new_fix / persistent / 子集分布）那一刻起，**同一轮 bash 不许结束**：紧接着写 `results/workflow<runDic>.md`（包含完整指标表 + 病灶定位 + 假设 + 回滚/继续建议）→ append `iteration_log.jsonl` R{n} entry（`results` 字段填本轮 metric，`hypothesis` 字段填下一动作）
- [ ] **regression 场景同样适用**：哪怕 R1 出现导航bvt 纯劣化、可聊可控大幅 -25 这种"必须回滚"信号，**先把分析落到 workflow<runDic>.md 和 iteration_log，再用 chat reply 给人回滚选项**。文件先落、聊天再发——顺序不能反。
- [ ] **不许"诊断只写聊天回复 / 等用户拍板再补盘"**：前端「分层结果分析」卡片读的是 `chat_root/results/workflow<runDic>.md`；你不写盘卡片永远显示上一轮，看不到 R1 结论。
- [ ] **runDic 推进**：每轮新评测结果落盘后，append `iteration_log.jsonl` 一条新 entry（runDic 写新轮的值，比如 R0 是 17793，R1 评测的 metric_diff 在 workflow17794，这条 entry 的 runDic 就写 17794），UI 卡片才会切到新一行。
- [ ] **凡是要让用户拍板的检查点（R0 baseline 之后、R{n} regression 之后、>200 候选要定 pattern、Gold drift 复核 等），必须先 append `step:"human-check"` 或 `step:"human-review"` running entry，再用 chat reply 提问**。**禁止只在 chat 里问而不写 gate entry**——UI 看不到拦截 = 视为没做这步，用户面板上看不到任何卡。详见下方 "Human-in-the-Loop 信号 → 写 gate 节点"。

### Step 4 准入（`augment`，需要修改训练集才进）
- [ ] **runDic 必须在当轮 eval `lark_template.json` 落盘后重新 `eval "$(./scripts/resolve_run_ids.sh)"` 取**——禁止复用之前缓存值或手算，eval 期间 resolve 会得到上一轮 workflow id 导致偏移 -1
- [ ] **§4.0 原始训练数据清洗（增强前必做）**——按 4 种动作走完逐 pattern 判定
- [ ] §4.0.1 Step A：候选定位输出 csv，**不直接改**
- [ ] ~~§4.0.1 Step A.5~~ label-master 预审已禁用，跳过
- [ ] §4.0.1 Step B：量级判定基于 Step A 候选量走对应支线（≤50 自动改 / 51-200 全量人审 / >200 触发 #3）
- [ ] §4.0.1 Step C：备份 `.bak` 文件
- [ ] §4.1 输入准备 / §4.2 GPT 调用 / §4.3 sanity check / §4.4 写入
- [ ] ~~§4.5 label-master 标签复核~~ 已禁用，仅保留层 1 格式校验（`validate_label_output.py`）

### Step 4 → Step 5 边界（**NEVER STOP 硬连接**，反复踩坑）
- [ ] 写完 `augment=complete` 那一刻，**同一轮 bash 不许结束**：紧接着跑层 1 格式校验 → 写 `sft=running` → 调 `submit_sft.sh` → 挂 watcher（评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起，不要在 SFT bg 里串接评测）
- [ ] **不许写"Step 4 完成"进度简报后 turn 结束**，不许"等回调后续做 SFT"
- [ ] H2 后台任务回调（`[system] 后台任务 ... exit_code=0`）**不是 turn 结束信号**——它只是 augment 子流程的一个中间节拍，agent 必须在同轮里继续走完 §4.5 → Step 5

### Step 5 准入（`sft`）
- [ ] 层 1 格式校验通过（`validate_label_output.py`）
- [ ] 旧 sft_output 已 `mv sft_output sft_output_r{prev}` 备份
- [ ] 训练参数从 config.yaml 读，model_path = basemodel（**不从上轮 ckpt 续训**）

### Step 6 准入（`log`）
- [ ] iteration_log.jsonl 完整 schema（hypothesis / intervention / prediction / results / verdict / root_cause_findings / error_delta / next_hypothesis）

### 跳步检查

每次写 `{step:"X","status":"running"}` 前，**先看 state file 上一条 step 是不是 complete**。如果上一条还在 running 或者跳过了某个 Step，**回去补**，不要往前走。

## ⚠️ Pipeline UI 同步约定（强制，必读）

**平台用户实时盯着 pipeline UI 看你跑到哪一步**。卡片状态完全靠你写 `program-state.jsonl` 驱动；**你不写、卡片不动；你嘴上说 running、文件里写 failed，UI 就显示 failed**。所以下面这些是**硬性纪律，不是建议**：

### R1（最重要）— 每一次状态切换都必须先 echo 再做事

进入新 step 之前，**第一件事**就是 append 一行 `running`。完成后**最后一件事**是 append 一行 `complete`。失败时也必须 append `failed`。**永远不要"嘴上"宣布状态切换却没落盘**。

错误示范（你不许这样）：
> 我现在开始 step 0，跑 CML 评测... \[执行很多操作\]... 已完成 ✅

正确示范：
> ```bash
> echo '{"step":"cml","status":"running","ts":"'$(date -Iseconds)'"}' >> "$SESSION_OUTPUT/program-state.jsonl"
> ```
> *（然后才执行 step 0 的具体操作）*
> ...
> ```bash
> echo '{"step":"cml","status":"complete","ts":"'$(date -Iseconds)'"}' >> "$SESSION_OUTPUT/program-state.jsonl"
> ```

### R1.5 — 每条 state entry 必须带 run_id（让 UI 准确归位 round）

新版 UI 按 R0 / R1 / R2 切分 sections。每个 R{n>=1} 同时包含 **Train** 和 **Analysis** 两个 section。**必须**每行带 `run_id`。

⚠️ **核心规则：run_id 只在 hypothesis=complete 之后递增。** 一个完整迭代（augment → sft → eval → analysis → hypothesis）全程使用同一个 run_id。

完整示例（一轮完整迭代 R1）：

```bash
# ─── R0·Baseline ───
echo '{"step":"cml","status":"running","run_id":"R0"}' >> $S
echo '{"step":"dist-analysis","status":"complete","run_id":"R0"}' >> $S
echo '{"step":"hypothesis","status":"complete","run_id":"R0"}' >> $S
# ← hypothesis=complete → 递增 run_id

# ─── R1·Train ───
echo '{"step":"augment","status":"running","run_id":"R1"}' >> $S
echo '{"step":"augment","status":"complete","run_id":"R1","count":80}' >> $S
echo '{"step":"sft","status":"running","run_id":"R1"}' >> $S
echo '{"step":"sft","status":"complete","run_id":"R1"}' >> $S

# ─── R1·Analysis（SFT 后的 eval 仍然是 R1，不是 R2！）───
echo '{"step":"cml","status":"running","run_id":"R1"}' >> $S
echo '{"step":"cml","status":"complete","run_id":"R1"}' >> $S
echo '{"step":"dist-analysis","status":"complete","run_id":"R1"}' >> $S
echo '{"step":"hypothesis","status":"complete","run_id":"R1"}' >> $S
# ← hypothesis=complete → 递增 run_id

# ─── R2·Train ───
echo '{"step":"augment","status":"running","run_id":"R2"}' >> $S
# ...
```

**错误（禁止）**：eval 后递增 run_id，导致 train 和 analysis 分属不同 round：
```
R1: augment+sft → R2: cml+analysis+hypothesis → R3: augment+sft → R4: cml+analysis ...
```

**为什么重要**：后端通过 `per_round[R{n}]` 从 R{n}·Analysis 的 watch/log entry 解析 runDic，再用这个 runDic 去找 R{n}·Train 的 augment 产物文件。如果 train 和 analysis 用了不同 run_id，augment 的产物文件找不到。

### R2 — 写入即生效，最后一行覆盖前面

后端按 `step` 字段取**最后一条**状态。如果你之前写过 failed、现在恢复了，**必须 append 一条新的 `running`**，否则 UI 永远停在 failed。

### R3 — Step 之间不允许跳步骤而不留状态

进入 dist-analysis 前必须确认 cml 状态已写到 `complete`（或显式 `failed` 后用户决定继续）。

### R4 — 中长任务用 progress 字段更新

CML 评测、SFT 训练这种几分钟以上的任务，**每次轮询/心跳时附带 progress 字段**写一条新的 `running` 行（旧 running 不需删，最后一行最新）：

```bash
echo '{"step":"cml","status":"running","progress":0.4,"ts":"'$(date -Iseconds)'"}' >> "$SESSION_OUTPUT/program-state.jsonl"
```

UI 卡底部进度条会跟着动；不写就一直显示初始进度。

### R5 — bg 任务作用域 = 单 step（**反复踩坑**）

bg 任务（`bash(run_in_background=true)`）的合法范围是**一个 step 内的"提交一个远端动作 + 等它的产物落盘"**——比如「提 cml workflow run + 等 metric_diff 落盘」、「提 SFT custom_train + 等 `_SUCCESS`」。这是 §「典型用法」推荐的标准结构。

**禁止**把 bg 范围扩到跨 step：

| ❌ 禁止 | ✅ 正确 |
|---|---|
| 一个 bg 串行 `等 SFT _SUCCESS → 提 cml workflow run（评测）→ 等评测 lark_template.json` | sft watcher fire complete 那一刻，agent 主对话**当轮**完成：写 `cml=running` + 起新 bg（提评测 + 等 lark_template.json）+ 注册新 watcher |
| bg 内部 sleep+poll 跨多个 step 的状态 | 每个 step 一个 bg + 一个 watcher；watcher fire = step 边界 = 主对话轮回来推进 |

**为什么是死规则**：bg 脚本本身不会 append `program-state.jsonl`，状态只活在 bg 进程内存。一个 bg 跨 step 跑起来后，前端从第一个 step complete 之后就完全感知不到后续 step——卡片永远停在第一个 step，watcher 也没注册新的。**bg 进程死了 / session 重连 / 中途想插手** 都补不回来，因为 program-state.jsonl 上没任何线索。

**自查判据**：bg 脚本里出现新 step 的关键词（`cml workflow run` 提评测、`submit_cml_eval.sh`、对应新 step 的 `echo running`），就是越界。bg 只该做"提一个远端 job 然后等它的产物"，**绝不能跨 step 边界**。

**watcher fire → 当轮 4 件套**（漏一件就断片）：
1. echo 上一 step `complete`（watcher 已自动写也算）
2. echo 下一 step `running`
3. 起新 bg「提交下一 step 的远端动作 + 等产物」
4. 注册新 watcher 监听下一 step 的产物文件

回主对话轮前自问一句：**"如果我现在被 kill，单看 program-state.jsonl 最后一行，下一个 agent 能不能接着干？"** 答不上来就是 4 件套没补全。

**绝不"事后补 status"绕过 watcher**：发现 step 在 UI 上一直 running、watcher 没自动标 complete，**正确做法是 debug watcher**（看 `_SUCCESS` / `lark_template.json` 是否真的落盘、watcher 进程是否还在），**不是**自己手 echo 一条 `step=complete` 把状态条点亮。后者掩盖 bug 但留下错位的 run_id，下一轮 watcher 会接着错。

### R6 — 同秒 running + complete 视为虚假完成（**反复踩坑**）

每个真实 step 都有最低耗时下限：

| step | 最低耗时 | 原因 |
|---|---|---|
| `cml` | 数分钟到数十分钟 | 远端 workflow 启动 + 评测 |
| `sft` | 30 分钟以上 | 训练 pod |
| `augment` | 数分钟 | GPT 仿写 + sanity + 格式校验 |
| `dist-analysis` | 1-3 分钟 | 读 metric_diff + 跨轮 diff + 写 workflow.md |
| `gold-drift` | 1-2 分钟 | drift 检测 |

如果 `step=running` 和 `step=complete` 两条 entry 的 `ts` 在**同一秒**（或差距远小于该 step 最低耗时），就是**虚假完成**——agent 平铺 echo 状态绕过了真实工作。**禁止**。

最常见踩坑：human-review / human-check 拿到用户决策后，agent 在同一轮里把 `hypothesis + augment + sft + watcher` 一口气 echo 出来，只真做了最后一步（提 SFT），中间的 `augment` 被跳过——`augment_<runDic>.jsonl` 根本没生成，前端 augment 卡永远空。

**正确做法**：human-review 之后必须按 program.md 逐 step 真跑：
1. 写 `hypothesis=running` → 真写假设到 iteration_log → 写 `hypothesis=complete`
2. 写 `augment=running` → 真跑 §4.0.1 + §4.1 + §4.2 + §4.3 + 层1格式校验 → 写 `augment=complete`（**这一步至少 5 分钟**）
3. 写 `sft=running` → 提交 cml custom_train + 起 watcher

**自查**：连续两个 step 的 `ts` 间隔 < 60 秒，必然有一个是假的。复盘时 grep program-state.jsonl 看相邻 entry 时间戳。

**前端表现的迷惑性**：虚假完成的 step **状态条仍显示 ✓ complete**（因为 entry 写了 complete），但**详情卡空**（因为 backend 按 step 关联的产物文件读内容——augment 卡读 4 个 `augment_<runDic>.*` 文件，文件不存在 = 卡片空）。**状态条说做完了、详情卡却空**——这是最具迷惑性的失败模式，必须从源头杜绝。

### R7 — `step=running` 与对应 `watch` 条目必须同事务落盘（**反复踩坑**）

watcher 触发 complete 时使用的 `run_id` **必须等于触发它的 watch 条目的 run_id**。后端 scanner 改造后实施两道闸门：

1. **每个 step 只 register 最新一条 watch 条目**（append-only jsonl 里历史 round 的 watch 不再被复活）
2. watch 条目的 `run_id` 必须**等于**该 step 最新一条 `status=running` 条目的 `run_id`，否则跳过等下一轮 scanner

这意味着 agent 一侧必须遵守：

- **写 `step=running, run_id=R<N>` 之后，必须紧接着写 `watch {step, run_id=R<N>, ...}`**——同一秒、同一个工具调用块内、不要被别的写入打断。理想是把"reset status + watcher 声明"做成一次 append 数组写入。
- **跨 round 的 watch path 即使重复也要重发**（如同样的 `_SUCCESS` 路径），不能依赖"上一 round 那个 watch 还在"——`run_id` 不一样，watcher 任务也是新的。
- **`run_id` 字段必填**，缺失会被 scanner 当成 `'?'` 处理，`'?' != 'R2'` 直接 skip，watcher 永远起不来。

**反模式（实际踩过的坑）**：
- agent 写完 `sft=running` 后没在同事务里写 watch → scanner 这一轮扫到时只有 status，没有匹配 run_id 的 watch → 用历史 watch 的 path 注册 watcher（旧逻辑下），run_id 错位
- watcher fire 后 agent 看 status 没动，又手动 echo 一条 `sft=complete`（违反 R5 末段）→ 后端的 scanner 已经修，但 agent 协议层面还是要 hold 住

**自查**：grep `program-state.jsonl` 里所有 `watch` 条目，每条的 `run_id` 必须能在前面（同一 round）找到匹配的 `step=<step>, status=running, run_id=<同值>` 条目。

### State 文件位置

```
<远端 workspace>/output/program-state.jsonl
```

bash 命令是在 **远端 workspace** 里跑的（一般 `/root/zk_agent_workspaces/LOCALID_xxx`），系统提示里的「当前会话目录」是 **后端宿主机的本地路径**，**不能直接拿来当 SESSION_OUTPUT**——拿了等于在远端凭空建一条同名死路径，后端 sync 永远读不到，前端卡片就一直不动。

正确做法：用远端 cwd 派生（`pwd` 就是当前远端 workspace），目录确保存在：

```bash
SESSION_OUTPUT="$(pwd)/output"
mkdir -p "$SESSION_OUTPUT"
```

或者显式写远端绝对路径 `SESSION_OUTPUT="/root/zk_agent_workspaces/LOCALID_<本会话 id>/output"`。**绝对不要**：
- 拷系统提示里的 `/home/mi/zk-data-agent-wsh/.port_sessions/.../output`（那是后端本地，远端没这条）
- 写到 `/mnt/wangsenhao/...` 或项目根目录（多会话互相覆盖）

后端 `_sync_remote_program_state` 只读远端 `{workspace_cwd}/output/program-state.jsonl` —— 路径写错 = 前端死锁。

### Step key 表（必须用这套 key，否则匹配不到卡片）

| step key | 对应卡片 |
|----------|----------|
| `cml` | CML 评测 |
| `gold-drift` | Gold Drift |
| `dist-analysis` | 分层结果分析 & 报告（含根因归类、workflow<runDic>.md 产出） |
| `hypothesis` | 形成假设 |
| `augment` | 数据增强 |
| `verify` | 修改返回验证 |
| `sft` | SFT 训练 |
| `log` | 记录迭代日志 |
| `next-round` | 下一轮评测（边界标记，**不生成卡片**，仅推进 UI 的 run_id 推断） |
| `human-check` | Human Check（HiTL 网关；命中 HiTL 时 agent 显式写，UI 在对应 round section 后插入 gate 卡） |
| `human-review` | Human Review（同上，多用于"建议人审 1/0"或"建议回滚"等需要人拍板的检查点） |

### KPI（你不要写，后端读权威源）

顶部 7 个 KPI（RUN ID / VERSION / 目标集合 / 大盘车载 / SPECIFIC TEST / ITERATION / 耗时）由后端从权威源直接读取：

| KPI | 后端读取自 |
|-----|-----------|
| RUN ID | `RUN_HISTORY_DIR`（默认 `/mnt/xiaoai-zk-model-train-tj5/workflow5/`）下 `workflow<N>/` 取 max N |
| VERSION | `skills/model-iteration/assets/config.yaml` 的 `cml_eval.version` |
| 目标集合 | 从 session 最新 user 消息里解析 `"开始, <name>"` 中的 name |
| 大盘车载 / SPECIFIC TEST | `RUN_HISTORY_DIR/workflow<runDic>/metric_diff/lark_template.json` |
| ITERATION | `$AUTORESEARCH_CHAT_ROOT/results/iteration_log.jsonl` 行数（每 chat 独立） |
| 耗时 | program-state.jsonl 第一条 `{step:"cml",status:"running"}` 的 `ts` 到现在 |

**你不要再写 `{"kpi":...}` 行**——写了也会被后端忽略，不显示。如果想让用户看到某个 KPI 的当前值，把它落到对应的权威源（比如改 config.yaml、写 iteration_log），后端会自动读到。

### Watcher（长任务的 UI 状态副车，**不是给你交回控制权的借口**）

CML 评测 / SFT 训练这类几分钟到几十分钟的长任务，声明 watcher 让**后端自动检测产物文件并把 `step=complete` 写到 state file**——这样 UI 的卡片状态不依赖你主动写 complete，watcher 替你写。

但你**仍然必须留在当前迭代里继续干活**，遵守 program.md 里的 **NEVER STOP / 全程不打断用户** 原则：

- **不许**给用户回"控制权交回 / 我先停下 / 等你来问"这种话
- **不许**主动结束本轮（除非命中 program.md 里写明的 H-i-T-L 阻断信号）
- 长任务（cml 评测、SFT 训练）**必须**用 `bash(run_in_background=true, wait_for_completion=true)` 提交：把"提交远端任务 + 等产物落盘"打包成一个 bg 命令；本轮 agent 主动结束（**不是**交回控制权——是把等待这件事 detach 给后端），产物落盘后后端自动起新一轮，agent 收到 stdout 直接进 Step 1
- **不许**在 bash 里写 `while true; do sleep 60; done` 这种轮询循环——bash 命令有 30s 硬超时，会被切成几十个 step，把本轮 50 step 上限耗尽留不出做分析的预算

watcher 解决的是 **UI 与 agent 异步**：UI 不用等 agent 写状态，watcher 替 agent 把 complete 落盘。它不是替你"放假"。

格式：

```bash
# 进入 step 时按规则写 running
echo '{"step":"cml","status":"running","ts":"'$(date -Iseconds)'"}' >> "$SESSION_OUTPUT/program-state.jsonl"

# 紧接着声明 watcher：当目标文件出现 → 后端自动写 cml=complete
echo '{"watch":{"step":"cml","kind":"file_exists","path":"/mnt/xiaoai-zk-model-train-tj5/workflow5/workflow17782/metric_diff/lark_template.json","interval":30,"timeout":3600,"run_id":"R0"}}' >> "$SESSION_OUTPUT/program-state.jsonl"
```

字段：

| 字段 | 必填 | 说明 |
|------|------|------|
| `step` | ✅ | 跟卡片 step key 一致（cml / sft / augment ...） |
| `kind` | ✅ | 当前只支持 `file_exists` |
| `path` | ✅ | 目标文件**绝对路径**。文件出现就视为 step 完成 |
| `interval` | ⭕ | 轮询间隔秒，默认 30，最小 2 |
| `timeout` | ⭕ | 整体超时秒，默认 3600，超时写 failed |
| `run_id` | ⭕ | 写入 Live Log 时的 iter 标签 |

**watcher 资源管理**：
- 同一 (session, step) 已有 watcher 在跑时，重复声明被忽略
- step 已经有 complete/failed 行后，watcher 声明会被忽略
- 后端 scanner 5 秒扫一次所有会话的 state file

**典型用法**（CML workflow，**用 bg 模式 + watcher**）：

长任务必须用 `bash(run_in_background=true, wait_for_completion=true)` 提交。bg 命令在远端 detach 跑、立刻返回 task_id；产物落盘后后端自动起新一轮把 stdout 作为 system 消息送回 agent，**不要**在 bash 里 sleep+poll。watcher 仍然要声明——它驱动 UI 卡片状态，跟 bg 任务是两条独立的通道。

```bash
# 1. 写 running + watcher 声明（UI 卡片靠它）
echo '{"step":"cml","status":"running","ts":"'$(date -Iseconds)'"}' >> $S

# 2. 用 bg 模式：把"提交 cml + 等 metric_diff 落盘"一次封装
#    wait_for_completion=true（默认）：进程结束后后端自动起新一轮把 stdout 送回
bash(run_in_background=true, command='''
  set -e
  RUNDIC=$(cml workflow run --workflow_id "$WORKFLOW_ID" ...)
  echo "RUNDIC=$RUNDIC"
  TARGET=/mnt/xiaoai-zk-model-train-tj5/workflow5/workflow$RUNDIC/metric_diff/lark_template.json
  while [ ! -f "$TARGET" ]; do sleep 60; done
  echo "EVAL_DONE runDic=$RUNDIC target=$TARGET"
''')

# 3. 紧接着声明 watcher（UI 卡片切到 complete 靠它，runDic 在 bg 任务 stdout 里）
#    watcher path 用稍后从 bg stdout 拿到的 runDic 拼；如果先不知道 runDic，
#    可以等 bg 任务返回 RUNDIC 后这一轮里再补声明 watcher。
echo '{"watch":{"step":"cml","kind":"file_exists","path":"'$ROOT/workflow$RUNDIC/metric_diff/lark_template.json'","interval":30,"timeout":7200,"run_id":"R'$N'"}}' >> $S

# 4. 跟用户简报一句"已提交，等产物落盘后会自动续"，agent 这一轮主动结束
#    （不是交回控制权——是把等待这件事 detach 给后端）

# 5. 产物落盘后，后端起新一轮，agent 收到 stdout（含 RUNDIC + EVAL_DONE）
#    → 立刻进入 Step 1，读 metric_diff、做分层分析、写报告……整轮 50 step 全用在分析上
```

**为什么不能在 bash 里 sleep+poll**：bash 工具有 30s 硬超时，`sleep 60` 会被切成 `sleep 25 + sleep 5` 两个 step；评测跑 25 分钟 = ~50 step 全耗在等待上，留不出做分析的预算。bg 模式只占 1 个 step。

**bg 任务排查**：agent 中途想看进度，可以 `bash_status(task_id)` 拿当前 stdout/exit_code 快照；想终止远端进程要 `bash_kill(task_id)`（前端按 stop 不会杀 bg 任务，那是 detach 的本意）。

### Live log（可选）

要把日志推到 UI 底部那块黑色 Live Log 区，追加：

```bash
echo '{"log":{"ts":"11:55:10","iter":"R1","text":"Step 1 dist-analysis: 写 workflow<runDic>.md，根因归类完成"}}' >> "$SESSION_OUTPUT/program-state.jsonl"
```

### 不要做的事

- **不要**写 program.md（旧机制已废弃，写了也没用，平台现在不读它）。
- **不要**把 state 文件写到 `/mnt/wangsenhao` 或仓库根目录——只写当前会话的 `output/program-state.jsonl`。
- **不要**用与上表不一致的 step key（比如 `step:"评测"`、`step:"step0"`），UI 匹配不到。
- **不要**重写整个文件（每行只追加，append 即可），重写会导致 UI 闪。

## 快速参考

| 项目 | 值 |
|---|---|
| 触发信号 | `"开始，<需求集合名>"` |
| 评测 workflow | `f-20260408161444-wu3pz`（版本见 `config.yaml`） |
| 训练基模 | `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507` |
| 工作目录 | `$AUTORESEARCH_CHAT_ROOT`（每 chat 独立，由后端注入；位于共享 NFS：`/mnt/<email_prefix>/autoresearch-zk-users/<chat_session_id>/`，jupyter pod 与训练 pod 都能读写） |
| 历史记录目录 | `/mnt/xiaoai-zk-model-train-tj5/workflow5/` |
| 需求集合位置 | `https://git.n.xiaomi.com/ai-service/ai-planning/-/tree/autoresearch-v1` 下 `ai-planning/data/specific_test_set/` |

## 工作空间隔离（按用户 + 每 chat 二级隔离，落在 NFS）

每个 chat session 一份独立工作区 `$AUTORESEARCH_CHAT_ROOT`（后端在 jupyter 启动时自动注入这个 env，agent 每次 bash 都能拿到）。该目录位于共享 NFS：

```
/mnt/<email_prefix>/autoresearch-zk-users/<chat_session_id>/
```

`<email_prefix>` 是用户登录时的小米邮箱前缀（`xxx@xiaomi.com` 取 `xxx`），用作账号根目录；同一用户跨 chat 共享根目录但 chat 之间完全隔离。

**为什么放 NFS 而不是 jupyter pod 私有路径**：SFT 训练在独立的 pytorch pod 里跑，那个 pod 不挂载 jupyter pod 的 workspace；放共享 NFS 是双方都能 `cd` 进去的唯一选择。

**所有写入路径都必须以 `$AUTORESEARCH_CHAT_ROOT` 开头**，不要写 hard-coded `/mnt/wangsenhao/autoresearch-zk/...` 全局路径，也不要写 jupyter pod 本地路径（`/root/zk_agent_workspaces/...`），训练 pod 看不到。

| 资产 | 位置 | 隔离方式 |
|---|---|---|
| `prepare_and_train_sft.py` | `$AUTORESEARCH_CHAT_ROOT/scripts/` | 后端 bind 时从 skill bundle 推过来，每次刷新最新版 |
| `ai-planning/`（含 corpus + augment） | `$AUTORESEARCH_CHAT_ROOT/ai-planning/` | **首次访问前你自己 git clone**（见 program.md 「ai-planning bootstrap」） |
| `zk_trainer/` | `$AUTORESEARCH_CHAT_ROOT/zk_trainer/` | **Step 5 首次 SFT 前你自己 git clone**：`git clone git@git.n.xiaomi.com:wangsenhao/zk_trainer.git`（见 program.md §5.0 line 1323）。**URL 写死了，不要问用户也不要换命名空间**；clone 失败先看 ssh key |
| basemodel（每轮 SFT 的 `--model_path`） | `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507` | **写死、强制**（见 program.md §5.0 line 1330 / 1345）。每轮 SFT 都从 basemodel 起，**不许从 sft_output 续训**，**不要问用户用哪个**——eval 阶段的 `model_path_new` 是另一回事 |
| `results/iteration_log.jsonl` | `$AUTORESEARCH_CHAT_ROOT/results/` | 每 chat 独立累积；后端 KPI 也读这里 |
| `results/error_registry.jsonl` | 同上 | 每 chat 独立 |
| `results/workflow<runDic>.md / data_clean_<runDic>/ / augment_raw/` | 同上 | 每 chat 独立 |
| `sft_output/` | `$AUTORESEARCH_CHAT_ROOT/sft_output/` | 每 chat 独立，互不覆盖；训练 pod 走 NFS 直接写 |
| `augment_<runDic>.jsonl` | `$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/` | 写在 chat 自己的 ai-planning clone 内，下一次 SFT prepare 只合并本 chat 的增量 |

**保持全局共享的资产**（不要按 chat 拆）：
- runDic 计数：`/mnt/xiaoai-zk-model-train-tj5/workflow5/`（max+1 是平台级唯一标识）
- 训练基模：`/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507`（只读模型权重）
- metric_diff 输出：`/mnt/xiaoai-zk-model-train-tj5/workflow5/workflow<runDic>/metric_diff/`（cml workflow 写）

**每个 step 落产物时务必用 `$AUTORESEARCH_CHAT_ROOT` 作前缀**（例如 `cd $AUTORESEARCH_CHAT_ROOT && python scripts/prepare_and_train_sft.py ...`，或 `echo ... >> $AUTORESEARCH_CHAT_ROOT/results/iteration_log.jsonl`）。

## 核心原则

1. **评测优先**：`"开始"` 信号的第一个动作永远是评测当前模型，绝不直接训练
2. **假设驱动**：每轮必须能回答"这轮验证了什么？学到了什么？下一轮改什么？"
3. **NEVER STOP**：全程不打断用户，循环直到达标或人类主动打断
4. **从 basemodel 训练**：每轮 SFT 的 `--model_path` = `/mnt/wangsenhao/verl_zk/Qwen3-4B-Instruct-2507`，禁止从上轮 ckpt 续训
5. **近邻检索先粗筛后精排**：>1 万行训练集禁止 O(M·N) 全量比对；倒排索引 → 50-300 候选 → 精排。详见 program.md §2.3
6. **大文件先切片**：>10MB 的 CSV/JSONL 先 bash `awk/grep/head` 切子集，禁止 `pd.read_csv` 整体加载
7. **pickle 缓存 /tmp**：训练集 + 倒排索引缓存到 pod `/tmp/`（mtime 作 key 自动失效），禁止每脚本重建
8. **stdout 分级 print**：只 print 决策聚合（<2KB），明细落 `/tmp/<step>_<runDic>_detail.json`
9. **一律 bash + 脚本文件**：`python3 -u /tmp/xxx.py 2>&1 | tee /tmp/xxx.log`，禁用 python_exec

## 达标条件

- **需求集合（目标集）** ≥ 95%（**最高优先级**）
- **大盘集（车载）** 降幅 ≤ 0.3%（次要）
- **specific test** 降幅 ≤ 1%（次要）

⚠️ **优先级铁律**：目标集未达 95% 前，其他集合轻微下降（大盘 ≤1%、specific ≤2%）不构成回滚理由。

## 主流程

完整 Step 0-7 规范见 `references/program.md`，**操作前必读**。

## 文件结构

```
references/
  program.md              # 完整迭代循环规范（必读，包含所有 Step 的详细说明）
  knowledge/
    navigation_routing_rules.md    # 地图导航 Agent/CT 分流规则
    travel_routing_rules.md        # 旅游 Agent/CT 分流规则
    multi_command_rules.md         # 多指令 vs ComplexTask 分流规则
    multiturn_continuity_rules.md  # 多轮对话 CT 继承规则

scripts/
  prepare_and_train_sft.py  # 数据组装 + SFT 训练脚本

assets/
  config.yaml              # 评估阈值 + CML workflow 配置
```

## 何时读取各文件

| 操作 | 读取 |
|---|---|
| 启动迭代前 | `references/program.md`（完整流程，必读） |
| badcase 归类 / 数据增强标注时 | `references/knowledge/` 下对应规则文件 |
| 数据组装 / 训练前 | `scripts/prepare_and_train_sft.py`（了解脚本参数） |
| 评测阈值 / workflow 版本 | `assets/config.yaml` |

## Human-in-the-Loop 信号（需暂停问人）

共 7 类迭代级信号 + 10 类训练集调整信号（T1-T10），详见 `references/program.md` 的"Human-in-the-Loop 时机"章节。

**最常触发的**：
- Gold drift ≥ 10 条（#1）
- 要批改旧标签 > 50 条（#3 / T1）
- 跨子集净退步（#4）
- 连续 3 轮目标子集净提升 ≤ 0.5pp（#5）

### ⛔ 落 gate 前必读：30 秒自检三问（**任一不过 = 不写 gate，直接进下一步**）

写 gate entry / 在 chat 里向用户提问之前，**逐条过这三问**。下面任何一段流程示例都默认你已经过了这三问；过不了，再漂亮的 summary/proposal/ask 也是干扰用户。

**Q1. 是真 §HiTL 信号吗？**
- ✅ 真信号：候选 > 50/200、Gold drift ≥ 10、跨子集净退步、连续 3 轮无提升 等列表里写明的条件
- ❌ 凑出来的理由：`第一次跑想让用户校方向` / `我担心副作用` / `想让用户拍板更稳` / `proposal 听起来风险大` —— 这些都是脑补，不是信号

**Q2. `ask` 是真分叉吗？**
- ✅ 真分叉 = 用户的判断能改变下一步动作：候选量收窄策略、回滚 vs 续训、换目标子集思路、改阈值
- ⛔ **"H? 假设组合"不是分叉**：`H1+H2 一起 vs 只跑 H1`、`先做 H1 还是先做 H2`、`激进 vs 保守 vs 中庸`、`要不要追加 H3` —— 假设的**拆分、组合、顺序、激进度**全是 agent 自己根据信号该决定的，决定完写进 `proposal` 公布即可，**不要甩给用户**

**Q3. 拆 H 的依据是数据信号还是脑补 trade-off？**
- ✅ 数据信号：mislabel 近邻数、no_neighbor 占比、pattern 分布、其他子集的实测分数差
- ❌ 脑补：`改这 12 条可能会让 X 子集下降` / `H2 加多了可能过拟合` —— 没跑过就是推测；推测可以写进 `proposal` 当 caveat（"预期回收 ~10 条，trade-off 待 R1 验证"），**做不出 ask**

> **反复踩坑案例**（5/24 真实复盘）：R0 baseline 跑完，37 错例里 12 条 mislabel（远低于 50 阈值），agent 自己脑补"H1 修标可能拖累线上挖掘召回complex 子集"，于是抛 `H1+H2 一起 vs 只跑 H1（激进 vs 保守）` 让用户拍板。**三问全挂**：信号没命中（Q1 ❌，12 < 50）、ask 是 H 组合（Q2 ❌）、副作用是推测（Q3 ❌）。**正确做法**：定下方案写进 proposal 公布，直接进 augment，trade-off 留给 R1 评测验证。

如果三问都过 = 真有事要用户拍 → 按下面 `### 写 gate 节点` 的写法落盘 + chat ask。
如果有一条不过 = 没事 → 不写 gate、不在 chat 里凑问题、直接进下一步。

### 写 gate 节点（让 UI 显示 Human Check / Review 卡）

凡是要让用户拍板的检查点，**必须** append 一条 gate entry 到 `program-state.jsonl`，**再** 用 chat reply 提问。UI 会在当前 round section 之后插入「Human Check」或「Human Review」横向卡片（IN PROGRESS 状态）。

> ⛔ **禁止**：只在 chat 里问用户、不写 gate entry。UI 看不到拦截 = 视为这一步没做——用户在面板里看不到任何卡，会以为流程卡死或还在自动推进。**这是反复踩坑点**：以前 agent 经常在 R0 baseline 跑完后聊天里问"要不要进 R1 train"，但 program-state 一直在写 augment running，UI 里完全看不到 gate。

#### 什么时候要写

需要用户拍板就写——不管 R0 还是 R{n≥1}，触发条件都按 §HiTL 信号判：

| 时机 | gate 类型 | run_id |
|---|---|---|
| R0 baseline 分析完成、命中 §HiTL 信号要让用户拍板（候选超阈值、目标子集异常、跨子集分歧大 等真实信号；⛔ **不是**"第一次跑想让用户校方向" / "我担心 H1 修标的副作用" / "想让用户在激进/保守里选"这种凑出来的理由——先过上面的自检三问） | `human-check` | `R0` |
| R{n≥1} analysis 完成、命中 §HiTL 信号（gold drift / regression / 跨子集退步 / 连续 3 轮无提升 等） | `human-review` | `R{n}` |
| §4.0.1 Step B：候选 > 200 条命中触发条件 #3 | `human-check` | 当前 round |
| 其他 HiTL 信号（候选 > 50 条需人审、Gold drift ≥ 10 条 等） | `human-check`（开始前）/ `human-review`（结果后） | 当前 round |

判断标准就一条：**只要你下一步打算 chat-ask 用户拍板，就先写 gate entry 再问**。

#### 写法

**强烈推荐结构化三段写法**（`summary` / `proposal` / `ask`）——UI 会渲染成"现状 / 提议 / 请选"三个带标签行，用户一眼看明白。`reason` 留作 fallback。

```bash
# ⛔ 写之前已经过了上面 §"30 秒自检三问"——否则不要写这条 gate。
# ⛔ ask 必须是真分叉。⛔ "H1+H2 一起 vs 只跑 H1"、"激进 vs 保守"、"先 H1 还是先 H2" 这类
#    "假设组合"统统不是分叉——是 agent 自己根据信号决定的，决定完写进 proposal 公布即可。
# R0 baseline 后命中真实 HiTL 信号（这里是候选量超阈值），让用户拍板更精细 pattern
echo '{"step":"human-check","status":"running","run_id":"R0",
  "summary":"R0 baseline 完成；复杂导航过召专项0511 = 58.89%(53/90)；候选 412 条（> 200 阈值）",
  "proposal":"H1（标签纠错）：改这 412 条 complex=true → complex=false。 H2（数据增强）：仿写 100 条 complex=false 的简单导航 query 补进训练集",
  "ask":"412 条偏多，是全部改、还是先收窄到 query 含「打开/进入」的子集（约 110 条）单独审一轮？",
  "ts":"'$(date -Iseconds)'"}' >> $S

# R1 评测发现 regression
echo '{"step":"human-review","status":"running","run_id":"R1",
  "summary":"R1 vs R0：导航bvt -3.2pp，可聊可控 -25pp，目标子集 +1.4pp",
  "proposal":"回滚到 R0 权重；下一轮把 H2 仿写量减半，避免对 complex=false 过拟合",
  "ask":"回滚 R0 还是继续跑 R2 看曲线？",
  "ts":"'$(date -Iseconds)'"}' >> $S

# fallback：只写 reason 也能跑（前端会启发式切分），但不如结构化清晰
echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选 412 条超阈值，需要人工定更精细 pattern 收窄","ts":"'$(date -Iseconds)'"}' >> $S
```

**顺序不能反**：先 echo gate entry → 再 chat reply 给用户。否则用户先看到聊天问话、UI 里却没卡，会困惑"流程是不是卡死了"。

#### 字段说明

| 字段 | 必填 | 说明 |
|---|---|---|
| `step` | ✅ | `human-check` 或 `human-review` |
| `status` | ✅ | 卡进入时写 `running`；用户回复后写 `complete` |
| `run_id` | ✅ | 当前所在轮次（决定 gate 插在哪个 section 后） |
| `summary` | 🔼 | **现状一句话**：跑了什么、关键数字。例：`R0 baseline 完成；专项 58.89%(53/90)，37 错全为 complex 误判` |
| `proposal` | 🔼 | **打算怎么干**：每个 H 单独说"H? (类型)：具体做什么"。详见下面规则 |
| `ask` | 🔼 | **让用户选什么**：必须是真分叉（用户的判断能改变下一步动作）。例：`候选 287 条偏多，全改、还是收窄到「打开/进入」子集（~110 条）单独审一轮？` ⛔ 反例：`H1+H2 一起 vs 只跑 H1`——假设组合是你定的，不甩给用户 |
| `reason` | ⭕ | 兜底用：没写 summary/proposal/ask 时前端会拿 reason 做启发式切分。但**优先用结构化三段**，别只写 reason |
| `ts` | ✅ | ISO 时间戳 |

🔼 = 强烈推荐写——三段都填 UI 会变成清晰的"现状/提议/请选"分块；都不填只填 reason 也能跑，但用户要自己抠语义。

#### proposal 写法规则（关键）

**每个假设单独一句，结构 = `H? (一两个字概括类型)：具体动作 + 数量 + 目标`**。比如：

- ✅ `H1（标签纠错）：把 7 条原标 complex=true 但实际是简单导航的样本改回 complex=false`
- ✅ `H2（数据增强）：仿写 100 条 complex=false 的简单导航 query 补进训练集`
- ❌ `H1 修 7 条 mislabeled` ← 用户要猜 mislabeled 是什么意思
- ❌ `H2 仿写 100 条 complex=false 简单导航` ← 没说补到哪、为啥补

**不要写进 proposal 的内容**：
- §X.X.X 规则引用（`触发 §4.0.1 Step B`、`命中条件 #3`）—— 用户不关心你按哪条规则做的，只关心你要做什么
- 流程自洽说明（`需要人工逐条审 1/0`、`走 sanity check`、`过格式校验`）—— 这些是 agent 内部流程，对用户决策没用
- 候选量区间括号注释（`100~150 触发 51-200 区间`）—— 数量 OK，区间归属归属是规则细节，删

**`ask` 字段尤其要注意：**

⛔ **不要把"H? 假设组合"作为分叉抛给用户**——如 "H1+H2 一起 vs 只跑 H1"、"先做 H1 还是先做 H2"。假设的拆分和组合是 agent 自己根据信号决定的，决定完写进 proposal 公布即可，不需要用户拍板。`ask` 只在**真有分叉**时才写：候选量收窄策略、回滚还是续训、目标子集换思路 等用户判断能改变下一步动作的场景。

如果一个 R0/R{n} 没有任何真分叉（只是想"汇报+确认"），不要硬凑 ask、也不要写 gate——直接进下一步。

写 proposal 时问自己：**"这句话如果交给一个新加入的产品同学看，他能不能 5 秒内明白要干什么"**。能 = ✅；得回头查 §X.X.X 才能懂 = ❌，重写。

**用户拍板回复后**：append `status:"complete"` 同 step + run_id 一行，gate 卡变 COMPLETED；然后才继续往下走（继续/回滚/调参）。

**两种 gate 的区分（约定）**：
- `human-check`：偏"开始前的检查"——baseline → train 网关、>200 条候选定 pattern、Gold drift 复核
- `human-review`：偏"结果出来要复核"——R{n} 评测出来发现 regression 要不要回滚、目标集合连续 3 轮无提升要不要换思路

实际差别在 UI 上不大（都是横向 dashed 卡），区分主要为了让用户从标题就大致知道是开始前还是结果后的检查点。

## CML 配置

见 `references/program.md` §5.2。

## 结果文件 → 前端卡片映射

| 文件 | 前端卡片 |
|---|---|
| `results/iteration_log.jsonl` | hypothesis / log |
| `results/error_registry.jsonl` | log |
| `results/workflow<runDic>.md` | dist-analysis |
| `output/relabel_candidates_<runDic>.csv` | dist-analysis（阶段一：候选） |
| `results/data_clean_<runDic>/modified_samples.jsonl` | augment（阶段二：落盘） |
| `results/augment_raw/augment_<runDic>_raw.jsonl` | augment |
| `ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl` | augment |
| `results/gold_drift/drift_<runDic>.json` | gold-drift |
