Files
Video-Tree-TRM5/research-wiki/designs/2026-07-12-per-category-pool-strategy-design.md

9.9 KiB
Raw Permalink Blame History

Per-Category Pool Strategy 设计

状态: 已批准
日期: 2026-07-12
关联: app-harness-designquestion-gen-v2-design

1. 动机

v2 题目生成管线为 12 个 task_type 各生成 30 题(共 360 题)。需要支持:

需求 说明
按类增量 baseline 类别生成完成后立即可跑 baseline inference,无需等全量完成
Per-category 分层划分 每类 30 题按 correctness 2:1 分为 20 train / 10 val
按类训练 --task-types 控制训练作用域,快速验证单类别效果
外部 test 池 test 用 Video-MME 900 道真题中同类题目,不从生成题中抽取
全局训练兼容 全类别联合训练时 system/tool 慢进化看全量 val 信号

2. 方案选型

方案 描述 取舍
A. 最小改动 build_pools 内加 if 分支 改动小但两条路径耦合
B. 替换为纯 per-category 删除全局模式 简洁但丢失 Video-MME 900 题训练能力
C. Pool 工厂模式 PoolStrategy Protocol + 具体策略 符合 Clean Architecture DIP/OCP;策略可独立测试替换

选择 C:池分割是真正易变的接缝(已有两种策略,未来可扩展),满足 CLAUDE.md 抽象引入条件。

3. 核心抽象

3.1 PoolStrategy Protocol

# app/ports.py 新增(应用层端口,非 core 层——因为返回值 Pools 定义在 app/harness/

class PoolStrategy(Protocol):
    """池构建策略端口。"""

    def build(
        self,
        questions: list[GeneratedQuestion],
        correctness: dict[str, bool],
        config: PoolConfig,
    ) -> Pools: ...

    def build_incremental(
        self,
        new_task_types: list[str],
        questions: list[GeneratedQuestion],
        correctness: dict[str, bool],
        config: PoolConfig,
    ) -> dict[str, dict[str, list[str]]]:
        """增量构建新类别的 train/val 划分。

        返回 {task_type: {"train": [qid, ...], "val": [qid, ...]}}。
        仅 PerCategoryPoolStrategy 有实质实现;GlobalPoolStrategy
        不支持增量,调用时抛出 NotImplementedError。
        """
        ...

层级决策PoolStrategy 放在 app/ports.py 而非 core/protocols.py,因为返回类型 Pools 定义在 app/harness/pools.py。若放入 core 会导致 core → app 的逆向依赖,违反 Clean Architecture 四层分层。

PoolConfig 的两组字段(Global 系列 / PerCategory 系列)由两个策略各取所需,未使用的字段被忽略。这避免了为每个策略定义专属 config 子类的过度抽象。

3.2 PoolConfig

# core/types.py 新增

@dataclass(frozen=True)
class PoolConfig:
    task_types: tuple[str, ...] | None  # None = 全部类别
    seed: int                           # 随机种子
    baseline_run_id: str                # 基线 run 标识(用于读 correctness、写入 pools 指纹)
    # GlobalStrategy 使用
    diag_size: int
    diag_correct_ratio: float
    val_size: int
    val_correct_ratio: float
    test_size: int
    eval_min_per_class: int
    # PerCategoryStrategy 使用
    train_ratio: float                  # train/(train+val),默认 2/3
    test_questions_dir: Path | None     # 外部 test 题源路径(与 RunConfig.test_questions 对应)

3.3 两个具体策略

GlobalPoolStrategy:封装现有 build_pools 逻辑,test → val → diag 全局三分。

PerCategoryPoolStrategy

对每个 task_type (共 N 题,目标 train=20, val=10):
    correct 题 (Nc 个) → shuffle(seed)
        train_correct = floor(Nc * 20/N)    # 按总量 20/10 比例分配
        val_correct   = Nc - train_correct
    wrong 题 (Nw 个)   → shuffle(seed)
        train_wrong   = 20 - train_correct  # 确保 train 总量恰好 20
        val_wrong     = Nw - train_wrong
    assert train_correct + train_wrong == 20
    assert val_correct + val_wrong == 10

diagnosis = 所有选中类别的 train 合并
validation = 所有选中类别的 val 合并
test = load_benchmark(test_questions_dir) 按 task_types 过滤

边界场景处理

场景 行为
某类别全部 correct0 wrong 退化为非分层 random 20/10,记录 WARNING
某类别全部 wrong0 correct 同上,退化为非分层 random 20/10
某类别 correctness 不完整(部分 qid 无 baseline fail-fast,列出缺失 qid
Video-MME 中某 task_type 无题 test 池该类别为空,记录 WARNING

4. RunConfig 变更

新增字段 类型 默认值 归属
task_types tuple[str, ...] | None None CLI 临时覆盖
pool_split_mode Literal["global", "per_category"] "global" 科研 YAML
train_ratio float 0.667 科研 YAML
test_questions str "benchmarks/Video-MME" 科研 YAML

default.yaml 新增:

harness:
  pool_split_mode: global
  train_ratio: 0.667
  test_questions: "benchmarks/Video-MME"

5. 增量 Baseline Inference

5.1 流程

# 类别 A、B 完成 → 跑 baseline
python main.py harness --mode infer \
    --run-id baseline_v2 --questions generated-v2-360 \
    --task-types "Object Recognition" "Scene Understanding"

# 类别 C 完成 → 追加到同一 run_id
python main.py harness --mode infer \
    --run-id baseline_v2 --questions generated-v2-360 \
    --task-types "Action Reasoning"

# promote 为 seed
python main.py harness --mode promote \
    --run-id baseline_v2 --seed baseline_v2_seed

# 按类训练
python main.py harness --mode train --fresh \
    --seed baseline_v2_seed --run-id train_obj_recog_01 \
    --task-types "Object Recognition" --pool-split-mode per_category

5.2 增量写入规则

唯一键 同 run_id 多次 infer 行为
_runs run_id upsert:更新 updated_at,保留原始 created_at
predictions (run_id, question_id) INSERT;主键冲突报错(防重复推理)
traces trace_id 正常 INSERTUUID 天然唯一)

重跑同类别:若需重新推理某类别,先 DELETE 该类别的 predictionsWHERE run_id=? AND question_id IN (?)),再重新 infer。

promote 读取规则promote_to_seed 读取指定 run_id 下全部 predictions,不区分追加批次。promote 前应确保所有目标类别均已完成 baseline。

6. 池冻结格式

6.1 per_category 格式

{
    "split_mode": "per_category",
    "train_ratio": 0.667,
    "baseline_run_id": "baseline_v2",
    "categories": {
        "Object Recognition": {
            "train": ["qid_1", "qid_2", "..."],
            "val": ["qid_21", "..."]
        }
    },
    "test_source": "benchmarks/Video-MME",
    "questions": [...]
}

6.2 增量更新

def build_or_load_pools(
    config: RunConfig,
    strategy: PoolStrategy,
    db_path: Path,
) -> Pools:
    """构建或加载三池。

    参数:
        config: 运行配置(含 task_types, pool_split_mode, baseline_run_id 等)。
        strategy: 池构建策略实例。
        db_path: harness.db 路径(用于读取 baseline correctness)。

    流程:
        1. 若 pools.json 存在 → 加载
           a. 若 config.task_types 中有类别不在已冻结的 categories → 增量构建并追加
           b. 若 pools.json 的 seed/train_ratio/baseline_run_id 与当前 config 不一致 → 报错
        2. 若不存在 → 从 DB 加载 correctness → strategy.build() → 冻结
        3. 按 config.task_types 从 categories 中组装 Pools 返回
    """

冻结一致性校验:加载已有 pools.json 时,若 split_modeseedtrain_ratiobaseline_run_id 与当前 config 不一致,直接报错并提示删除 pools.json 重建。已冻结类别的划分不可变,新类别可追加。

6.3 向后兼容

旧格式 pools.json(无 split_mode 字段)自动识别为 global 模式。

7. task_types 作用域贯穿

策略在池构建阶段完成所有过滤,下游无需感知 task_types:

阶段 行为
池构建 strategy 只处理指定类别
mini-batch 消费 scoped 后的 diagnosis 池,无改动
per-skill gate 池中只有指定类别的题,自然 scoped
慢进化 用 scoped 后的 val 池评估,照常进化
held-out eval test 池已是 Video-MME 同类题

8. main.py train 接线

# composition root 伪代码
elif config.mode == "train":
    strategy = (PerCategoryPoolStrategy()
                if config.pool_split_mode == "per_category"
                else GlobalPoolStrategy())
    pools = build_or_load_pools(config, strategy)
    await runner.train(pools)

9. 文件改动地图

文件 改动类型 内容
app/ports.py 新增 PoolStrategy Protocol(应用层端口,避免 core → app 逆向依赖)
core/types.py 新增 PoolConfig dataclass
app/harness/pools.py 重构 GlobalPoolStrategy + PerCategoryPoolStrategy + 增量逻辑
app/harness/config.py 修改 RunConfig 新增 4 字段 + 校验;pool_split_mode 用 Literal 类型
config/default.yaml 修改 3 个新配置项
main.py 修改 task_types 纳入 cli_overrides + RunConfigtrain 接线;strategy 组装
app/harness/log.py 微调 _runsINSERT OR IGNORE 改为 ON CONFLICT DO UPDATE(增量 infer 更新时间戳)
tests/unit/test_pools.py 新增 两种策略单元测试
tests/integration/test_pool_strategy.py 新增 端到端集成测试

不改动

  • app/harness/batching.py — 消费 scoped 池,无需感知策略
  • core/evolution/ — 进化引擎与池来源解耦
  • app/harness/inference.py — 推理逻辑不变
  • app/question_gen/ — 题目生成与池构建解耦