Files
Video-Tree-TRM5/research-wiki/designs/2026-07-07-question-gen-design.md
T
iomgaa dbc9d38cd7 plan(tree/repair): 三项改造实现计划(遥测加固+断点续跑+并发)
6 个 Task: telemetry 防御加固 → call_id 根因修复 → detector L2/L1 扩展
→ progress 管理 → 并发编排+CLI → lint+全量测试
2026-07-09 00:08:23 -04:00

181 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 当错题、对题在前返回顺序、题型保底、池不足报错、种子可复现 |