# 训练前缺陷修复设计（video-split → Video-MME 900 自进化训练）

> 2026-07-16。两轮审查（第一轮接线预检 + 第二轮 8 维度多代理审查：**19 条确认 / 4 条存疑 / 12 条误报**）。本设计覆盖：19 条确认缺陷 + 4 条存疑中采纳 2 条 + 接线缺口 2 项 + 切分层优化 3 项，逐条映射见 §2.5 覆盖矩阵。
> **目标**：让 video-split 冻结切分驱动的 Video-MME 900 题自进化训练**一次跑出有效结果**（不崩、不静默失效、信号不被污染）。

## 1. 背景与问题总览

自进化训练对标 PyTorch 训练循环（出题=DataLoader / 推理=forward / 诊断=backward / 进化=optimizer.step）。审查发现三类根问题：

| 类别 | 代表 | 后果 |
|------|------|------|
| **进化引擎哑火** | 5 个 evolve/momentum 模板 TRM4→TRM5 迁移遗漏，`else ""` 静默兜底 | 3 epochs 跑完但 skills 零进化，best 恒 v1 |
| **诊断链路坍缩** | traces 表从不写入（TracePlugin 未迁移） | 诊断瀑布拿空轨迹，算法保真 #7 失效 |
| **崩溃与信号污染** | 微型题型断言崩、prediction 非标量击穿、缓存跨 epoch 重放、SSE 截断毒化等 | 训练中途崩溃或统计信号失真 |

叠加**接线缺口**：冻结 `pools.json` 无路径进入训练 workspace（`--fresh` 从 seed 重建不带切分），直接训练会静默丢弃标定成果、按 unit 重切。

## 2. 设计目标与非目标

**目标**：修复 §2.5 矩阵所列全部缺陷（19 确认 + 2 存疑采纳）+ 接线；切分层做三处优化（可训练性门槛、val_ratio、tier 感知分配）提升信号质量；产出零参可复现的训练入口。

**非目标**：不改 gate/EMA 核心数学（算法保真 #4/#5）；不实现 epoch 层 per-skill best 混搭（见 §9 future work）；不引入向后兼容（P4 显式优于隐式，直接改）。

## 2.5 缺陷覆盖矩阵（可追溯性）

原始编号取自第二轮工作流报告（`research-wiki/reviews/2026-07-16-preflight-train-review.md`）与第一轮接线预检。状态：C=确认 / P=存疑采纳 / W=接线（已知缺口）。

| 设计编号 | 原始位置 | 状态 | 修法节 |
|---------|---------|------|--------|
| P0-1 | runner.py:2272 evolve 模板缺失 | C | §4 |
| P0-6 | momentum.py:151 slow_momentum.md 缺失 | C | §4 |
| P0-2 | inference.py:462 traces 未写 | C | §6 |
| P0-3 | validate.py:682 + gate.py:99 + runner.py:2044 微型题型三连雷 | C | §6 |
| P0-4 | inference.py:423 prediction 非标量击穿 | C | §6 |
| P0-5 | runner.py:316 early_stop step 单位 | C | §6 |
| P1-1 | main.py:93 缓存跨 epoch 重放 + TTL | C | §8 |
| P1-2 | llm.py:116 SSE 截断毒化 | C | §8 |
| P1-3 | llm.py:182 断连不重试 | C | §8 |
| P1-4 | validate.py:304 基线臂 INFRA 污染 | C | §8 |
| P1-5 | diagnose.py:2194 + runner.py:1019 降级误入 defect | C | §6 |
| P1-6 | patch.py:343 冻结区跨度 | C | §7 |
| P1-7 | workspace.py:282 manifest 非原子写 | C | §8 |
| P2-1 | video_split_cli.py:557 冻结产物无覆盖保护 | C | §5.2 |
| P2-2 | log.py:77 基线元数据被改写 | C | §8 |
| P2-3 | diagnose.py:2081 + inference.py:461 重复行双计 | C+P | §6（step DELETE） |
| P2-4 | diagnose.py:2194（离线）uncertain 永久降级 | C | §8（--retry-uncertain） |
| P2-5 | runner.py:1444 dual_metric 口径歧义 | P | §8 |
| P2-6 | breaker.py:36 熔断半开烧穿 | P | §8 |
| 慢更新非幂等 | 第一轮已知 #5 | W | §6（gate_epoch_observed 立即落盘） |
| 接线-1 | 冻结 pools 无路径进 workspace（已知 #1） | W | §5.3 |
| 接线-2 | global 加载零一致性校验（已知 #2） | W | §5.3 |

**有意合并/不单列的项**（说明去向，避免"静默漏修"）：

| 第一轮原始项 | 处置 |
|-------------|------|
| gate_ladder.py:93 strict=False 缺题静默当错题 | 由 §6 可训练性预检消除主场景（缺题类被剔除）；残余以 fail-loud 补强（预检后 gate 建立时断言 baseline 覆盖全部 ladder 单元） |
| batching.py:200 缺 correctness 静默丢弃 | 同上，预检保证 diag 池题全在 correctness 内；保留 fail-loud（缺 correctness 即报错而非丢弃） |
| resume 硬依赖 checkpoint（已知 #8） | 本轮为 fresh 训练，seed 携带 pools 后无需 resume 建池；不改 resume 语义 |
| baseline_diagnosis 断点续跑粒度（存疑，非采纳） | 仅修 docstring 表述（§8 末），行为不改（Redis 缓存已缓解重烧） |

## 3. 架构：四工作包按依赖序

```mermaid
graph LR
    WP1[WP1 资产迁移<br/>模板+fail-loud+死字段] --> WP3[WP3 训练循环与进化引擎]
    WP2[WP2 切分与接线<br/>产出训练输入] --> RUN[训练启动]
    WP3 --> RUN
    WP4[WP4 韧性与持久化] --> RUN
    WP1 -.集成测试依赖.-> WP3
```

WP1 零风险纯搬运先解锁引擎；WP2/WP4 与 WP3 无代码交集可并行；WP3 主体修复依赖 WP1 模板做集成测试。

## 4. WP1 —— 资产迁移（修 P0-1 / P0-6）

从 `/home/iomgaa/Projects/Video-Tree-TRM4/prompts/` 拷 5 文件到 TRM5 根 `prompts/`：`evolve_skill/system/tool/rank.md`、`slow_momentum.md`（进化引导 prompt = 引擎一部分，不参与版本化进化）。

- **迁移验收（强制）**：逐个核对模板输出契约与 TRM5 解析代码期望——`evolve_*` 的 `{suggestions, edits}` 结构 vs `_parse_llm_json`/`_apply_one`；`slow_momentum.md` vs `run_slow_momentum`。TRM4→TRM5 代码有漂移，裸拷贝不核对 = 新 bug。
- **加载器 fail-loud**：`_load_evolve_prompts`/`_load_diagnose_prompts` 的 `else ""` 改 `FileNotFoundError`（列缺失文件名）。
- **死字段清理**：删 `EvolvePrompts.consolidate_system`（consolidate_appendix 用内联 prompt）、`DiagnosePrompts.span_eval_user`（无消费点）及对应 runner 加载行。

## 5. WP2 —— 切分与接线

### 5.1 tier 感知切分（`split_selection.py` / `build_split.py`）

trainval 内部视频组分配从"correctness 分层随机"升级为 **tier 感知确定性贪心**（纯函数、固定 seed）：

```
输入: trainval 视频组集合 G（每组: t2_count/t1_count/wrong_count/question_count，
      tier 来自 baseline_diagnosis 按 fingerprint 读）；val_ratio；val_wrong_min
目标: 最大化 diag 池 T2 捕获，同时满足 val 功效
约束: ①视频原子（组内题同进同出） ②val 题数 ≈ val_ratio×trainval题数 ③val 错题 ≥ val_wrong_min

阶段1 初分（单趟，O(n log n)）:
  按 (t2_count desc, wrong_count desc, video_id) 排序 G
  依次装入 diag，累计题数达 (1-val_ratio)×总题数 即停 → 余下全部进 val
阶段2 功效修复（单调收敛，每组至多移动一次）:
  while val 错题 < val_wrong_min:
    从 diag 中挑「wrong_count>0 且 t2_count 最小」的组移入 val   # 严格减少 diag 错题冗余
    该组标记 moved，后续不再参与挑选                          # 单调变元：候选集严格缩小
    if 无 moved=false 且 wrong_count>0 的组可挑 → raise InfeasibleSplitError
  # 终止性：每轮候选集 |{未 moved 且 wrong>0}| 至少减 1，有限步内收敛或 fail loud
```

与既有 video-split 设计（`2026-07-15-results-driven-video-split-design.md §8`）的口径变更：**保留** correctness 分层的本质（阶段2 只在 wrong_count>0 的组间挪动，保证两池都有对/错题）、`eval_min_per_class`（由 §6 预检独立强制）、McNemar 功效（`val_wrong_min≈20` 硬约束不变）；**替代**"随机分层"为"tier 优先确定性贪心"（新增 T2 捕获目标 + 固定 seed 可复现）。

配置：`val_ratio` 作用域 = **trainval 池（当前 300 题）内部**的 val 题数占比，`0.3 → 0.4`。数字推算基于当前冻结切分统计（trainval 错题 93 = val 29 + diag 64）：val 错题 ≈ ⌈93×0.4⌉ = 38、diag 错题 ≈ ⌊93×0.6⌋ = 55~56（tier 感知使 T2 尽量留 diag，实际错题落点略有偏移，以重切后统计为准）。甜点判断：val 38 让整包终审更抗噪、diag 56 进化信号仍过剩；0.5 过冲（每类 3 epochs 仅 ~4 次进化机会，迭代受损）。`val_ratio`、`val_wrong_min` 作为**数值参数进科研 YAML + run 快照**（会被扫动/对比）；tier 感知**策略**固定不加 on/off 开关（无扫动需求，YAGNI）。重切零 LLM 成本（诊断已按指纹落库）。

### 5.2 冻结产物覆盖保护（修 P2-1）

`build_split` 冻结前检查产物存在性：存在且内容指纹不同 → 报错并提示 `--force`（CLI 补上此缺失参数）；`--force` 时旧产物重命名 `pools.json.bak.<旧指纹前8>` 再写。

### 5.3 seed 携带切分（接线核心）

| 项 | 设计 |
|----|------|
| `init_seed` | 新增可选 `pools_json`/`split_manifest`，提供则拷入 seed 目录 |
| `init_workspace_from_seed` | seed 目录有 `pools.json` 则连 manifest 拷入 workspace（拷 baseline.db 之后） |
| `build_or_load_pools`（global 补校验，修已知 #2） | 加载冻结 pools 强校验 `baseline_run_id` 匹配 seed；有 manifest 时校验 `sha256(pools.json)==manifest.pools_sha256` |
| 新 seed `adhoc-baseline` | `extract_run_db` 加 `dedupe_per_question`（每 qid 按 rowid 取首行，902→900 对齐 canonical）+ `init_seed(baseline_run_id='infer_adhoc', pools_json=…)` |

seed 最终形态：`seed.json / baseline.db(900) / pools.json / split_manifest.json / skills/v1 / prompts/v1`。

## 6. WP3 —— 训练循环与进化引擎

| # | 缺陷 | 修法 |
|---|------|------|
| P0-3 | 微型题型三连雷 | `train()` 加**可训练性预检**（纯函数）：题型 `n_val<eval_min_per_class(2)` 或 `n_units<trainable_min_units(8)` → 从 diag/val + gate task_types 剔除并打印清单；断言/空阶梯保留作纵深防御；test 池不过滤 |
| P0-4 | prediction 非标量击穿 | 落库前 `_to_text_field` 归一化；`log.insert` 移入单题 try 块，绑定异常记 error 不击穿 gather |
| P0-5 | early_stop 单位错误 | step→**epoch** 计数（`epochs_since_best_improved`），config 注释对齐，checkpoint 字段同步改名 |
| P0-2 | traces 空 | 复用离线管线 `StepsJsonRunLog` 适配器包装训练 RunLog，`get_traces` 空时从 steps_json 转换，算法 #7 恢复 |
| holdout | 四向重复评估 | baseline 从 seed 基线预测推导(0 推理)；final 真评 600；best_hard 版本备忘录(未评过且≠final 才评)；best_mixed 引用赢家成绩标指针(0 推理)。**去重做在 harness 逻辑层**（配合 epoch 盐，同版本不重采样）。**resume 语义**：备忘录不单独持久化，而在 resume 时从 `holdout_eval` 表 hydrate——查已落库的 `(skills_v,prompts_v)→test_acc` 重建已评集合（该表本就是每次评估的落库处），checkpoint 无需新增字段、无旧 checkpoint 迁移问题；hydrate 后同版本不再重评 |
| P1-5 | 诊断降级误入 defect | `cause_category=None`(judge 基础设施异常)与 `degraded=True` 错题按 **lapse 方向分流**；step 降级占比>50% 报错中止 |
| P2-3/#5 | step 重跑双计 + 慢更新非幂等 | `_run_step` 开始先 `DELETE ... WHERE run_id=<本step>`；`_refresh_gate_ladder` 保存 gate_pools 后**立即**原子落盘 checkpoint(`gate_epoch_observed=True`) 消除双计窗口 |

`run_holdout_eval` 保持开启（去重版），逐 epoch test 曲线四线齐全，成本从 2400 降至 600~1200 次/epoch。

## 7. WP3 —— 进化引擎 patch 加固（修 P1-6，算法 #8 防御）

| 层 | 改动 |
|----|------|
| 跨度判定 | `_in_ranges` 从"只查起点"改为"整个 target 跨度 [pos,pos+len) 与保护区相交即拒"；`_do_insert_after` 同 |
| 注入检查 | edit payload/target 含 `APPENDIX_START/END`、`MOMENTUM_START/END` 字面量 → 拒绝该 edit |
| 最后防线 | `validate_skill` 增 marker 完整性校验（成对、有序、各至多一对），违反整体 reject |

不改"保护跨度"语义方向，只把已声明的保护做完整。

## 8. WP4 —— 韧性与持久化

| # | 缺陷 | 修法 |
|---|------|------|
| P1-1 | 缓存跨 epoch 重放 | `chat()` 加 `cache_salt`；训练 rollout/val/test/**gate 候选臂**推理注入含 epoch 的盐（跨 epoch 真实重采样、同 epoch 续跑仍命中）；judge/evolve/离线切分无盐。TTL 修：`REDIS_CACHE_TTL≤0` 启动即 ValueError（消灭"0=永不过期"隐式语义）。**同步改 `.env`/`.env.example` 给正整数**（当前值 0，不改则修复后启动即崩），训练场景建议 ≥ 单次训练时长 |
| P1-1a | epoch 盐 vs gate 配对保真（Codex Critical） | **gate 双臂配对语义由架构保证、不受 LLM 缓存盐影响**：基线臂走 app 层 `BaselineCache`（unit 键 `task_type+s_hash+prompts_version+unit_id`，固定快照，`validate.py:_resolve_baseline_block`），候选臂每块新采样（`_run_candidate_block`）。配对翻转比较的是「固定基线快照 vs 新候选采样」（算法保真 #6 现有语义），epoch 盐只作用于**候选臂/rollout 的 LLM 层缓存键**，不触碰基线臂、不改变配对语义。gate 序贯 e-process 消费阶梯上**不同 block（不同 unit）**累积 e-value，非对同 unit 反复测；候选臂盐用 block 级 `run_id`（已含 epoch+block+candidate 区分）保证每次真实重测独立采样。**实现约束**：基线臂 miss 时的新鲜推理**不注入 epoch 盐**（保持基线快照跨 epoch 稳定，与 BaselineCache 语义一致）|
| P1-2 | SSE 截断毒化 | `_consume_stream` 校验 `[DONE]`/finish_reason 才算成功；流耗尽未完成抛 `_SseAnomaly` 进重试梯，**绝不写缓存** |
| P1-3 | 断连不重试 | `_is_transient_error` 扩为 `httpx.TimeoutException`+`httpx.TransportError` 两族 |
| P2-6 | 熔断半开烧穿 | cooldown 到期只放行 1 探针（half-open 锁），成功才闭合 |
| P1-4 | 基线臂 INFRA 污染 | INFRA 单元不写 BaselineCache、不计 W/L；护栏检查移到写缓存**之前** |
| P1-7 | manifest 非原子写 | `update_manifest/record_run/update_best/_scaffold` 统一 tmp+`os.replace`（复用 checkpoint 模式）|
| P2-2 | 基线元数据被改写 | 只读查询基线的两处改走只读连接，不经 `HarnessLog.__init__` upsert |
| P2-5 | dual_metric 口径歧义 | 慢更新 R2 行 `version_kind` 改 `slow_candidate`，`final` 恢复唯一语义 |
| P2-4 | 离线 uncertain 不可重试 | `video_split_cli` 加 `--retry-uncertain`（done 集排除 uncertain 行）；docstring "逐行落库"表述修正为"run 末批量落库" |

## 9. Rejected alternatives / Future work

| 选项 | 为何不做 |
|------|---------|
| epoch 层 per-skill best 混搭 | per-class val 样本太小(中类 5-10 题)终审频繁选错；prompts 随慢更新漂移致跨 epoch 成绩不可拼接。合理形态是训练**结束后**一次性 final assembly（每类取 gate 末次接受版本 + 最终 prompts，全 val 评一次比 best_hard），成本一次推理、不动循环 → **记 future work** |
| val_ratio 0.5 | 每类 3 epochs 仅 ~4 次进化机会，迭代受损；val 错题 38→46 边际收益趋零 |
| 确定性评估(temp=0) | 统计效率最高但基线用默认温度采，换口径要重烧全部基线，本轮不可行 |
| 会计层去重替代 epoch 盐 | 需动 gate/EMA 保真区且造不出新信息（每题永远只见一次抽样）|

## 10. 非功能四维（全设计汇总）

| 维度 | 保障 |
|------|------|
| **持久化** | 冻结产物/manifest/checkpoint 全 tmp+replace 原子写；holdout 备忘录入 checkpoint |
| **幂等** | step 重跑先 DELETE 后写；seed/冻结产物存在即拒覆盖(--force 显式)；诊断按指纹 upsert 不变 |
| **断点续跑** | epoch 盐保同 epoch 命中；gate_epoch_observed 在 gate_pools 保存后立即落盘，消除慢更新双计窗口 |
| **原子性** | manifest 补齐原子写（对齐 checkpoint 先例）；pools 冻结沿用现有原子写 |

## 11. 测试策略

| 层 | 覆盖 |
|----|------|
| 单元(新增) | tier 选择器(真实 baseline_diagnosis 二次构造：T2 捕获/val 错题≥20/视频原子)；可训练性预检(真实 pools 驱动，5 微型类被剔)；patch 三层校验；SSE 未完成流拒收；瞬时错误清单；缓存盐入键；extract_run_db 去重；seed 携带拷贝；holdout 备忘录 |
| 单元(改语义) | early_stop 按 epoch；加载器 fail-loud；prediction 归一化 |
| 集成 | fake-LLM 小池 train 冒烟(预检→模板→traces 适配→holdout 去重计数→step DELETE)；5 模板契约测试 |
| 回归 | 全量 1473 保持绿；agent/LLM 测试产 MD 到 `tests/outputs/` |

## 12. 训练启动 runbook（修复完成后）

```
1. 四工作包完成 + make test 绿
2. 备份 workspaces/video-split/ → 改 val_ratio:0.4 → 重跑 build_video_split.sh(诊断命中缓存秒级)
   → 核对: val 错题≥20 / T2 入 diag 数 / 预检模拟的可训类清单
3. 建 seed: extract_run_db(infer_adhoc,dedupe) + init_seed('adhoc-baseline', pools_json=…)
4. 新增 config/train_videomme.yaml + scripts/train_videomme.sh(零参可复现):
   epochs=3, early_stop_patience=2(epoch), run_holdout_eval=true(去重版),
   trainable_min_units=8, 其余 gate/batch 沿用 default.yaml
5. tmux: CUDA_VISIBLE_DEVICES=0 bash scripts/train_videomme.sh
```
