fix(question_gen): sample_anchor Codex 审查修正

- C1/C2: Temporal Reasoning ≥3 L2 + Object Reasoning ≥2 L2 下限检查
- I1: L2 题型子帧不足时 ValueError
- I2: L3 过滤无 frame_path 的节点
- I3/I4: distractor_texts 扩展到整棵树范围
- I5-I7: 测试补强 Information Synopsis/Temporal Reasoning/Object Reasoning
- M1: Spatial Reasoning 测试断言 spatial_layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 05:24:30 -04:00
parent a597a9f901
commit 40b04f886e
2 changed files with 50 additions and 28 deletions
+41 -20
View File
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
import random
from app.tree.index import L1Node, L2Node, L3Node, TreeIndex
from app.tree.index import L2Node, L3Node, TreeIndex
@dataclass(frozen=True)
@@ -170,27 +170,33 @@ def _sample_l3(
异常:
ValueError: 候选 L3 节点不足。
"""
# Phase 1: 收集所有 L3 候选
# Phase 1: 收集所有 L3 候选(必须有 frame_path
candidates: list[tuple[L3Node, L2Node]] = []
for root in tree.roots:
for l2 in root.children:
for l3 in l2.children:
if l3.id not in used_node_ids:
if l3.id not in used_node_ids and l3.frame_path:
candidates.append((l3, l2))
if not candidates:
raise ValueError(f"锚节点不足: {task_type} 无可用 L3 节点")
raise ValueError(f"锚节点不足: {task_type} 无可用 L3 节点(需具备 frame_path")
# Phase 2: 随机选取
chosen_l3, parent_l2 = rng.choice(candidates)
# Phase 3: 构造上下文
# Phase 3: 构造上下文frame_path 已在候选过滤中保证非 None)
card_text = _serialize_l3_card(chosen_l3, spec.context_fields)
frame_paths = [chosen_l3.frame_path] if chosen_l3.frame_path else []
frame_paths = [chosen_l3.frame_path] # type: ignore[list-item]
subtitle = chosen_l3.subtitle or ""
# Phase 4: 干扰项——同 L2 下其他 L3 的 frame_summary
distractor_texts = [l3.card.frame_summary for l3 in parent_l2.children if l3.id != chosen_l3.id]
# Phase 4: 干扰项——整棵树中其他 L3 的 frame_summary
distractor_texts = [
l3.card.frame_summary
for root in tree.roots
for l2 in root.children
for l3 in l2.children
if l3.id != chosen_l3.id
]
return AnchorContext(
node_id=chosen_l3.id,
@@ -227,17 +233,17 @@ def _sample_l2(
ValueError: 候选 L2 节点不足。
"""
# Phase 1: 收集所有 L2 候选
all_l2: list[tuple[L2Node, L1Node]] = []
all_l2: list[L2Node] = []
for root in tree.roots:
for l2 in root.children:
if l2.id not in used_node_ids:
all_l2.append((l2, root))
all_l2.append(l2)
if not all_l2:
raise ValueError(f"锚节点不足: {task_type} 无可用 L2 节点")
# Phase 2: 随机选取
chosen_l2, parent_l1 = rng.choice(all_l2)
chosen_l2 = rng.choice(all_l2)
is_temporal_perception = task_type == "Temporal Perception"
@@ -252,8 +258,13 @@ def _sample_l2(
else:
# 普通 L2:随机采样 2-3 个 L3 帧
children_with_frames = [l3 for l3 in chosen_l2.children if l3.frame_path]
if len(children_with_frames) < 2:
raise ValueError(
f"锚节点不足: {task_type} 需要 >=2 个子帧,"
f"{chosen_l2.id} 仅有 {len(children_with_frames)} 个可用帧"
)
n_frames = min(rng.randint(2, 3), len(children_with_frames))
sampled = rng.sample(children_with_frames, n_frames) if n_frames > 0 else []
sampled = rng.sample(children_with_frames, n_frames)
frame_paths = [l3.frame_path for l3 in sampled if l3.frame_path]
# Phase 4: card_text
@@ -266,9 +277,12 @@ def _sample_l2(
if chosen_l2.children and chosen_l2.children[0].subtitle:
subtitle = chosen_l2.children[0].subtitle
# Phase 6: 干扰项——同 L1 下其他 L2 的 event_description
# Phase 6: 干扰项——整棵树中其他 L2 的 event_description
distractor_texts = [
l2.card.event_description for l2 in parent_l1.children if l2.id != chosen_l2.id
l2.card.event_description
for root in tree.roots
for l2 in root.children
if l2.id != chosen_l2.id
]
return AnchorContext(
@@ -290,7 +304,7 @@ def _sample_l1(
"""L1 层级锚节点采样(Temporal Reasoning / Information Synopsis)。
Information Synopsis:使用目标 L1 下全部 L2 子节点。
Temporal Reasoning使用 >=3 个 L2 子节点不足 3 个则全部使用)
Temporal Reasoning严格要求 >=3 个 L2 子节点不足则抛 ValueError
L2 按 time_range 升序排列,每个 L2 取一帧代表。
参数:
@@ -304,7 +318,7 @@ def _sample_l1(
AnchorContext 实例。
异常:
ValueError: 候选 L1 节点不足。
ValueError: 候选 L1 节点不足,或 Temporal Reasoning 的 L2 子节点 <3
"""
# Phase 1: 收集可用 L1
candidates = [r for r in tree.roots if r.id not in used_node_ids]
@@ -319,8 +333,13 @@ def _sample_l1(
# 必须使用全部 L2
selected_l2 = list(chosen_l1.children)
else:
# Temporal Reasoning>=3 个 L2(不足则全部)
if len(chosen_l1.children) <= 3:
# Temporal Reasoning严格要求 >=3 个 L2
if len(chosen_l1.children) < 3:
raise ValueError(
f"锚节点不足: {task_type} 需要 >=3 个 L2 子节点,"
f"{chosen_l1.id} 仅有 {len(chosen_l1.children)}"
)
if len(chosen_l1.children) == 3:
selected_l2 = list(chosen_l1.children)
else:
selected_l2 = rng.sample(chosen_l1.children, rng.randint(3, len(chosen_l1.children)))
@@ -385,8 +404,10 @@ def _sample_l1_l2(
if l2.id not in used_node_ids:
all_l2.append(l2)
if not all_l2:
raise ValueError(f"锚节点不足: {task_type} 无可用 L2 节点")
if len(all_l2) < 2:
raise ValueError(
f"锚节点不足: {task_type} 需要 >=2 个 L2 节点,但仅有 {len(all_l2)} 个可用"
)
# Phase 2: 随机选 2-3 个
n_pick = min(rng.randint(2, 3), len(all_l2))