Files
Video-Tree-TRM5/research-wiki/designs/fix-diagnosis-tree-data-link.md
T

120 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
type: design
node_id: design:fix-diagnosis-tree-data-link
title: "修复诊断 tree_data 断链 bugTRM4→TRM5 迁移 regression"
date: 2026-07-15
---
# 修复诊断 tree_data 断链 bugTRM4→TRM5 迁移 regression
> 诊断编排全程传 `tree_data={}`,导致 ground_truth 恒空、error_type 归因 100% 坍缩成 extraction_failure。本设计接通断掉的树加载环,补齐 P5 fail-loud,双注入点(离线诊断 + 训练循环)一并修复。
>
> **状态**:已含 Codex 独立审查修订(2 Critical + 8 Important 全部核验并采纳;展平实现从"复用 to_dict"改为"遍历 json"level 改由遍历深度赋值,补视频覆盖 fail-loud,重冻 test 成员变化诚实化)。
## 1. 根因(已坐实)
| 环节 | 位置 | 事实 |
|------|------|------|
| CLI 注入空树 | `app/harness/video_split_cli.py:335` | `tree_data={}`,注释谎称"由诊断管线内部按需加载" |
| 训练循环同病 | `app/harness/runner.py:2180` | `tree_data={}` + 同一句假注释 |
| 编排层无加载 | `core/evolution/diagnose.py:2068-2074` | `if tree_data and "nodes"...``{}` 为假 → else 分支 `tree_data_by_video={}`**根本无加载逻辑** |
| ground_truth 恒空 | `diagnose.py:736,755` | `{}.get("nodes",{})` → node card 取空 → `""` |
| span 评估无参照 | `diagnose.py:509-515` | judge 看不到"应提取什么"`extraction_completeness` 系统性偏低,解析失败还默认 0.0 |
| 归因瀑布短路 | `diagnose.py:928` | `avg_completeness < 0.5` 恒真 → **100% extraction_failure** |
实测:82 个 T2 全部 extraction_failureevolution_target 全 tool48 格多样性退化成 11 格(error_type 维坍缩成单值)。`judge_missed_nodes``tree_content` 同样为空,search_failure 分支永不触发。
## 2. Regression 溯源:TRM4 无此 bug
| 层面 | TRM4(正常) | TRM5(坏了) |
|------|------|------|
| tree.json 结构 | 扁平 `{"nodes": {id: {node_id,level,time_range,parent_id,children_ids,card}}}` | 嵌套 `{"metadata", "roots": [{id,card,time_range,children}]}` |
| tree_cache 填充 | `diagnose.py:1677` `_load_json(tree.json)` 直接得 nodes | `tree_data={}`,填充逻辑被删 |
| 诊断拿 ground_truth | ✅ `_load_json(...)["nodes"]` 直接可用 | ❌ 恒空 |
**双重 regression**:(a) 建树模块重写把产物格式从扁平 nodes 演进成嵌套 roots(合理架构升级);(b) 诊断代码迁移时既未适配新格式、又把加载逻辑删成空 dict。因此 TRM5 需要一个 TRM4 不需要的**格式桥接展平器**roots → nodes)。
## 3. 影响范围
| 维度 | 是否污染 | 原因 |
|------|---------|------|
| tier 分层 T0/T1/T2/uncertain | ✅ 干净 | `classify_defect_vs_lapse` **不吃 tree_data** |
| floor_k / test 代表性 | ✅ 干净 | 按 task_type 总数,与 error_type 无关 |
| error_type 归因 | ❌ 全坍缩 | ground_truth 恒空 |
| evolution_target 路由 | ❌ 全 tool | error_type 的下游派生 |
| 48 格多样性覆盖 | ⚠️ 退化 | error_type 维失效 → 实际仅按 task_type 单维 |
| `judge_missed_nodes` | ⚠️ 退化 | tree_content 空 |
结论:已冻结 `pools.json` 的 tier/floor/代表性**可信**,仅 error_type 维及其下游被污染。
## 4. 修复设计
### 4.1 单元拆分(单一职责)
| 单元 | 位置 | 职责 | 依赖 |
|------|------|------|------|
| 树展平器 | `app/harness/tree_nodes.py`(新) | `load_tree_nodes(store_dir, video_id) -> dict`**递归遍历 tree.json 的 `roots`(嵌套 dict**,按遍历深度赋 `level`root=1/child=2/孙=3),抽取每节点 `id/card/time_range`,组装成扁平 `{"nodes": {node_id: {card, level, time_range}}}` | 只读文件 + json**不走 TreeIndex 对象层**,见 §4.2 核验) |
| 离线注入 | `app/harness/video_split_cli.py``build_diagnosis_deps`/`run_pipeline` | 由 `wrong_ids` → 涉及 video 去重加载成 `{video_id: {"nodes":...}}`,填充 `DiagnosisDeps.tree_data``baseline_diagnosis` 仅透传) | ← 展平器 |
| 训练注入 | `app/harness/runner.py``_run_diagnosis` | 由 batch `question_ids` → 涉及 video 同法加载注入,删 `tree_data={}` 假注释 | ← 展平器 |
### 4.2 关键决策
| 边界 | 决策 | 理由 |
|------|------|------|
| 加载职责 | **app 层**video_split_cli / runner),core 只消费 dict | Clean Architecturecore 不碰文件系统;`run_diagnosis` else 分支已支持 `{video_id:...}` 形态,core 零改动 |
| 展平实现 | **递归遍历 tree.json `roots` dict**(原始 json,card 已是 dict 直接取),不走对象层 | 核验:仅 `L1Node``to_dict``index.py:260`),L2/L3 是其内部闭包;且 to_dict 输出无 `level`、L3 无 `time_range`(用 `timestamp`)。遍历 json 更省且零改建树模块 |
| level 字段 | **由遍历深度直接赋值**root=1/child=2/孙=3),不解析 node_id | node_id 累积式 `..._L1_..._L2_..._L3_`,正则首匹配会把 L2/L3 误判成 1Codex I7 |
| 预加载范围 | 只加载被诊断题涉及的 video,按 video 缓存去重 | YAGNI236 错题涉及 <200 视频,避免加载无关树 |
| fail-loud | tree.json 缺失 → `FileNotFoundError`(沿用 `factory.py:87` 先例);展平后 nodes 为空 → `ValueError`;**本次诊断涉及的每个 video 必须被 tree_data 覆盖**`run_diagnosis``.get(vid, {})` 静默回退(`diagnose.py:2144-2145`)改为缺失即 raise | 补 P5:空树/漏加载本应报错,不再静默退化 |
| 诊断瀑布本身 | **不动**algo §4.7 #7) | 修复只接通输入,不改归因逻辑 |
> **L3 无 `time_range` 的已知次要行为**`node.to_dict` 语义下 L3 节点用 `timestamp``_load_tree_content` 取 `time_range` 时 L3 退化为默认 `[0,0]`,仅影响 `judge_missed_nodes` 文本里 L3 的时间显示,不影响 ground_truthcard)。展平器可选:从 L3 `timestamp` 合成 `[timestamp, timestamp]`。
## 5. 非功能性四维
| 维度 | 结论 |
|------|------|
| 持久化 | 展平器纯内存只读,不落盘;诊断结果仍逐题 upsert(不变) |
| 幂等性 | 展平确定性(同 tree.json → 同 nodes);诊断 upsert 幂等(不变) |
| 断点续跑 | `done_question_ids` 续跑机制不变;fingerprint 因代码改动而变 → 全量重跑一次 |
| 原子性 | 无新写操作;pools.json 重冻仍走既有原子写 |
## 6. 重跑与重冻衔接
代码改动 → git short SHA 变 → `diag_fingerprint` 变 → 全量重跑 236 题诊断。
- `classify_defect_vs_lapse` / evidence / bias / skill judge 输入不含 tree_data → **预期 Redis 缓存命中,tier 分层 T2/T1 应不变**。但这是"大概率"而非"必然":缓存键内容未逐字段核证,且存在非 tree_data 的不稳定源(如 C3 异常吞并 attribution`diagnose.py:2186`)。**重跑后须实测校验 T2=82/T1=152 是否保持**,偏差需归因。
- `evaluate_span`ground_truth 由空变有)+ `judge_missed_nodes`tree_content 由空变有)缓存失效 → 重算(约半数 LLM 调用,~1h)。
- 诊断完成后重跑 Phase 2 重新冻结覆盖 `pools.json`**error_type 恢复判别力会改变多样性阶段的选择顺序**(cells 按 `(task_type, error_type)` 排序,`split_selection.py:139/193`)→ trainval 组成变 → **test 补集成员随之变化,不保证稳定**。floor 与代表性 ε 约束仍保证 test **代表性合格**,但具体成员会变——用户已接受重冻覆盖,这是预期结果而非风险。
## 7. 测试策略
| 测试 | 类型 | 验证点 |
|------|------|--------|
| 展平器正确性 | unit | 真实 tree.json → `{"nodes":{...}}`,节点数=递归总数,node 含 card(dict)/level/time_range |
| 展平器 level 赋值 | unit | 遍历深度 → level 1/2/3,与节点嵌套层级一致(不依赖 node_id 解析) |
| 展平器 fail-loud | unit | tree.json 缺失 → FileNotFoundError;空树/空 nodes → ValueError |
| 视频覆盖校验 | unit | 诊断涉及的 video 未被 tree_data 覆盖 → raise(不静默回退) |
| ground_truth 接通 | integration | 对真实 T2 样本注入真实树,断言 `evaluate_span` 收到**非空 ground_truth**(充分条件);**观察** error_type 恢复多值(软验收——分布是否脱离单值取决于 judge,非硬保证) |
| core 依赖方向 | 结构检查 | `core/evolution/diagnose.py` 不 import `app.tree` |
## 8. 算法保真声明
本修复触及核心算法 **§4.7 #7 诊断瀑布**:仅接通其输入(tree_data),**不改** `attribute_error` 归因分支、severity 函数或 defect/lapse 判定逻辑。参考 TRM4 `core/harness/diagnose.py` 的 tree_cache 语义对照,确保 TRM5 诊断拿到与 TRM4 一致的 ground_truth。
## 9. 被拒方案
| 方案 | 拒因 |
|------|------|
| 复用 `TreeIndex.load_json` + `node.to_dict` 走对象层 | 核验否决:仅 `L1Node``to_dict``index.py:260`),L2/L3 为其内部闭包;输出无 `level`、L3 无 `time_range`,补了也要后处理 |
| 补 L2/L3Node.to_dict 再走对象层 | 改核心建树模块 `index.py`(推理/save_json/建树全链路依赖,algo §4.7 #1-3);为诊断读取需求反向改生产方接口,违反 YAGNI |
| 让 core 诊断直接吃 `TreeEnvironment` 对象 | 破坏依赖方向(core 依赖 app.tree);且需构建 env(要 frames_dir)过重 |
| 保留现有 pools.json 不重冻 | train 多样性仍基于污染的 error_type,违背修复初衷 |
| 只修离线诊断、不修 runner | 训练循环 backward 每轮复现坍缩,进化盲目只改 tool |
## 关联
- 影响指标:`metric:split-cell-coverage`48 格覆盖)、`metric:split-signal-tier-distribution`
- 数据表:`schema:baseline-diagnosis`
- 上游设计:`design:results-driven-video-split`
- 参考实现:TRM4 `core/harness/diagnose.py:1677`tree_cache 填充)