--- id: task-type-strategy title: 出题管线 TaskTypeStrategy 拆分设计(Clean Architecture) type: design created: 2026-07-14 status: approved --- # 出题管线 TaskTypeStrategy 拆分设计 ## 1. 目标 借鉴 Clean Architecture 思想,将出题管线从 5 个粗粒度 Family 替换为 12 个题型级别的 TaskTypeStrategy,实现题型独立的出题策略。第一个特化实现为 ActionRecognitionStrategy(含 6 个失败子模式靶向出题)。 ### 设计驱动 | 问题 | 数据来源 | |------|---------| | v2-360 全部 30 道 AR 题 100% 单帧可答 | `research-wiki/findings/2026-07-14-question-quality-gap-analysis.md` | | 12/12 题型存在 CRITICAL 或 HIGH 差距 | 同上 | | 同一 task_type 跨 family 随机选择导致行为不确定 | `app/question_gen/families.py` — AR 同时在 RETRIEVAL 和 VISUAL | | 5 个 prompt 模板零题型分支 | `store/prompts/question_gen/*.md` | ## 2. 架构 ### 2.1 替换关系 ``` 之前:pipeline → get_family_for_slot() → QuestionFamilySpec(5 个,随机选择) 之后:pipeline → get_strategy(task_type) → TaskTypeStrategy(12 个,确定性查找) ``` ### 2.2 类层次 ``` TaskTypeStrategy (Protocol) │ ├── BaseTaskTypeStrategy (类) │ └── 封装现有 family 行为,确定性绑定一个 QuestionFamilySpec │ └── select_sub_pattern → None │ └── extra_gates → [] │ └── 11 个题型用此类 │ └── ActionRecognitionStrategy (类) └── 自包含采样/prompt/SubPattern └── 仍提供 strategy_name / skill_target / leak_probe_template(gate/store 合约) └── 6 个 SubPattern + maintenance_pool ``` ### 2.3 接口定义 ```python class TaskTypeStrategy(Protocol): """题型出题策略 — pipeline 的唯一接口。""" @property def task_type(self) -> str: ... # ── 采样 ── @property def sampling_level(self) -> int: ... @property def sampling_constraint(self) -> SamplingConstraint: ... # ── Prompt ── @property def prompt_template(self) -> str: ... def select_sub_pattern(self, rng: Random) -> SubPattern | None: ... def build_prompt_context(self, material: MaterialContext, sub_pattern: SubPattern | None) -> dict: ... # ── Gate / Store 合约 ── @property def strategy_name(self) -> str: """策略标识名,写入 store family 字段(如 "RETRIEVAL"、"ACTION_RECOGNITION")。""" @property def skill_target(self) -> str: """目标失败机制编号,写入 store(如 "M1"、"AR")。""" @property def leak_probe_template(self) -> str: """泄漏检测 prompt 模板文件名,供 leak_test gate 使用。""" def extra_gates(self, candidate: CandidateQuestion) -> list[GateResult]: """题型专属的额外验证(base 返回空列表)。""" ``` `strategy_name` / `skill_target` / `leak_probe_template` 替代了 pipeline 中对 `slot.family.name` / `slot.family.skill_target` / `slot.family.leak_profile.probe_template` 的访问,确保 gate 和 store 合约不破坏。 ## 3. SubPattern 数据结构 ```python @dataclass(frozen=True) class SubPattern: """出题子模式 — 靶向特定失败机制。""" name: str weight: float sampling_level_override: int | None constraint_override: SamplingConstraint | None instruction: str # 核心指令(1-3 句话) positive_examples: list[dict] # VME 原题 few-shot negative_examples: list[dict] # 反面示例 distractor_rules: str # 干扰项构造规则 ``` ## 4. ActionRecognitionStrategy 的 6 个 SubPattern 来源:22 道错题双分类器仲裁分析。 | SubPattern | 权重 | 采样 | 错题# | 失败机制 | |-----------|------|------|-------|---------| | premature_evidence_anchoring | 0.20 | L1 | #3,4,12,17 | 找到一个证据就停搜,未验证全部选项 | | temporal_reasoning_failure | 0.20 | L1 | #8,9,15,16 | 事件排序错误 / 无法定位第 N 次事件 | | semantic_rigidity | 0.15 | L2 | #1,2,22 | 要求字面匹配,拒绝同义词释义 | | fine_grained_visual_action | 0.15 | L2 | #6,13,14,18 | 识别"做了什么"但分不清"怎么做的" | | cross_segment_entity_tracking | 0.15 | L1 | #7,19,20 | 单段正确但无法跨段合并 | | evidence_gap_confabulation | 0.15 | L2 | #5,10,11,21 | 缺证据时编造因果链 | ### ActionRecognitionStrategy 的 gate/store 合约值 | 字段 | 值 | 说明 | |------|-----|------| | strategy_name | `"ACTION_RECOGNITION"` | store 的 family 字段 | | skill_target | `"M1_AR"` | 继承自 RETRIEVAL 的 M1 + AR 后缀区分 | | leak_probe_template | `"gate_leak_retrieval.md"` | 复用 RETRIEVAL 的泄漏检测模板 | | sampling_level(默认) | 2 | L2 事件级(从 L3 提升,有意变更) | | sampling_constraint(默认) | `min_subtitles=3, min_l3_nodes=5, require_frames=True, cross_l2_span=True` | 加强约束(有意变更) | SubPattern 可通过 `sampling_level_override` / `constraint_override` 进一步覆盖默认值。 ### 4.1 能力保持机制 capability_maintenance 不是生成子模式,而是**采样来源**:从 Video-MME AR 的 41 道正确题中采样,直接进训练集。通过 `batch_correct_ratio` 控制混合比例。 ``` 训练 batch 组成: ├── 错误题来源: 6 个 SubPattern 生成的新题 └── 正确题来源: maintenance_pool (41 道已验证正确的 VME 原题) ``` ## 5. pipeline 集成 ### 5.1 调用链变更 ``` 之前: slot.family → sample_material_v2(family_spec=...) → generate_one_v2(family_spec=...) → run_gates(family_spec=...) → store.record_item(family=slot.family.name) 之后: strategy = get_strategy(slot.task_type) sub_pattern = strategy.select_sub_pattern(rng) level = sub_pattern.sampling_level_override or strategy.sampling_level constraint = sub_pattern.constraint_override or strategy.sampling_constraint material = sample_material_v2(level=level, constraint=constraint, ...) prompt_ctx = strategy.build_prompt_context(material, sub_pattern) candidate = generate_one_v2(prompt_ctx=prompt_ctx, ...) report = run_gates(candidate, leak_template=strategy.leak_probe_template, ...) extra = strategy.extra_gates(candidate) store.record_item(..., family=strategy.strategy_name, skill_target=strategy.skill_target, sub_pattern=sub_pattern.name if sub_pattern else None) ``` ### 5.2 接口影响面 | 文件 | 变更 | 影响范围 | |------|------|---------| | `sampler_v2.py` | `sample_material_v2` 签名:`family_spec` → `level: int` + `constraint: SamplingConstraint` | 函数签名 + 内部 `constraint = family_spec.sampling` 行 | | `pipeline_v2.py` | `_process_one_slot`:`slot.family` → `strategy`;`SlotAssignment.family` 字段移除 | `_assign_slots` / `_process_one_slot` / `_process_wrapper` | | `generator_v2.py` | `generate_one_v2` 签名:新增 `prompt_ctx: dict` 参数,替代内部读 `family_spec.prompt_template` | `_build_v2_prompt` / `generate_one_v2` | | `gates.py` | `run_gates` 签名:`family_spec` → `leak_template: str` | `_run_leak_test` 参数变更 | | `run_store.py` | `record_item` 新增 `sub_pattern: str | None` 参数 | DDL 加 `sub_pattern TEXT` 列 | ### 5.3 不变的部分 后处理(postprocess)、四门 gate 内部逻辑、去重(embedding)、重量抽检(heavy_check)、on_accept 回调、progress 断点续跑 — 全部不动。 ## 6. Strategy 注册 ```python _STRATEGY_REGISTRY: dict[str, TaskTypeStrategy] = {} def get_strategy(task_type: str) -> TaskTypeStrategy: if task_type in _STRATEGY_REGISTRY: return _STRATEGY_REGISTRY[task_type] return _build_default_strategy(task_type) ``` ### 6.1 BaseTaskTypeStrategy family 消歧绑定表 消除多 family 随机性。每个 task_type 确定性绑定一个 family。 | task_type | 旧合法 families | 新绑定 | 被移除行为 | 理由 | |-----------|---------------|--------|-----------|------| | Action Recognition | RETRIEVAL, VISUAL | **特化策略** | 不走 family | 完全自包含 | | Object Recognition | RETRIEVAL | RETRIEVAL | 无 | 唯一 | | Object Reasoning | RETRIEVAL, REASONING | **REASONING** | RETRIEVAL 的 L3 单帧 + 禁推理 prompt | 需要推理;RETRIEVAL 产出识别题是已知 bug | | Action Reasoning | REASONING | REASONING | 无 | 唯一 | | Attribute Perception | RETRIEVAL, VISUAL | **VISUAL** | RETRIEVAL 的 require_frames=False | 属性感知需要帧 | | OCR Problems | RETRIEVAL, VISUAL | **VISUAL** | RETRIEVAL 的 require_frames=False | OCR 需要帧 | | Counting Problem | ENUMERATION, VISUAL | **ENUMERATION** | VISUAL 的 L3 单帧采样 | 计数需要跨帧 | | Information Synopsis | REASONING, ENUMERATION | **REASONING** | ENUMERATION 的 cross_l2_span=False | 综述需要跨段 | | Temporal Reasoning | ENUMERATION | ENUMERATION | 无 | 唯一 | | Temporal Perception | ENUMERATION | ENUMERATION | 无 | 唯一 | | Spatial Reasoning | SPATIAL | SPATIAL | 无 | 唯一 | | Spatial Perception | SPATIAL | SPATIAL | 无 | 唯一 | ### 6.2 多归属题型约束变化明细 确定性绑定会改变多归属题型的采样约束,这是**有意的行为变更**(旧行为是随机混合,本身就是不可控的)。 | task_type | 旧约束(随机选择) | 新约束(确定绑定) | 变化 | |-----------|-----------------|-----------------|------| | Object Reasoning | RETRIEVAL(L3,no-frame,no-cross) 或 REASONING(L2,no-frame,cross) | REASONING 固定 | 不再退化为 L3 识别题 | | Attribute Perception | RETRIEVAL(no-frame) 或 VISUAL(frame) | VISUAL 固定 | 始终要求帧 | | OCR Problems | RETRIEVAL(no-frame) 或 VISUAL(frame) | VISUAL 固定 | 始终要求帧 | | Counting Problem | ENUMERATION(no-frame) 或 VISUAL(frame) | ENUMERATION 固定 | 不再混入 VISUAL 的 L3 采样 | | Information Synopsis | REASONING(cross) 或 ENUMERATION(no-cross) | REASONING 固定 | 始终跨段 | ## 7. 文件结构 | 操作 | 文件 | 职责 | |------|------|------| | 新建 | `app/question_gen/strategy.py` | Protocol + SubPattern + BaseTaskTypeStrategy + 注册表 | | 新建 | `app/question_gen/strategy_action_recognition.py` | ActionRecognitionStrategy 实现 + 6 个 SubPattern 定义 | | 修改 | `app/question_gen/pipeline_v2.py` | `_process_one_slot` / `_assign_slots` 改为通过 strategy | | 修改 | `app/question_gen/sampler_v2.py` | 签名 `family_spec` → `level` + `constraint` | | 修改 | `app/question_gen/generator_v2.py` | 签名 `family_spec` → `prompt_ctx` | | 修改 | `app/question_gen/gates.py` | `run_gates` 签名 `family_spec` → `leak_template` | | 修改 | `app/question_gen/run_store.py` | `record_item` 新增 `sub_pattern` 参数 + DDL 加列 | | 不动 | `app/question_gen/families.py` | 保留,BaseTaskTypeStrategy 内部使用 | | 不动 | `app/question_gen/postprocess.py` | 后处理不变 | ## 8. 非功能性需求 | 维度 | 设计 | |------|------| | 持久化 | 不变 — on_accept 逐题回调。SubPattern 名写入 store 的 sub_pattern 列以便追溯 | | 幂等性 | 不变 — 同 seed → 同 slot 分配。strategy 查找确定性,sub_pattern 选择由 rng 控制 | | 断点续跑 | 不变 — progress 机制在 pipeline 层,strategy 无状态 | | 原子性 | 不变 — 逐题落库 | | store 迁移 | `question_gen_items` 表新增 `sub_pattern TEXT` 列,默认 NULL(BaseTaskTypeStrategy 写 NULL) | ## 9. 行为保真检查清单 | # | 行为 | 状态 | 说明 | |---|------|------|------| | 1 | 12 种 task_type 列表 | 保留 | pipeline 不变 | | 2 | slot round-robin 视频 | 保留 | pipeline 不变 | | 3 | family 加权随机选择 | **有意变更** | 消除不确定性 → 确定性绑定。详见 §6.2 | | 4 | _TASK_TYPE_TO_LEVEL 映射 | **有意变更** | BaseTaskTypeStrategy 从绑定 family 读取原值(唯一归属题型数值不变);AR 特化策略从 L3→L2(有意提升);多归属题型因消歧而变化(详见 §6.2) | | 5 | SamplingConstraint 约束 | **有意变更** | 同 #4 逻辑:唯一归属不变,多归属因消歧变化,AR 有意加强 | | 6 | 重出循环 + 换视频 | 保留 | | | 7 | 后处理洗牌 + verbatim | 保留 | | | 8 | 四门 gate | 保留 | leak_test 通过 strategy.leak_probe_template 获取模板 | | 9 | embedding 去重 | 保留 | | | 10 | 重量抽检 | 保留 | | | 11 | on_accept 回调 | 保留 | | | 12 | progress 断点续跑 | 保留 | | | 13 | reject_reason 传入重出 | 保留 | | | 14 | QuestionGenStore 记录 | 保留 | family → strategy_name;新增 sub_pattern 列 | ## 10. 渐进替换路径 ``` Phase 1 (当前): ActionRecognitionStrategy 特化 + 11 个 BaseTaskTypeStrategy Phase 2 (按需): 逐个替换表现差的题型为特化策略 Phase N (最终): 12 个特化策略,families.py 可移除 ```