Merge branch 'wsh_dev' into 'main'

fix: pipeline排序/runDic映射/label-master repeatable/达标优先级

See merge request wuyang6/zk-data-agent!4
This commit is contained in:
王森浩
2026-05-26 14:12:04 +08:00
7 changed files with 103 additions and 32 deletions
+50 -16
View File
@@ -1768,12 +1768,11 @@ def create_app(state: AgentState) -> FastAPI:
if isinstance(run_id, str) and run_id.strip(): if isinstance(run_id, str) and run_id.strip():
rid = run_id.strip() rid = run_id.strip()
per_round = _resolve_run_dic_per_round(state_entries) per_round = _resolve_run_dic_per_round(state_entries)
# augment 处理的是「上一轮评测的错例」,产物按 R{n-1} 的 runDic 命名 # augment 产物的 runDic = resolve_run_ids.sh 在当轮 eval 落盘后取到的
# data_clean_<R{n-1}>/、augment_<R{n-1}>.jsonl ...),不是本轮 eval workflow # max(已落盘 workflow) = 当轮 eval 自身的 workflow id,即 per_round[R{n}]
# (不是 R{n-1},脚本取的是 eval 之后的 max,此时 max 已经包含当轮 eval)
if step == 'augment': if step == 'augment':
m = re.match(r'^[Rr](\d+)$', rid) pass # 直接走下面 per_round.get(rid) 取本轮 runDic
if m and int(m.group(1)) >= 1:
run_dic = per_round.get(f'R{int(m.group(1)) - 1}')
if run_dic is None: if run_dic is None:
run_dic = per_round.get(rid) run_dic = per_round.get(rid)
if run_dic is None: if run_dic is None:
@@ -6395,9 +6394,20 @@ def _build_pipeline_items(
for c in sec['cards']: for c in sec['cards']:
c.pop('_order', None) c.pop('_order', None)
# 4. Build round items in display order. # 3b. If a gate is running for a given run_id, downgrade running cards
# Order: by run_index ascending, then within same round: train before analysis. # in that run to 'waiting' (the step is blocked on human review).
section_order = {'baseline': 0, 'train': 0, 'analysis': 1} runs_with_gate_running: set[str] = set()
for entry in latest_gate_by_key.values():
if entry.get('status') == 'running':
runs_with_gate_running.add(entry['_run_id'])
if runs_with_gate_running:
for sec in sections.values():
if sec['run_id'] in runs_with_gate_running:
for card in sec['cards']:
if card['status'] == 'running':
card['status'] = 'waiting'
# 4. Build round items in display order (chronological by first_ts).
section_items: list[dict[str, Any]] = [] section_items: list[dict[str, Any]] = []
for (run_id, section_type), sec in sections.items(): for (run_id, section_type), sec in sections.items():
run_index = _parse_run_index(run_id) run_index = _parse_run_index(run_id)
@@ -6415,7 +6425,7 @@ def _build_pipeline_items(
'description': _SECTION_DESCRIPTIONS[section_type], 'description': _SECTION_DESCRIPTIONS[section_type],
'status': round_status, 'status': round_status,
'cards': sec['cards'], 'cards': sec['cards'],
'_order_key': (run_index, section_order.get(section_type, 9), sec.get('first_ts', '')), '_order_key': sec.get('first_ts', ''),
}) })
# 5. Build gate items. # 5. Build gate items.
@@ -6499,7 +6509,9 @@ def _compute_in_flight(
seen_keys: set[str] = set() seen_keys: set[str] = set()
for idx in range(len(state_entries) - 1, -1, -1): for idx in range(len(state_entries) - 1, -1, -1):
entry = state_entries[idx] entry = state_entries[idx]
if entry.get('kpi') or entry.get('log'): if entry.get('kpi'):
continue
if entry.get('log') and not entry.get('step'):
continue continue
step = entry.get('step') step = entry.get('step')
if not isinstance(step, str) or not step: if not isinstance(step, str) or not step:
@@ -6531,9 +6543,22 @@ def _compute_in_flight(
if not target_run_id or not target_section: if not target_run_id or not target_section:
return None return None
# Check if a gate (human-check/human-review) is running for the target run_id
gate_running_for_run = False
for entry in state_entries:
step = entry.get('step')
if not isinstance(step, str) or not step:
continue
if _STEP_KIND.get(step) == 'gate' and entry.get('status') == 'running':
entry_run_id = entry.get('run_id')
if entry_run_id == target_run_id:
gate_running_for_run = True
latest_per_step: dict[str, dict[str, Any]] = {} latest_per_step: dict[str, dict[str, Any]] = {}
for idx, entry in enumerate(state_entries): for idx, entry in enumerate(state_entries):
if entry.get('kpi') or entry.get('log'): if entry.get('kpi'):
continue
if entry.get('log') and not entry.get('step'):
continue continue
step = entry.get('step') step = entry.get('step')
if not isinstance(step, str) or not step: if not isinstance(step, str) or not step:
@@ -6559,6 +6584,8 @@ def _compute_in_flight(
status = entry.get('status') status = entry.get('status')
if status not in ('complete', 'running'): if status not in ('complete', 'running'):
continue continue
if status == 'running' and gate_running_for_run:
status = 'waiting'
meta = _CARD_META.get((step, target_section), { meta = _CARD_META.get((step, target_section), {
'icon': 'clock', 'icon': 'clock',
'title': step, 'title': step,
@@ -6634,7 +6661,9 @@ def _apply_program_state(
# whenever a running section is hidden. # whenever a running section is hidden.
latest_per_step: dict[str, str] = {} latest_per_step: dict[str, str] = {}
for entry in state_entries: for entry in state_entries:
if entry.get('kpi') or entry.get('log'): if entry.get('kpi'):
continue
if entry.get('log') and not entry.get('step'):
continue continue
step = entry.get('step') step = entry.get('step')
status = entry.get('status') status = entry.get('status')
@@ -6655,14 +6684,18 @@ def _apply_program_state(
statuses = list(latest_per_step.values()) statuses = list(latest_per_step.values())
if not statuses: if not statuses:
payload['status']['state'] = 'pending' payload['status']['state'] = 'pending'
elif non_gate_running: elif non_gate_running and not gate_running:
payload['status']['state'] = 'running' payload['status']['state'] = 'running'
elif gate_running: elif gate_running:
# Only HiTL gates are 'running' — agent has handed control back to the # HiTL gate is running — agent has handed control back to the user.
# user. Surface as 'waiting' so the header pill says "等待人工" instead # Even if a parent step (e.g. augment) is still technically 'running',
# of the spinning "Running" badge. # the gate blocks progress. Surface as 'waiting' so the header pill
# says "等待人工" instead of the spinning "Running" badge.
payload['status']['state'] = 'waiting' payload['status']['state'] = 'waiting'
elif all(s == 'complete' for s in statuses): elif all(s == 'complete' for s in statuses):
if run_active:
payload['status']['state'] = 'running'
else:
payload['status']['state'] = 'complete' payload['status']['state'] = 'complete'
elif 'complete' in statuses: elif 'complete' in statuses:
payload['status']['state'] = 'running' payload['status']['state'] = 'running'
@@ -6702,6 +6735,7 @@ def _apply_program_state(
and payload['status']['state'] == 'running' and payload['status']['state'] == 'running'
and running_steps and running_steps
and not all_watched and not all_watched
and not gate_running
): ):
session_failure_status = 'failed' session_failure_status = 'failed'
+1
View File
@@ -4,6 +4,7 @@ description: 标签大师:读取和使用中控标签知识库,辅助标签
when_to_use: 当用户要求判断 query 应该归属哪个标签、解释标签边界、校验数据生成目标中的 target、整理标签知识或分析标签混淆时使用。 when_to_use: 当用户要求判断 query 应该归属哪个标签、解释标签边界、校验数据生成目标中的 target、整理标签知识或分析标签混淆时使用。
aliases: label-knowledge, 标签大师, 标签知识, label, 标签体系 aliases: label-knowledge, 标签大师, 标签知识, label, 标签体系
allowed_tools: read_file, write_file, edit_file, grep_search, glob_search, ask_user_question, python_exec, bash allowed_tools: read_file, write_file, edit_file, grep_search, glob_search, ask_user_question, python_exec, bash
repeatable: true
--- ---
使用这个 skill 作为“标签知识理解和标签边界分析”的入口。它不直接把标签判断收敛成一个黑盒分类工具,而是指导 Agent 逐步阅读知识、比较候选、解释依据,并在不确定时向用户确认。 使用这个 skill 作为“标签知识理解和标签边界分析”的入口。它不直接把标签判断收敛成一个黑盒分类工具,而是指导 Agent 逐步阅读知识、比较候选、解释依据,并在不确定时向用户确认。
+13 -3
View File
@@ -65,6 +65,13 @@ when_to_use: |
- [ ] **`step:"hypothesis"` entry 写在当前 roundR{n})名下**——不要写成 R{n+1}。后端把 hypothesis 归类为 analysis 类,是 R{n}·Baseline / R{n}·Analysis 的最后一张卡,不是 R{n+1}·Train 的开头。这样 gateHuman Check / Review)会插在 hypothesis 卡之后、R{n+1}·Train 之前,**用户看到假设内容再拍板是否进 train**。 - [ ] **`step:"hypothesis"` entry 写在当前 roundR{n})名下**——不要写成 R{n+1}。后端把 hypothesis 归类为 analysis 类,是 R{n}·Baseline / R{n}·Analysis 的最后一张卡,不是 R{n+1}·Train 的开头。这样 gateHuman 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 拼错前端按缺失处理。 - [ ] **`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 边界(**CRITICALhypothesis=complete 后禁止空手结束 turn**
- [ ] **`hypothesis=complete` 写入 program-state.jsonl 之后,当前 turn 绝对不允许结束**——必须在同一轮里做下面两件之一(二选一,不能都不做):
- (A) 如果命中 HiTL 信号(§HiTL 阈值):写 `human-check` gate entryrunning)→ 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 都适用) ### Step 1 → Step 3 边界(**NEVER STOP 硬连接**R0 / R1+ regression 都适用)
- [ ] dist-analysis 算完 deltanew_fail / new_fix / persistent / 子集分布)那一刻起,**同一轮 bash 不许结束**:紧接着写 `results/workflow<runDic>.md`(包含完整指标表 + 病灶定位 + 假设 + 回滚/继续建议)→ append `iteration_log.jsonl` R{n} entry`results` 字段填本轮 metric`hypothesis` 字段填下一动作) - [ ] dist-analysis 算完 deltanew_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 给人回滚选项**。文件先落、聊天再发——顺序不能反。 - [ ] **regression 场景同样适用**:哪怕 R1 出现导航bvt 纯劣化、可聊可控大幅 -25 这种"必须回滚"信号,**先把分析落到 workflow<runDic>.md 和 iteration_log,再用 chat reply 给人回滚选项**。文件先落、聊天再发——顺序不能反。
@@ -81,6 +88,7 @@ when_to_use: |
- [ ] §4.0.1 Step C:备份 `.bak` 文件 - [ ] §4.0.1 Step C:备份 `.bak` 文件
- [ ] §4.1 输入准备 / §4.2 GPT 调用 / §4.3 sanity check / §4.4 写入 - [ ] §4.1 输入准备 / §4.2 GPT 调用 / §4.3 sanity check / §4.4 写入
- [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` 只跑层 1(格式)—— 语义已在 §4.0.1 Step A.5 完成;H2 `augment_<runDic>.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),**逐条 1:1 全量覆盖**(`augment` 多少行 `label_master_review.jsonl` H2 部分就多少行,禁止抽样 / spot-check / X/X pass 外推),verdict 写 `results/data_clean_<runDic>/label_master_review.jsonl`,**不通过必须 = 0**(任何不通过必须修正后重新 review 直到全 pass 才能进 Step 5 - [ ] **§4.5 label-master 标签复核(落盘后必做,强制)**:H1 `modified_samples.jsonl` 只跑层 1(格式)—— 语义已在 §4.0.1 Step A.5 完成;H2 `augment_<runDic>.jsonl` 跑两层(`validate_label_output.py` 格式 + Skill 调用 `label-master` 语义),**逐条 1:1 全量覆盖**(`augment` 多少行 `label_master_review.jsonl` H2 部分就多少行,禁止抽样 / spot-check / X/X pass 外推),verdict 写 `results/data_clean_<runDic>/label_master_review.jsonl`,**不通过必须 = 0**(任何不通过必须修正后重新 review 直到全 pass 才能进 Step 5
- [ ] **§4.5 层 2 语义复核必须调用 `Skill(skill="label-master")`,禁止正则/规则脚本替代**:层 2 的本质是"用 label-master 知识体系对每条 (query, label) 做独立语义判定"。label-master 已标记 `repeatable: true`,可在同一 run 内多次调用。**禁止**:纯正则/关键词匹配脚本、"target 都一样所以直接 pass"逻辑、批量写 pass 不看 query 内容。已踩坑:R1 agent 写了个正则脚本充当"layer 2",完全没走语义判断。
### Step 4 → Step 5 边界(**NEVER STOP 硬连接**,反复踩坑) ### Step 4 → Step 5 边界(**NEVER STOP 硬连接**,反复踩坑)
- [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft.sh` → 挂 watcher(评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起,不要在 SFT bg 里串接评测) - [ ] 写完 `augment=complete` 那一刻,**同一轮 bash 不许结束**:紧接着跑 §4.5 label-master 复核 → 写 `sft=running` → 调 `submit_sft.sh` → 挂 watcher(评测在 SFT `_SUCCESS` 落盘后的下一轮单独用 `submit_cml_eval.sh` 起,不要在 SFT bg 里串接评测)
@@ -418,9 +426,11 @@ echo '{"log":{"ts":"11:55:10","iter":"R1","text":"Step 1 dist-analysis: 写 work
## 达标条件 ## 达标条件
- **需求集合** ≥ 95%(最高优先级) - **需求集合(目标集)** ≥ 95%**最高优先级,一切以此为准**
- **大盘集(车载)** 降幅 ≤ 0.3% - **大盘集(车载)** 降幅 ≤ 0.3%(次要,目标集未达 95% 前不因大盘微降而回滚/停训)
- **specific test** 降幅 ≤ 1% - **specific test** 降幅 ≤ 1%(次要,同上)
⚠️ **优先级铁律**:目标集上 95% 是主线任务。只要目标集还没到 95%,其他集合的轻微下降(大盘 ≤1%、specific ≤2%)**不构成回滚理由**,继续迭代。只有目标集已达标后,才需要关注并修复其他集合的 regression。
## 主流程 ## 主流程
+31 -8
View File
@@ -178,13 +178,17 @@ for k in prev:
3. **specific test** 3. **specific test**
**达标条件** **达标条件**
- 需求集合 ≥ 95% - 需求集合(目标集) ≥ 95%**最高优先级**
- 大盘集(车载)降幅 ≤ 0.3% - 大盘集(车载)降幅 ≤ 0.3%(次要)
- specific test 降幅 ≤ 1% - specific test 降幅 ≤ 1%(次要)
⚠️ **优先级铁律**:目标集上 95% 是主线任务。只要目标集还没到 95%,其他集合的轻微下降(大盘 ≤1%、specific ≤2%)**不构成回滚理由**,应继续迭代优化目标集。只有目标集已达 95% 后,才需要关注并修复其他集合的 regression。
**决策** **决策**
- 全部达标 → 部署(`python modules/cloudml_deploy.py <rl_checkpoint>`),记录,循环结束 - 全部达标 → 部署(`python modules/cloudml_deploy.py <rl_checkpoint>`),记录,循环结束
- 达标 → 在 Step 1 内继续做问题分析(见下文),**分析顺序:需求集合 → 车载大盘 → specific test** - 目标集未达标 → 继续迭代(即使其他集合有轻微下降也不回滚)
- 目标集已达标但其他集合超限 → 微调修复其他集合
- 分析顺序:**需求集合 → 车载大盘 → specific test**
> **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接在 Step 1 内分析基准模型的错误分布,为后续迭代建立对照基准。 > **首次评测差异**:由于新旧模型相同,"降幅"和"vs 基线"无意义(均为 0)。此轮只记录各集合的**绝对准确率**作为 baseline,不做达标/不达标判定,不触发部署。直接在 Step 1 内分析基准模型的错误分布,为后续迭代建立对照基准。
@@ -655,6 +659,12 @@ for r in b_rows:
> 若 Step 1 显示全部达标 → 跳过 Step 3–5,直接部署。 > 若 Step 1 显示全部达标 → 跳过 Step 3–5,直接部署。
> ⛔ **CRITICAL: hypothesis=complete 后禁止空手结束 turn**。写完 hypothesis 到 program-state.jsonl 之后,当前 turn 内必须做下面两件之一(不能都不做就结束):
> - 命中 HiTL → 写 `human-check` gate entry (running) → chat reply 给用户选项
> - 不命中 HiTL → 写 `augment=running` → 开始 Step 4
>
> **"turn 预算不够"不是合法理由**——没有自动 wake 机制,turn 结束 = session 永久停止。已踩坑:agent 在 R0 hypothesis=complete 后误判"turn 用满"而空手结束,用户看到 pipeline 卡在 hypothesis 不动、无任何后续(session `qf1ni1jh`25/50 turns 实际只用一半)。
### 4. 数据生成(GPT-5.4 从 badcase 增强) ### 4. 数据生成(GPT-5.4 从 badcase 增强)
**只在 §2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由: **只在 §2.4 归因为 `data` 时做。** 其他归因直接跳过 Step 4,按下表路由:
@@ -743,18 +753,19 @@ with open(out_csv, 'w', encoding='utf-8-sig') as fp:
候选阶段(246 条等量级)的 CSV 必须写到 `output/relabel_candidates_<runDic>.csv` 让分析卡片可读;确认修改阶段(用户审核后落盘的 135 条)写入 `results/data_clean_<runDic>/modified_samples.jsonl` 让数据增强卡片可读。**不要混落**——把候选 CSV 写到 `/tmp/` 或 `data_clean_/` 都会让 UI 看不到。 候选阶段(246 条等量级)的 CSV 必须写到 `output/relabel_candidates_<runDic>.csv` 让分析卡片可读;确认修改阶段(用户审核后落盘的 135 条)写入 `results/data_clean_<runDic>/modified_samples.jsonl` 让数据增强卡片可读。**不要混落**——把候选 CSV 写到 `/tmp/` 或 `data_clean_/` 都会让 UI 看不到。
> **runDic 命名语义(backend 已对齐,改 backend 时勿 revert**augment 类产物(`data_clean_<N>/`、`augment_raw/augment_<N>_raw.jsonl`、`zk_intent/augment_<N>.jsonl`、`label_master_review.jsonl`)的 `<N>` 永远是**「源轮次的 runDic」**,也就是 `R{n-1}.runDic`——agent 在 R{n} 跑 augment 时处理的是 R{n-1} 评测落地的错例,文件命名沿用 R{n-1} 的 workflow id。Backend step-detail 端点 (`server.py` 内 `_step_artifact_paths('augment', ...)` 调用方) 在 `step=='augment'` 时也是查 `R{n-1}` 的 runDic,跟落盘命名一一对应。其他 stepcml / dist-analysis / hypothesis / gold-drift)的产物按本轮自己的 runDic 命名,没有偏移。**不要把 augment 产物改名成 R{n} 的 runDic**——会同时打破历史归档和 backend 解析 > **runDic 命名语义(backend 已对齐)**augment 类产物(`data_clean_<N>/`、`augment_raw/augment_<N>_raw.jsonl`、`zk_intent/augment_<N>.jsonl`、`label_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。规则简单到没必要靠模型: > ⚠️ **runDic 不许手算**augment 落盘、SFT yaml、归档目录、cml workflow run 用的 runDic **必须**从 `scripts/resolve_run_ids.sh` 取,不准 agent 自己 `ls workflow5 | tail -1` 然后心算 +1。规则简单到没必要靠模型:
> >
> ```bash > ```bash
> eval "$(./scripts/resolve_run_ids.sh)" > eval "$(./scripts/resolve_run_ids.sh)"
> # 现在 shell 里有: > # 现在 shell 里有:
> # $SFT_RUNDIC = R{n-1}.runDic augment 落盘 / yaml / 归档全用这个,不 +1) > # $SFT_RUNDIC = max(已落盘 workflow) = 当轮 eval 的 workflow id
> # $EVAL_RUNDIC = R{n-1}.runDic + 1 (仅 cml workflow run 时使用,唯一一次 +1) > # augment 落盘 / yaml / 归档全用这个
> # $EVAL_RUNDIC = SFT_RUNDIC + 1(仅下一轮 cml workflow run 提交时使用)
> ``` > ```
> >
> 已踩坑:R2 augment 把 `data_clean_<N>/` 写成了 `EVAL_RUNDIC`(多 +1 一次),跳过了 R1.runDic 整段命名空间,前端 augment 卡片直接读不到。**写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。 > **写 augment / SFT / 归档脚本时一律用 `$SFT_RUNDIC`,不要写 `${RUNDIC}` 也不要写 `${EVAL_RUNDIC}`**。
> >
> 🚨 **调用时序(CRITICAL**`resolve_run_ids.sh` **只能在当轮 CML eval 的 `lark_template.json` 已落盘之后调用**。禁止在 eval 提交前或 watcher 等待期间预先调用——此时 max workflow 仍是上一轮的值,会导致 SFT_RUNDIC 偏移 -1(已踩坑:R0 workflow17823 评测中提前 resolve 得到 17822augment 文件全部错位)。正确时序:`cml step=complete` → `resolve_run_ids.sh` → augment/SFT。每次进入 augment/SFT 步骤前**必须重新调用**,不许复用之前缓存的值。 > 🚨 **调用时序(CRITICAL**`resolve_run_ids.sh` **只能在当轮 CML eval 的 `lark_template.json` 已落盘之后调用**。禁止在 eval 提交前或 watcher 等待期间预先调用——此时 max workflow 仍是上一轮的值,会导致 SFT_RUNDIC 偏移 -1(已踩坑:R0 workflow17823 评测中提前 resolve 得到 17822augment 文件全部错位)。正确时序:`cml step=complete` → `resolve_run_ids.sh` → augment/SFT。每次进入 augment/SFT 步骤前**必须重新调用**,不许复用之前缓存的值。
@@ -1262,6 +1273,8 @@ python skills/label-master/scripts/validate_label_output.py \
**层 2:语义复核(Skill 调用 label-master Agent,仅 H2** **层 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,必须在此补一次语义判定: 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 退回。 🚨 **覆盖率硬规则**`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 退回。
@@ -1485,6 +1498,16 @@ python prepare_and_train_sft.py prepare --regen_from_csv --output_dir ./sft_data
> 验证集文件名不能含 `valid`/`test`/`eval`HF datasets 会推断错误的 split),脚本自动重命名为 `part-0.jsonl`。 > 验证集文件名不能含 `valid`/`test`/`eval`HF datasets 会推断错误的 split),脚本自动重命名为 `part-0.jsonl`。
⚠️ **每轮 SFT 前必须在 log 中展示训练数据组成**
`prepare_and_train_sft.py` 会 glob 目录下**所有** `augment_*.jsonl`。agent 在每轮 SFT 前必须在 program-state log 中列出最终参与训练的文件及行数:
```jsonl
{"log":{"ts":"...","iter":"R3","text":"SFT 数据组成: all_train.jsonl(35379) + augment_17829.jsonl(210) = 35589 条 | 排除: augment_17828.jsonl(150) rename .pre_R3R2 pattern 导致导航 bvt 劣化)"}}
```
必须展示:(1) 参与训练的所有文件及行数 (2) 被排除的旧轮文件及排除原因(如果有)。
#### 5.2 启动训练(cml 任务,R29 起统一走这条路径) #### 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 卡,本地零占用。 R23-R28 期间用本地 `nohup python3 prepare_and_train_sft.py train ...` 启动,会因登出/网络/会话退出而中断,并占用本地工作机 8 卡 GPU。**R29 起统一改为 cml custom_train submit 提交训练任务**,由 CloudML 调度到 `bj-nlp` 队列的 h20-96g 8 卡,本地零占用。
@@ -1,11 +1,12 @@
#!/bin/bash #!/bin/bash
# resolve_run_ids.sh —— SFT/EVAL runDic 单一可信来源 # resolve_run_ids.sh —— SFT/EVAL runDic 单一可信来源
# #
# 规则(与 program.md §746 + §5.2 对齐): # 规则(与 program.md §756 对齐):
# SFT_RUNDIC = workflow5/ 下「已落盘」(含 metric_diff/lark_template.json)的最大 runDic # SFT_RUNDIC = workflow5/ 下「已落盘」(含 metric_diff/lark_template.json)的最大 runDic
# 即 R{n-1}.runDic —— augment 落盘、SFT yaml、modified_samples 归档全用这个值 # = 当轮 eval 自身的 workflow id(因为 resolve 在 eval 落盘后调用)
# augment 落盘、SFT yaml、modified_samples 归档全用这个值
# EVAL_RUNDIC = SFT_RUNDIC + 1 # EVAL_RUNDIC = SFT_RUNDIC + 1
# 仅 cml workflow run 提交本轮评测时使用 # 仅下一轮 cml workflow run 提交时使用
# #
# 用法: # 用法:
# eval "$(./scripts/resolve_run_ids.sh)" # eval "$(./scripts/resolve_run_ids.sh)"
+1 -1
View File
@@ -2681,7 +2681,7 @@ class LocalCodingAgent:
bundled = find_bundled_skill(skill_name, cwd=self.runtime_config.cwd) bundled = find_bundled_skill(skill_name, cwd=self.runtime_config.cwd)
if bundled is not None: if bundled is not None:
skill_key = bundled.name.strip().lower() skill_key = bundled.name.strip().lower()
if active_skill_names is not None and skill_key in active_skill_names: if active_skill_names is not None and skill_key in active_skill_names and not bundled.repeatable:
duplicate_count = 1 duplicate_count = 1
if duplicate_skill_counts is not None: if duplicate_skill_counts is not None:
duplicate_count = duplicate_skill_counts.get(skill_key, 0) + 1 duplicate_count = duplicate_skill_counts.get(skill_key, 0) + 1
+2
View File
@@ -35,6 +35,7 @@ class BundledSkill:
aliases: tuple[str, ...] = () aliases: tuple[str, ...] = ()
allowed_tools: tuple[str, ...] = () allowed_tools: tuple[str, ...] = ()
user_invocable: bool = True user_invocable: bool = True
repeatable: bool = False
source: str = 'python' source: str = 'python'
path: str | None = None path: str | None = None
get_prompt: Callable[['LocalCodingAgent', str], str] = lambda _a, _args: '' get_prompt: Callable[['LocalCodingAgent', str], str] = lambda _a, _args: ''
@@ -190,6 +191,7 @@ def _load_directory_skill(path: Path, *, source: str = 'directory') -> BundledSk
aliases=_split_csv(metadata.get('aliases')), aliases=_split_csv(metadata.get('aliases')),
allowed_tools=_split_csv(metadata.get('allowed_tools')), allowed_tools=_split_csv(metadata.get('allowed_tools')),
user_invocable=_parse_bool(metadata.get('user_invocable'), default=True), user_invocable=_parse_bool(metadata.get('user_invocable'), default=True),
repeatable=_parse_bool(metadata.get('repeatable'), default=False),
source=source, source=source,
path=str(path), path=str(path),
get_prompt=_directory_skill_prompt(body.strip()), get_prompt=_directory_skill_prompt(body.strip()),