type, node_id, title, date
| type |
node_id |
title |
date |
| design |
design:fix-diagnosis-tree-data-link |
修复诊断 tree_data 断链 bug(TRM4→TRM5 迁移 regression) |
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 填充)