Files
zk-data-agent/skills/model-iteration/references/program.md
T
wangsenhao b48032e15c feat: backend gate fallback + metric_diff docs + submit_sft fix
- Auto-inject human-review gate when agent asks question but forgets gate entry
- Add complex_dev/complex_base field docs to program.md metric_diff table
- Fix submit_sft.sh minor issues
- jupyter_runtime improvements

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 15:42:02 +08:00

99 KiB
Raw Blame History

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

这是小爱中控理解调度模型的自主迭代框架。你是一个完全自主的 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 一律由 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)、跨轮 diffpersistent/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_newmodel_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 操作失败时)

# 检查是否存在 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.pyai-planning corpus 需要 agent 自己 git clone(之前依赖全局共享,已废弃):

# 检测是否已 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_.jsonl 写入、近邻检索、训练集修改)。prepare_and_train_sft.py 在脚本顶部统一以 CHAT_ROOT = Path(__file__).resolve().parent.parent 解析 chat 工作区(脚本本身位于 $AUTORESEARCH_CHAT_ROOT/scripts/),AI_PLANNINGzk_trainer_dir 都基于此推导,不要在 agent 侧手动 patch Path(__file__)——以前需要改两处的坑已在脚本侧统一。

zk_trainer 的 clone 在首次 SFT 前进行(见 §5.0),目标也是 $AUTORESEARCH_CHAT_ROOT/zk_trainer

cml 环境检查(每次评测前必做)

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 自动装(非交互式可完成):

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 showdefault_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 还是交互式,历史上踩过坑):

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-configworkspace_id=10065auth_type=keyconfig 文件路径 ~/.config/cloudml/config.yaml

迭代循环

LOOP FOREVER

0. CML 评测(当前模型,先定位再动手)

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_newmodel_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=TrueFalse),但本地 CSV 完全没动。如果不检查就当成模型回退,会用错误数据继续仿写。

自动检查(写到 Step 0 末尾):

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.yamltest_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%
相对基线变化 新模型准确率 基线准确率(百分点)
准确率 该子集 GSB 统一准确率(cleaned_predict vs label
分流错误 base 模型和 dev 模型对该 case 的「是否复杂」二值判断不一致(一边判复杂、一边判不复杂),cleaned tag 是否相同不影响判定。具体怎么从两列原始输出里抽出「是否复杂」这个判断,由 agent 分析前先 head 当前评测产出反推,不要照抄历史固定字符串
语义错误 cleaned_predict_base != cleaned_predict_dev,意图/tag 本身判错
持久错误 该 case 在上一轮也错(查 error_registry.jsonl
新引入错误 该 case 在上一轮对,本轮错 —— 最危险的信号,说明上轮干预有副作用

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

字段 含义
origin_predict_base 旧模型(baseline)原始输出。具体格式(是否含 complex= 前缀、tag 包装、自定义 class 名等)以当前评测产出为准,分析前 head 一下实物
origin_predict_dev 新模型(迭代模型)原始输出。同上,格式以当前产出为准
complex_dev 迭代模型的 complex 标签(true/false),从 origin_predict_dev 解析出的复杂度判定
complex_base baseline 模型的 complex 标签(true/false),从 origin_predict_base 解析出的复杂度判定
cleaned_predict_* 清洗后输出,用于 GSB 对比
label / code_label_base ground truthlabel 为空时回退解析 code_label_base

2.2 分析步骤

  1. metric_diff/specific_comparison.csv 和需求集合对应 CSVutf-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 矛盾 ⚠️训练集打错标,必须落盘到 $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 信号、不是结论信号。

    ⚠️ 算法选型:错例集 O(几十)、训练集 O(几万) — 不要用复杂度高、要把训练集全量物化成稀疏矩阵或 embedding 的算法(如 sklearn.TfidfVectorizer.fit_transform 全量 + cosine — 实测 35k 行字符 n-gram 600s 仍 timeout)。

    ⚠️ >1 万行训练集做近邻必须先粗筛后精排,不许直接 O(M·N) 全量比对——37 错例 × 30k 训练 = 1.1M 次精排,朴素写法实测 ~3 min(v32 R0 case:错例侧逐条扫训练集 + 每例剪枝仍 182s,且后续每加一步 mislabel 定位都要再跑一遍)。两段式:

    1. 粗筛(倒排索引,全表只扫一次):训练集一次性建 token → set(line_idx) 倒排表,key 用 query 的 2-gram 字符 token30k 行 query 通常 3k5k 唯一 token,建表 O(N) 秒级)。
    2. 错例侧逐条取候选:每条错例提它自己的 2-gram tokensunion 训练集对应 posting list → 通常每条得到 50–300 候选行(不是 30k)。
    3. 精排(小集合上做精细打分):仅对粗筛候选跑 Jaccard / char-overlap / Levenshtein 取 top-3。

    预算对照(37 × 30k):朴素 O(M·N)=1.1M 次精排(~3 min);倒排粗筛后 ~37×200=7.4k 次精排(~10s)。错例侧的"轻量剪枝"只是常数优化,不能替代粗筛——粗筛把候选基数砍到 1/100 才是数量级提速的来源。

    ⚠️ 训练集 + 倒排索引必须 pickle 缓存到 /tmp,不要每个脚本重建dist-analysis 一轮要跑 kn_search / judge_check / mislabel_list / cross_subset 等多个脚本,每个都要读 30k 行 all_train.jsonl + 重建倒排索引——v32 R0 实测 5 次重读累计 ~50s 全是浪费。pod /tmp 是本地 SSDpickle.dump 之后所有后续脚本 unpickle <1s。模板:

    # 每个会读 all_train 的脚本头部统一这一段,cache key 用文件 mtime 自动失效
    import os, json, pickle
    from pathlib import Path
    ALL_TRAIN = Path(os.environ['AUTORESEARCH_CHAT_ROOT']) / 'ai-planning/data/train_set/zk_intent/all_train.jsonl'
    CACHE = Path(f'/tmp/all_train_{int(ALL_TRAIN.stat().st_mtime)}.pkl')      # mtime 当版本号
    IDX_CACHE = Path(f'/tmp/all_train_idx_{int(ALL_TRAIN.stat().st_mtime)}.pkl')
    
    if CACHE.exists():
        rows = pickle.loads(CACHE.read_bytes())                                # <1s
    else:
        rows = [json.loads(l) for l in ALL_TRAIN.open('r', encoding='utf-8')]  # 515s NFS
        CACHE.write_bytes(pickle.dumps(rows))
    
    # 倒排索引同样缓存(first script 建好就一直复用)
    if IDX_CACHE.exists():
        inv = pickle.loads(IDX_CACHE.read_bytes())
    else:
        inv = {}
        for i, r in enumerate(rows):
            q = r.get('query', '')
            for tok in {q[k:k+2] for k in range(len(q)-1)}:                   # 2-gram
                inv.setdefault(tok, set()).add(i)
        IDX_CACHE.write_bytes(pickle.dumps(inv))
    

    忌:每个脚本头都重写一遍 for line in open(jsonl): json.loads(line);用 /tmp/<runDic>.pkl 而忽略 mtime(训练集中途被改不失效);把 cache 写到 NFS(白做了)。

    ⚠️ stdout 是下一轮 LLM 的 input,分析脚本只 print 决策聚合,明细落 /tmp/<step>_<runDic>_detail.json:每个分析脚本的 stdout 会原样喂回 LLM 当下一轮上下文,每 KB 都要 prefill。v32 R0 实测 kn_search 一次 stdout 18KB37 错例 × 3 邻居 × 6 列)、judge_check 4.7KB、mislabel 11.3KB,整个 dist-analysis 累计 ~50KB 全是重复模式行。kn_search 模板(其他步骤同构):

    # 不要 print 整张 candidate × neighbors 表;只 print 三档分布 + 每档 top-3 范例
    import json, collections, os
    buckets = collections.Counter()
    exemplars = collections.defaultdict(list)
    detail = []                                         # 全量明细,落盘不 print
    for case in failed_cases:
        neigh = topk(case, k=3)                         # 粗筛 → 精排已就位
        verdict = classify(case, neigh)                 # 'no_neighbor' / 'consistent' / 'mislabel'
        buckets[verdict] += 1
        if len(exemplars[verdict]) < 3:
            exemplars[verdict].append({
                'query': case['query'][:50],
                'gold': case['gold'],
                'top_neigh': [{'q': n['query'][:50], 'label': n['label']} for n in neigh],
            })
        detail.append({'case': case['idx'], 'verdict': verdict, 'neigh': neigh})
    
    detail_path = f'/tmp/kn_search_{os.environ["RUN_DIC"]}_detail.json'
    json.dump(detail, open(detail_path, 'w'), ensure_ascii=False)
    
    # 只 print 决策需要的聚合(目标 < 2KB)
    print(f'[kn_search] total={len(failed_cases)} buckets={dict(buckets)} detail={detail_path}')
    for v, exs in exemplars.items():
        print(f'-- top-3 of {v} --')
        for e in exs:
            print(json.dumps(e, ensure_ascii=False))
    

    忌:把每条错例的 k 个邻居全 print;DataFrame.to_string() 直接喂 stdout(自带 padding,一张表 2KB+);用 cat > /tmp/x.py <<EOF ... EOF 写脚本(runtime 会把每行 PS2 提示符 > 回显进 stdout,一次累计 1–2KB),改 python3 - <<'PY' ... PYpython3 -c "$(base64 -d <<<'...')" 注入。

    ⚠️ 大文件 I/O>10MB 量级的远端 CSV/JSONL(如本步要读的 specific_comparison.csv,常见 90+MB),先用 bash awk/grep/head 在远端切目标子集再处理。直接 pd.read_csv 整体加载、或把 DataFrame 当返回值穿过工具调用 → 会撞 runtime 输出/返回大小限制(实测 99MB 直接读报"太大了")。

    ⚠️ 执行通道:所有 python 一律走 bash + /tmp/xxx.py + python3 -u … | tee /tmp/xxx.log,禁用 python_execpython_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,几乎抵消。

自动诊断

# 跨子集同形 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 报告模板

首次评测使用下方"首次评测报告模板",后续迭代使用"迭代报告模板"。

首次评测报告模板
# 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. ...
迭代报告模板
# 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 参考实现

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)

    is_triage_err 的判定无法用通用代码——agent 先 head 当前
    origin_predict_base / origin_predict_dev 实物,根据真实格式现写 parser
    抽出 base / dev 的「是否复杂」二值判断再比对。
    """
    cpb, cpd = row['cleaned_predict_base'], row['cleaned_predict_dev']
    is_triage   = ...  # TODO: 见 docstring,按当前评测产出格式现写
    is_semantic = cpb != cpd
    return is_triage, is_semantic

def case_hash(query: str, label: str) -> str:
    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 的证据)

只有证据充分时才往下走。 在开始训练前,写入 results/iteration_log.jsonlhypothesis 字段:

  • 假设:本轮要验证什么?(例:"分流错误主因是 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 qf1ni1jh25/50 turns 实际只用一半)。

4. 数据生成(GPT-5.4 从 badcase 增强)

只在 §2.4 归因为 data 时做。 其他归因直接跳过 Step 4,按下表路由:

归因 本步动作 跳到哪步
data 做 Step 4 数据增强(在已有数据基础上增删改) Step 5SFT 重训)
格式prompt / label schema 不对齐) 改 prompt 模板或 label 渲染逻辑 Step 5(SFT 重训,格式变了必须重新 SFT)
hparam config.yamlLR / batch / epoch 等) Step 5SFT 重训)
假设本身站不住(Step 1 证据不支持 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:候选定位(程序化扫描,不直接改)

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 看不到。

runDic 命名语义(backend 已对齐)augment 类产物(data_clean_<N>/augment_raw/augment_<N>_raw.jsonlzk_intent/augment_<N>.jsonllabel_master_review.jsonl)的 <N> = $SFT_RUNDIC = 当轮 eval 落盘后 resolve_run_ids.sh 取到的 max(已落盘 workflow)。由于 resolve 是在当轮 eval 的 lark_template.json 落盘之后调用的,此时 max 就是当轮 eval 自身的 workflow id。Backend step-detail 端点对 augment 也直接用本轮(per_round[R{n}])的 runDic 去读。其他 stepcml / dist-analysis / hypothesis / gold-drift)的产物同理按本轮自己的 runDic 命名。

⚠️ runDic 不许手算augment 落盘、SFT yaml、归档目录、cml workflow run 用的 runDic 必须scripts/resolve_run_ids.sh 取,不准 agent 自己 ls workflow5 | tail -1 然后心算 +1。规则简单到没必要靠模型:

eval "$(./scripts/resolve_run_ids.sh)"
# 现在 shell 里有:
#   $SFT_RUNDIC   = max(已落盘 workflow) = 当轮 eval 的 workflow id
#                   augment 落盘 / yaml / 归档全用这个
#   $EVAL_RUNDIC  = SFT_RUNDIC + 1(仅下一轮 cml workflow run 提交时使用)

写 augment / SFT / 归档脚本时一律用 $SFT_RUNDIC,不要写 ${RUNDIC} 也不要写 ${EVAL_RUNDIC}

🚨 调用时序(CRITICALresolve_run_ids.sh 只能在当轮 CML eval 的 lark_template.json 已落盘之后调用。禁止在 eval 提交前或 watcher 等待期间预先调用——此时 max workflow 仍是上一轮的值,会导致 SFT_RUNDIC 偏移 -1(已踩坑:R0 workflow17823 评测中提前 resolve 得到 17822augment 文件全部错位)。正确时序:cml step=completeresolve_run_ids.sh → augment/SFT。每次进入 augment/SFT 步骤前必须重新调用,不许复用之前缓存的值。

Step A.5label-master 预审(量级判定之前必跑,强制)

把 Step A 产出的候选交给 label-master 做语义判定,用 label-master 的"推荐标签"覆盖候选原始推测的 new_label,目的是在拿去人审之前先消化掉 label-master 自己就能定的那部分,缩小残留候选量。

具体步骤:

  1. 抽出每条候选的 (file, line, query, old_label, suspected_new_label)
  2. 逐条Skill(skill="label-master", args=...)——每次只传一条 (query, old_label)给 suspected_new_label,避免锚定),让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 输出 → {verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}
    • ⚠️ 禁止把多条 query 塞进同一次 Skill 调用——批量调用会让 label-master 在多条之间相互锚定,分错率显著升高(已踩坑)
    • 可在同一轮 assistant response 里并发多次 Skill 调用(建议 ≤ 8 路并发)以提升吞吐
  3. 把 label-master 的"推荐标签"回写到 relabel_candidates_<runDic>.csv 覆盖原 建议新label 列;新增列 verdictlabel_master_理由,便于回查
  4. 收尾时按以下规则筛 candidate list残留候选 = 真正进入 Step B 的列表):
    • 推荐标签 == old_labellabel-master 不认同要改 → 从候选里剔除(这条原 label 可能本来就是对的)
    • 推荐标签 != old_labellabel-master 同意要改(不论与 suspected_new_label 是否一致)→ 保留new_label = 推荐标签

Step B:量级判定(基于 label-master 处理后的残留候选量)

残留候选量 处理
≤ 50 条 程序化 sanity check + 按 label-master 推荐自动改(不再走人审)
51 ~ 200 条 必须全量导出到飞书 sheet 让人逐条审 1/0(不抽样);导出时带 query / old_label / 推荐标签 / label_master_理由 四列
> 200 条 强制 H-i-T-L 介入(触发条件 #3),让人定更精细 pattern 收窄

注:阈值仍按原来 autoresearch 的人审规则;变化只在于残留候选已经过 label-master 一遍语义筛减,避免把 label-master 自己就能定的条目也塞进飞书让人重审。

Step C:备份(强制,覆盖前必做)

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:修改执行(按文件批量,避免重复读写)

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. 数据 sanityprepare_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 1 里 纯模型GSB == 'B' 的 case。按两个维度排优先级:

    1. 测试集维度:本次需求集合 > 其他 specific test / 大盘(需求集合是当前瓶颈,优先补)
    2. 跨轮维度new / regressed > persistent(新引入/回退的先处理,持久错误兜底)

    组合优先级 P0→P3:需求+new/regressed → 需求+persistent → 其他+new/regressed → 其他+persistent。

  • 按子集聚类:每个需求子集/specific 子集分别处理,保证增强数据在该子集上的覆盖度。

  • 每条 badcase 的字段query对话历史(从 input 提取的 [对话历史] 段)、labelorigin_predict_dev(错误输出)、sub_cate

4.1.1 SFT 训练数据格式规范(生成前必读)

所有生成的训练数据最终写入 augment_<runDic>.jsonl 时,必须是如下 3 字段 JSONL 格式(与 all_train.jsonl 完全一致):

{
  "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]
[系统状态] 固定 {} {}

正确示例

{"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 格式)的列名保持一致,避免归档时再做字段映射。

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

自动检查代码模板
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 1 错例分析后,对所有未被 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 被拦下)
自动归纳代码
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()

### 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 复核:格式 + 语义。没过这关不许进 Step 5。

复核范围(两份必须过格式层;语义层只对 H2):

文件 来源 层 1(格式) 层 2(语义,Skill 调 label-master
$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl H1 改标(complex 翻转 / tag 改写等) 必跑 已在 §4.0.1 Step A.5 完成,不再重跑label-master 不会推翻自己的推荐,重跑只是浪费 token)
$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl H2 仿写(新增训练样本,未经过 §4.0.1) 必跑 必跑

层 1:格式校验(确定性,自动跑)

cd /home/mi/zk-data-agent-wsh   # 或 wherever skills 包在的项目根
# H1 改标
python skills/label-master/scripts/validate_label_output.py \
  --file "$AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/modified_samples.jsonl" \
  --field output_after
# H2 仿写
python skills/label-master/scripts/validate_label_output.py \
  --file "$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_<runDic>.jsonl" \
  --field output

任一行 failtag 不存在 / Agent 包装错 / function 引用错)→ 必须修,不许直接跳过。修完原地重跑直到全过。

层 2:语义复核(Skill 调用 label-master Agent,仅 H2

禁止用正则/规则脚本替代 LLM 语义判断。层 2 不是格式校验,是"该 query 在 label-master 知识体系下是否应该标成这个 label"的语义问题。唯一合法实现方式是调用 Skill(skill="label-master")label-master 已标记 repeatable: true,可在同一 run 内多次调用)。已踩坑:R1 agent 写了个检查 hist 里有没有导航触发词的正则脚本充当"layer 2",完全没走语义判断,导致错误样本全量混入训练集。

H1 在 §4.0.1 Step A.5 已经过 label-master 推荐覆盖,这里不再重跑。H2 仿写是 §4.2 GPT 新增的样本,没经过 Step A.5,必须在此补一次语义判定:

🚨 覆盖率硬规则label_master_review.jsonl 里 H2 部分的行数必须等于 augment_<runDic>.jsonl 的行数(1:1 全量覆盖)。禁止抽样、禁止"前 N 条 spot-check"、禁止"5/5 pass 推 100 OK"——§5.0 准入门会用 wc -l 拦。已踩坑:R17817 跑了 5 条样本就上报"100 条 pass",被 §5.0 退回。

  1. 逐行读 augment_<runDic>.jsonl 全部 (query, output) 对(N 条就是 N 条,不省略、不抽样)
  2. 逐条Skill(skill="label-master", args=...)——每次只传一条 (query, 当前 label)
    • 让 label-master 按 §决策流程 / §候选召回索引 / §高频混淆边界 判定该条
    • 输出格式:{verdict: 通过 | 不通过, 推荐标签, 排除理由, 易混淆边界}
    • ⚠️ 禁止把多条 query 塞进同一次 Skill 调用——批量调用会让 label-master 在多条之间相互锚定,分错率显著升高(已踩坑)
    • 可在同一轮 assistant response 里并发多次 Skill 调用(建议 ≤ 8 路并发)以提升吞吐
  3. 收集 verdict,写入 $AUTORESEARCH_CHAT_ROOT/results/data_clean_<runDic>/label_master_review.jsonl,每行一条 verdict(只含 H2 条目)

verdict 处置规则(自动)

比例 处置
不通过 = 0 全量通过,允许进入 Step 5
不通过 > 0 必须修正H2 → 修正 augment 样本后重新 reviewH1 → 回滚或修正后重新 review),循环直到不通过 = 0 才能进 Step 5。禁止"自动丢弃不通过样本继续"——每条都必须过

**写完复核报告才允许进 Step 5。**verdict 文件不存在 / 没跑层 2 → Step 5 拒绝启动(5.0 的准入检查会查 label_master_review.jsonl 存在)。

🚨 注意label-master 是"知识 + Agent 推理"型 Skill,不是黑盒分类器。每一次调用都必须输出 verdict + 推荐标签 + 排除理由 三件套,便于人类回查;严禁要求它返回 1/0 标量。(批量调用本身已在第 2 步禁掉,这里再强调一次输出 schema。)

5. SFT 训练

🚨 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.sh <SFT_RUNDIC> [PREV_RUNDIC] 并立刻挂 watcher(§5.2)。训练 _SUCCESS 落盘后的下一轮 step 单独用 bash scripts/submit_cml_eval.sh <EVAL_RUNDIC> 起评测,不要把评测串到 SFT bg 任务里——跨 step bg 是禁区

反模式(被反复踩坑,永远禁止):

  • 写完 augment=complete → 写一份漂亮的"Step 4 完成"进度简报 → turn 结束 → 等下一次触发
  • "augment 已完成,下一步等回调后续做 SFT" — 不存在等回调,augment 完成的那一刻就是 SFT 启动的那一刻
  • 把 H2 后台任务的 stdout 当成"用户消息"处理完就 turn 结束 — bg 任务回调不是 turn 结束信号,augment=complete 也不是

模型本能会想停("任务段落感"),SKILL NEVER STOP 压不住这个本能——所以这里写成硬命令清单,照清单第一条到第三条机械执行,做完才允许 turn 结束。

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

5.0 环境准备

准入检查(少一项不许进)

# 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)
if [ "$NOT_PASS" -gt 0 ]; then
  echo "label-master 不通过 $NOT_PASS 条(要求 = 0),必须修正后重新 review 直到全 pass"
  exit 1
fi

# 1b. coverage 断言:augment 行数必须被 review 完整覆盖(杜绝抽样外推)
#     review.jsonl 同时包含 §4.0.1 Step A.5 的 mislabel candidate review + §4.5 的 augment 仿写 review
#     所以行数下界 = augment.jsonl 行数(mislabel 部分多出来的那批不影响下界)
AUG="$AUTORESEARCH_CHAT_ROOT/ai-planning/data/train_set/zk_intent/augment_${RUNDIC}.jsonl"
if [ -f "$AUG" ]; then
  AUG_N=$(wc -l < "$AUG")
  REV_N=$(wc -l < "$REVIEW")
  if [ "$REV_N" -lt "$AUG_N" ]; then
    echo "label-master review 行数 $REV_N < augment 条数 $AUG_N — §4.5 必须逐条覆盖,禁止抽样外推(如 5/5 pass 推 100 OK),回 §4.5 把 augment.jsonl 的每一条都 append 一行 review entry"
    exit 1
  fi
fi

# 2. zk_trainer 仓库已 cloneURL 写死,不要换命名空间,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-tj5labelref.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.tplimageCommand--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 经验沉淀)

训练前强制

# 上一轮 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),训练用 submit_sft.sh 提交、评测用 submit_cml_eval.sh 单独起。这两步不再串接——agent 用 watcher 监听 sft_output/_SUCCESSwatcher fire 后下一轮主动调 submit_cml_eval.sh。本地无 PID 可监控,全靠 watcher 串两个 step。

Claude-side 双 watcher 强制要求(R17775 经验沉淀)

旧版 submit_sft_via_cml.sh 在脚本内嵌 watcher 自动起评测——已废弃(违反"bg 任务作用域 = 单 step"原则)。现在 submit_sft.sh 只负责提 SFT、立刻退出;起评测必须由 agent 在 SFT watcher fire 后新一轮手动调 submit_cml_eval.sh。Claude 如果只挂一个"等评测产物"的 watcher,训练完成事件会被漏报(R17775 训练 19:57 完成,Claude 40+min 不知道,直到用户问)。

Claude 调用 submit 脚本后,必须立刻用 Bash run_in_background 起两个 watcher task(一个都不能少):

⚠️rm -f sft_output/_SUCCESS 再挂 Watcher 1R17777 经验:上一轮遗留的 _SUCCESS 会让 watcher 瞬间误报 train done)。cml 容器内 imageCommandrm -rf sft_output 要等到 state=deploying→running 之后才执行,若 watcher 只靠 _SUCCESS 存在与否判断,会在 deploy 阶段直接触发。

# 预清理:必须在挂 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 2R17775 犯的错) —— 训练完到评测完之间的窗口期完全失联
  • 依赖 tail -f /tmp/r<RUNDIC>_logs/cml_watcher.log —— Claude 不会主动 tail
  • 依赖用户看到别的信号来触发 —— 违反 "NEVER STOP" 自主循环原则

旧版 nohup 路径下的 watcher 模板(仅在 5.2 例外情况使用):

#!/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 数据组装

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.jsonlaugment_*.jsonl 等)
  3. 合并输出到 zk_trainer 所需的目录结构:
<output_dir>/
  train/zk_sft_new_structure/merged_train.jsonl    # 合并后的训练数据
  validation/zk_sft_new_structure/part-0.jsonl      # 合并后的验证数据

验证集文件名不能含 valid/test/evalHF datasets 会推断错误的 split),脚本自动重命名为 part-0.jsonl

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

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

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

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

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 卡,本地零占用。

一键提交脚本

⚠️ runDic 不许手敲:先 eval 一下 resolve_run_ids.sh 拿到这一轮的 $SFT_RUNDIC / $EVAL_RUNDIC,再原样传给两个独立脚本。R2 踩过的坑就是手算 runDic 时把 SFT yaml 的命名也跟着 +1,导致 data_clean_<EVAL_RUNDIC>/ 跳过了 R1 命名空间。

提交流程拆成两步(两个独立脚本,两个 step):

cd "$AUTORESEARCH_CHAT_ROOT"
eval "$(./scripts/resolve_run_ids.sh)"            # 解析出 $SFT_RUNDIC / $EVAL_RUNDIC

# Step 5:提交 SFT 训练(不附带任何评测逻辑)
./scripts/submit_sft.sh "$SFT_RUNDIC"

# 训练 _SUCCESS 落盘后的下一轮 step 才起评测:
./scripts/submit_cml_eval.sh "$EVAL_RUNDIC"

submit_sft.sh <SFT_RUNDIC> [PREV_RUNDIC]

  1. 渲染 scripts/sft_train_job.yaml.tpl 为本轮 yaml(替换 {RUNDIC}$SFT_RUNDIC{PREV_RUNDIC}$PREV_RUNDIC
  2. cml custom_train submit --filename <yaml> 提交任务,打印 JobID,立刻退出
  3. 不再内嵌 watcher——agent 自己用 watcher 监听 $AUTORESEARCH_CHAT_ROOT/sft_output/_SUCCESSwatcher fire 后下一轮再调 submit_cml_eval.sh

submit_cml_eval.sh <EVAL_RUNDIC> [MODEL_NEW] [MODEL_OLD]

  1. skills/model-iteration/assets/config.yamlcml_eval.workflow_idcml_eval.version(版本不再硬编码,调版本就改 config.yaml
  2. MODEL_NEW 默认 $AUTORESEARCH_ROOT/sft_outputMODEL_OLD 默认旧基线(也可以通过 env / 参数覆盖)
  3. cml workflow run --workflow_id ... --version ... --global_inputs runDic=$EVAL_RUNDIC ... 起评测,评测 runDic=$EVAL_RUNDIC(整条流水线唯一一次 +1
  4. 立刻退出,UI 卡片状态走 program-state.jsonl 的 watcher 通道
yaml 模板要点(scripts/sft_train_job.yaml.tpl
字段 说明
imageUrl micr.cloud.mioffice.cn/wsw/large-lm:1.0.15-2 zk_trainer 默认训练镜像(torch 2.6 + accelerate 1.7.0
queueId 6052 (bj-nlp) h20-96g 资源池
resourceName cloudml.ng2h20-8-8.20-199 8 卡 H20 96G
juiceFsMountConfigs wangsenhao + xiaoai-zk-model-train-tj5 训练数据 / 产出路径
imageCommand 内嵌 prepare → train → mark _SUCCESS 训练成功才落标记
retryConfig enableRetry: true, NodeFailure 节点级故障自动重试 2 次
alertConfig FAILED + SUCCEED 飞书 P2 告警 异常立即知道
监控命令
# 查任务实时状态
cml custom_train describe <JOB_ID>

# 查训练实时日志(含 accelerate / trainer 输出)
cml custom_train logs <JOB_ID> --follow

# 停任务
cml custom_train kill <JOB_ID>

# SFT 不再内嵌 watcher,自动评测改成 agent watcher → 单独调 submit_cml_eval.sh
与 nohup 路径的对比
维度 nohup(旧) cml submit(新)
本地 GPU 占用 8 卡满载 0
网络/会话中断容忍 中断即停 任务持续跑
节点故障重试 自动 2 次
失败告警 飞书 P2
任务历史 仅本地 log cml 平台可查
启动复杂度 单行 nohup submit_sft.sh + watcher + submit_cml_eval.sh
何时仍用 nohup 本地训

只有以下情况例外使用本地训:

  • cml 队列资源排队 > 30min(紧急复现验证)
  • 调试新训练逻辑(频繁改代码)
  • 实验性超小规模训练(< 1000 条数据,< 2 epoch

其余一律走 cml。

兼容旧脚本

prepare_and_train_sft.py 不动,cml 任务的 imageCommand 内部仍调用它。train 子命令本身只负责训练,需先执行 5.1 组装数据。配置见 5.0。

6. 记录结果

每轮写入 results/iteration_log.jsonl 一行,schema

{
  "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. 全量 case 直接铺进对话(凡是"该不该改 / 该不该删 / 该不该新增 N 条"型决策都适用,包括但不限于 H-i-T-L #1/#2/#3/#6 与 T1/T2/T3/T5):
    • 必须把"经过 label-master 认证的全部候选"按 pattern 分组、每条一行(紧凑表)贴到同一条 ask 消息体里,不是只给 CSV 路径或飞书链接,也不要分段连发——一段全铺,让用户一次滚完
    • 不允许抽样、不允许"前 N 条样例"、不允许"代表 case"——抽样让用户看不到边界外的长尾,决策无意义
    • 每条至少含:query / old_label / 推荐标签 / 理由(≤40字)H1(改标)类必含 file:lineH2(仿写)类必含 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_.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。