--- id: question-gen title: 出题模块迁移设计(question_gen) type: design created: 2026-07-07 status: approved --- # 出题模块迁移设计 ## 1. 目标 从 TRM4 `core/harness/question_gen.py` 迁移出题数据结构与采样逻辑到 TRM5 Clean Architecture,同时预留 LLM 驱动出题的 Protocol 接口。 | 维度 | 说明 | |------|------| | 迁移范围 | benchmark 加载 + 分层采样(纯函数,180 行) | | 预留接口 | `QuestionGenerator` Protocol(不实现,后续参考 TRM4 `research-wiki/designs/2026-07-06-question-gen-synth-design.md`) | | 不做 | LLM 出题实现、校准脚本、去重机制 | ## 2. Clean Architecture 分层决策 ### 2.1 类型放置 `GeneratedQuestion` 被 `core/evolution/`(diagnose、validate)和 `app/harness/`(runner、batching、pools、inference)跨层使用。按依赖方向(core 不可依赖 app),必须放 `core/types.py`,与 `LLMResponse` 同级。 ```mermaid flowchart LR CT["core/types.py\nGeneratedQuestion"] --> CE["core/evolution/\ndiagnose · validate"] CT --> AH["app/harness/\nrunner · batching · pools"] CT --> AQ["app/question_gen/\nloader"] ``` ### 2.2 模块结构 ``` core/types.py ← 追加 GeneratedQuestion app/ports.py ← 追加 QuestionGenerator Protocol app/question_gen/ ├── __init__.py ← 公开 API re-export └── loader.py ← load_benchmark() + stratified_sample() ``` **否决方案**: | 方案 | 否决理由 | |------|---------| | `GeneratedQuestion` 放 `app/question_gen/types.py` | `core/evolution/` 无法 import `app/` 层,违反依赖方向 | | loader / sampler 拆两文件 | sampler 仅 ~100 行,不值得独立文件 | | Protocol 放 `app/question_gen/protocols.py` | 与 `EmbeddingProvider` 在 `app/ports.py` 的既有模式不一致 | ## 3. 类型定义 ### 3.1 GeneratedQuestion(`core/types.py` 追加) ```python @dataclass(frozen=True) class GeneratedQuestion: """单条生成/加载的题目。跨层共享类型。""" question_id: str video_id: str task_type: str question: str options: tuple[str, ...] answer: str source_nodes: tuple[str, ...] difficulty: str ``` **与 TRM4 的有意变更**: | 变更 | 理由 | |------|------| | `options: list → tuple` | 配合 `frozen=True` 不可变语义 | | `source_nodes: list → tuple` | 同上 | | `difficulty` 移除默认值 `"medium"` | 显式传入(§4.1 P4: 显式优于隐式) | **移除 `QuestionGenResult`**:TRM5 无消费者,YAGNI。 ### 3.2 QuestionGenerator Protocol(`app/ports.py` 追加) ```python @runtime_checkable class QuestionGenerator(Protocol): """LLM 驱动的题目生成端口(预留接口)。""" async def generate( self, video_id: str, task_type: str, tree: TreeIndex, *, exemplars: list[GeneratedQuestion], ) -> GeneratedQuestion: ... ``` 接口设计参考 TRM4 仓库 `research-wiki/designs/2026-07-06-question-gen-synth-design.md`(位于 `/home/iomgaa/Projects/Video-Tree-TRM4/`,不复制到 TRM5)中的"题型-层级映射 + few-shot exemplar"模式。`tree` 参数提供锚节点上下文,`exemplars` 提供风格示例。具体实现在后续 `tools/generate_questions.py`(一次性脚本)中完成,通过 `adapters/` 层的 Protocol 实现注入。 ## 4. 函数接口 ### 4.1 load_benchmark ``` load_benchmark(questions_dir: Path) -> list[GeneratedQuestion] ``` 从指定目录 glob `*.json`,每个文件以 `stem` 为 `video_id`,解析为 `GeneratedQuestion` 列表。JSON 格式与 `store/questions/benchmarks/Video-MME/*.json` 完全一致。 **与 TRM4 对比**:算法 100% 保真。`options` 和 `source_nodes` 转为 `tuple`。 **`difficulty` 字段处理规则**:现有 benchmark JSON(`store/questions/benchmarks/Video-MME/`)不含 `difficulty` 字段,这是 legacy schema 特征。加载时按如下规则显式转换(非默认值掩盖): | JSON 情况 | 处理 | |-----------|------| | 有 `difficulty` 字段 | 取 JSON 值 | | 无 `difficulty` 字段 | 赋 `_LEGACY_DEFAULT_DIFFICULTY = "medium"` 常量 | 常量集中定义在 `loader.py` 顶部,测试用例覆盖两种情况。 ### 4.2 stratified_sample ``` stratified_sample( questions: list[GeneratedQuestion], correctness: dict[str, bool], size: int, correct_ratio: float | None, task_types: list[str] | None, seed: int, min_per_class: int | None, ) -> list[GeneratedQuestion] ``` 所有参数显式传入,无默认值(§4.1 P4)。 **算法保真清单**(逐一比对 TRM4): | 逻辑点 | TRM4 行为 | TRM5 保持 | |--------|----------|----------| | `task_types` 过滤 | `task_types` 非 None 时,先过滤 pool 只保留指定题型 | 保持 | | `correct_ratio=None` | 自然分布分支,随机抽样 `size` 道 | 保持 | | `correct_ratio` 有值 | 按对错比例分层,对题 `round(size * ratio)` | 保持 | | `correctness.get(id, False)` | 未知 correctness 的题统一当错题处理 | 保持 | | 分层返回顺序 | 对题在前、错题在后 | 保持 | | 池不足 | `ValueError` 报错,不静默降级 | 保持 | | `min_per_class` 补足 | 遍历 pool 全部题型(非仅 sampled 命中的),按首次出现顺序确定性枚举 | 保持 | | 补足不足时 | 全取,不报错 | 保持 | | 随机种子 | `random.Random(seed)` 局部实例 | 保持 | 内部辅助函数 `_ratio_stratified_sample` 和 `_backfill_per_class` 完整保留。 ## 5. 职责边界 | 组件 | 职责 | 位置 | 谁 import 谁 | |------|------|------|-------------| | `GeneratedQuestion` | 题目数据结构 | `core/types.py` | 被所有层 import | | `load_benchmark` / `stratified_sample` | 加载 + 采样 | `app/question_gen/loader.py` | 被 `app/harness/` import | | `QuestionGenerator` Protocol | LLM 出题接口定义 | `app/ports.py` | 被未来 `adapters/` 实现 | | `tools/generate_questions.py`(未来) | LLM 出题一次性脚本 | `tools/` | 独立工具,不被其他模块 import | `tools/generate_questions.py` 未来可实例化 `QuestionGenerator` 的 adapter 实现,但 `tools/` 本身不被 `app/` import(§5 硬性规则)。 ## 6. 文档同步 以下章节需要更新: | 文档 | 章节 | 变更 | |------|------|------| | `ARCHITECTURE.md` §1 表格 | DataLoader 行 `app/question_gen/generator.py` | → `app/question_gen/loader.py` | | `ARCHITECTURE.md` §2.2 Mermaid | `QGEN` 节点 `generator.py` | → `loader.py` | | `CLAUDE.md` §1.5 表格 | DataLoader 行 `app/question_gen/generator.py` | → `app/question_gen/loader.py` | **不变更**:`ARCHITECTURE.md §6` 核心算法保真清单 — `stratified_sample` 是采样工具函数,不属于 13 项核心算法(那些是建树 + 训练的关键算法)。 ## 7. 测试策略 | 测试 | 路径 | 覆盖点 | |------|------|--------| | `GeneratedQuestion` 冻结性 | `tests/unit/test_core_types.py`(追加) | frozen 不可变、字段完整性 | | `load_benchmark` | `tests/unit/test_question_loader.py` | 正常加载、空目录、JSON 格式异常 | | `stratified_sample` | `tests/unit/test_question_loader.py` | 自然分布、分层采样、题型过滤、未知 correctness 当错题、对题在前返回顺序、题型保底、池不足报错、种子可复现 |