---
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 可移除
```
