docs: design for diagnosis tree_data broken-link fix (TRM4 regression)
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
---
|
||||
type: design
|
||||
node_id: design:fix-diagnosis-tree-data-link
|
||||
title: "修复诊断 tree_data 断链 bug(TRM4→TRM5 迁移 regression)"
|
||||
date: 2026-07-15
|
||||
---
|
||||
|
||||
# 修复诊断 tree_data 断链 bug(TRM4→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_failure,evolution_target 全 tool,48 格多样性退化成 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 Architecture:core 不碰文件系统;`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 误判成 1(Codex I7) |
|
||||
| 预加载范围 | 只加载被诊断题涉及的 video,按 video 缓存去重 | YAGNI;236 错题涉及 <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_truth(card)。展平器可选:从 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 填充)
|
||||
Reference in New Issue
Block a user