docs(wiki): 赛题生成工具设计 + 实现计划
design: synthesizer + factory + CLI 三模块架构 plan: 9 个 Task(前置修复 + synthesizer 4 步 + factory + CLI generate/calibrate + re-export) calibrate: Fisher exact test 组合判定替代固定阈值 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
---
|
||||
id: question-gen-synth
|
||||
title: 赛题生成工具设计(Question Generation Synthesis)
|
||||
type: design
|
||||
created: 2026-07-09
|
||||
status: draft
|
||||
---
|
||||
|
||||
# 赛题生成工具设计
|
||||
|
||||
## 1. 目标与动机
|
||||
|
||||
让视频树自行生成与 Video-MME 原始赛题风格、难度近似的四选一选择题,用于自进化训练循环的 DataLoader。原始 900 道 benchmark 题保留为 held-out 最终评测集,避免"直接拿答案调"的审稿质疑。
|
||||
|
||||
**角色定位**:生成题 = 训练集,原始题 = 测试集。进化循环的改进效果最终由原始 benchmark 验证泛化能力。
|
||||
|
||||
**训练 vs 论文评测的区分**:训练循环全程使用生成题(三池切分——诊断池/验证池/test 池——均来自生成题),论文报告的 held-out 泛化指标是训练结束后,用最终 best 版本对原始 benchmark 全量 900 题单独跑推理得到的结果。两步分离,Runner 代码无需改动。
|
||||
|
||||
## 2. 模块结构与职责边界
|
||||
|
||||
### 2.1 文件布局
|
||||
|
||||
```
|
||||
app/question_gen/
|
||||
├── __init__.py ← 已有:re-export loader API
|
||||
├── loader.py ← 已有:load_benchmark + stratified_sample
|
||||
└── synthesizer.py ← 新增①:出题核心逻辑
|
||||
|
||||
app/harness/
|
||||
└── factory.py ← 新增②:推理依赖组装(wiring)
|
||||
|
||||
tools/generate_questions.py ← 新增③:CLI 壳(generate + calibrate)
|
||||
```
|
||||
|
||||
### 2.2 职责切分
|
||||
|
||||
| 模块 | 职责 | 消费者 |
|
||||
|------|------|--------|
|
||||
| `synthesizer.py` | 题型-层级映射、锚节点采样、prompt 构造(few-shot)、embedding 去重、单题生成编排 | `tools/generate_questions.py` |
|
||||
| `factory.py` | 给定 store 路径 + config → 组装 LLM/VLM/Embedding/SearchToolDispatcher/PromptManager 全套推理依赖 | `tools/generate_questions.py`(校准)、未来 `main.py`、Runner |
|
||||
| `tools/generate_questions.py` | CLI 参数解析、并发编排(Semaphore)、进度日志、JSON 输出 | 用户直接运行 |
|
||||
|
||||
### 2.3 依赖方向
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
TOOLS["tools/generate_questions.py"] --> SYN["app/question_gen/synthesizer"]
|
||||
TOOLS --> FAC["app/harness/factory"]
|
||||
TOOLS --> ADP["adapters/*"]
|
||||
FAC --> SEARCH["app/search/*"]
|
||||
FAC --> ENV["app/tree/environment"]
|
||||
FAC --> ADP
|
||||
SYN --> PROTO["core/protocols (VLMProvider, EmbeddingProvider via DI)"]
|
||||
SYN --> TYPES["core/types (GeneratedQuestion)"]
|
||||
SYN --> IDX["app/tree/index (TreeIndex)"]
|
||||
```
|
||||
|
||||
全部合规——外层→内层,`core/` 不依赖任何外层。
|
||||
|
||||
### 2.4 与 QuestionGenerator Protocol 的关系
|
||||
|
||||
`app/ports.py` 已预留 `QuestionGenerator` Protocol。本设计**不实现该 Protocol**——出题是一次性离线工具而非运行时能力,Runner 不需要运行时出题。`synthesizer.py` 的函数式接口(`generate_one` 等纯函数 + async 编排)比 Protocol class 更适合工具脚本场景。`QuestionGenerator` Protocol 保留但标记为"预留,当前无实现",不删除——若未来需要运行时出题可基于 synthesizer 的纯函数包装实现。
|
||||
|
||||
### 2.5 方案选择与否决
|
||||
|
||||
| 方案 | 否决理由 |
|
||||
|------|---------|
|
||||
| A: 单体脚本(全部逻辑放 `tools/`) | 业务逻辑(题型映射、采样、prompt、去重)混在 CLI 编排中,不可独立测试;不匹配 repair 管线的 app/ + tools/ 分层惯例 |
|
||||
| B: Protocol 实现 + 脚本编排(`adapters/` 实现 `QuestionGenerator`) | adapter 层语义是外部服务接口,出题逻辑是应用层业务规则,放 adapter 层语义不匹配 |
|
||||
| **C: app/ 业务逻辑 + tools/ CLI 壳(采用)** | 与 repair 管线结构一致,Clean Architecture 依赖方向合规,业务逻辑可独立测试 |
|
||||
|
||||
## 3. synthesizer.py 核心设计
|
||||
|
||||
### 3.1 题型-层级映射
|
||||
|
||||
模块级常量,沿用 TRM4 设计文档的映射表:
|
||||
|
||||
| 锚定层级 | 题型 | 帧图 | 文本上下文 | 帧数 |
|
||||
|---------|------|------|-----------|------|
|
||||
| L3 | Object Recognition | 必须 | frame_summary | 1 |
|
||||
| L3 | Attribute Perception | 必须 | frame_summary | 1 |
|
||||
| L3 | OCR Problems | 必须 | frame_summary | 1 |
|
||||
| L3 | Spatial Reasoning | 必须 | frame_summary + spatial_layout | 1 |
|
||||
| L3 | Spatial Perception | 必须 | frame_summary | 1 |
|
||||
| L2 | Action Recognition | 必须 | 事件 card | 2-3(子帧均匀采样) |
|
||||
| L2 | Action Reasoning | 必须 | 事件 card | 2-3 |
|
||||
| L2 | Counting Problem | 必须 | 事件 card | 2-3 |
|
||||
| L2 | Temporal Perception | 可选 | 事件 card + time_range | 0-1 |
|
||||
| L1 | Temporal Reasoning | 必须 | 根 card + 多个 L2 card(≥3) | 每 L2 取 1 张代表帧 |
|
||||
| L1 | Information Synopsis | 必须 | 根 card + 全部 L2 card | 每 L2 取 1 张代表帧 |
|
||||
| L1-L2 | Object Reasoning | 必须 | 2-3 个 L2 card | 每 L2 取 1 张代表帧 |
|
||||
|
||||
节点采样:每道题从全部视频树中随机选一棵,在对应层级随机选一个锚节点。同视频同题型不重复。L1 题型使用多个 L2 子节点联合输入时,按时间顺序组织节点,保持叙事连贯性。
|
||||
|
||||
### 3.2 AnchorContext 数据结构
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class AnchorContext:
|
||||
"""锚节点上下文——生成单道题所需的全部素材。"""
|
||||
node_id: str # 锚节点 ID
|
||||
card_text: str # 锚节点 card 序列化文本
|
||||
frame_paths: list[str] # 帧图片路径
|
||||
subtitle: str # 对应字幕(可空)
|
||||
distractor_texts: list[str] # 同视频其他节点摘要(供 VLM 生成干扰项)
|
||||
```
|
||||
|
||||
### 3.3 核心函数签名
|
||||
|
||||
```python
|
||||
# 纯函数:从树中采样锚节点 + 帧 + 上下文
|
||||
def sample_anchor(
|
||||
tree: TreeIndex,
|
||||
task_type: str,
|
||||
used_node_ids: set[str],
|
||||
rng: random.Random,
|
||||
) -> AnchorContext
|
||||
|
||||
# 纯函数:组装 VLM prompt(system + user,含 few-shot exemplar)
|
||||
def build_generation_prompt(
|
||||
task_type: str,
|
||||
anchor: AnchorContext,
|
||||
exemplars: list[GeneratedQuestion],
|
||||
) -> tuple[list[dict], list[str]]
|
||||
# 返回:(messages, image_paths) — 直接喂给 VLMProvider
|
||||
|
||||
# 纯函数:解析 VLM 返回的 JSON → 部分字段字典
|
||||
# source_nodes 和 difficulty 由 generate_one 在 parse 后用 anchor 信息补齐
|
||||
def parse_vlm_response(
|
||||
raw: str,
|
||||
video_id: str,
|
||||
task_type: str,
|
||||
seq: int,
|
||||
) -> dict
|
||||
# 返回:{"question_id", "question", "options", "answer"} 字典
|
||||
# 调用方补齐 source_nodes/difficulty 后构造 GeneratedQuestion
|
||||
|
||||
# 纯函数:embedding 去重判定
|
||||
def is_duplicate(
|
||||
question_text: str,
|
||||
pool_embeddings: np.ndarray,
|
||||
embed_fn: Callable[[str | list[str]], np.ndarray],
|
||||
threshold: float,
|
||||
) -> bool
|
||||
|
||||
# 异步编排:生成单道题(含重试 + 去重循环)
|
||||
async def generate_one(
|
||||
vlm: VLMProvider,
|
||||
embed_fn: Callable[[str | list[str]], np.ndarray],
|
||||
tree: TreeIndex,
|
||||
video_id: str,
|
||||
task_type: str,
|
||||
seq: int,
|
||||
*,
|
||||
exemplars: list[GeneratedQuestion],
|
||||
pool_embeddings: np.ndarray,
|
||||
used_node_ids: set[str],
|
||||
max_retries: int,
|
||||
similarity_threshold: float,
|
||||
rng: random.Random,
|
||||
session_id: str,
|
||||
) -> GeneratedQuestion | None
|
||||
```
|
||||
|
||||
**设计要点**:
|
||||
- 纯函数(sample_anchor、build_generation_prompt、parse_vlm_response、is_duplicate)可独立单测,不需要 VLM
|
||||
- `generate_one` 是唯一异步函数,接收 `VLMProvider` 通过 DI
|
||||
- 干扰项来自 `AnchorContext.distractor_texts`——同视频其他节点的真实信息
|
||||
|
||||
### 3.4 few-shot exemplar 选择
|
||||
|
||||
生成 prompt 包含 2-3 道同题型的原始 benchmark 题作示例,对齐风格和难度。
|
||||
|
||||
选择策略:
|
||||
- 每题型取 `min(3, 该题型 benchmark 总量)` 道
|
||||
- 按 seed 随机采样 + 跨视频去重(避免 exemplar 全来自同一视频)
|
||||
- exemplar 是只读引用,不从 benchmark 评测集中移除
|
||||
|
||||
### 3.5 prompt 结构
|
||||
|
||||
```
|
||||
System: 视频理解题目生成器,根据视频树节点内容和帧图生成 {task_type} 四选一题。
|
||||
|
||||
[2-3 道该题型原始 benchmark 题作示例]
|
||||
|
||||
约束:
|
||||
- 问题必须基于给定节点内容,不能靠常识推断
|
||||
- 干扰项来自同视频其他节点的真实信息(非凭空捏造)
|
||||
- 难度和问法风格与示例一致
|
||||
|
||||
User: [锚节点 card + 字幕 + 帧图] + [同视频其他节点摘要,供干扰项素材]
|
||||
```
|
||||
|
||||
### 3.6 去重机制
|
||||
|
||||
用 `EmbeddingProvider`(nomic-embed-text-v1.5)对 question 文本做 embedding,余弦相似度检查:
|
||||
|
||||
| 检查对 | 阈值 | 处理 |
|
||||
|--------|------|------|
|
||||
| 生成题 vs 原始 benchmark 同题型题 | ≥ similarity_threshold | 丢弃,换节点重试 |
|
||||
| 生成题 vs 已生成的同题型题 | ≥ similarity_threshold | 丢弃,换节点重试 |
|
||||
|
||||
维护 embedding 池(原始题 + 已通过的生成题),每生成一道新题即时查重。单题最多重试 `max_retries` 次。某题型连续耗尽重试配额时,脚本报错退出并输出已完成/未完成的题型统计,不静默少题。
|
||||
|
||||
**并发去重安全**:embedding 池的"检查 + 添加"必须是原子操作。并发 `generate_one` 任务成功后,通过单线程汇总点(asyncio.Queue 或 await 后顺序提交)更新 embedding 池 + 写 JSON + 更新 progress,避免竞态导致相似题同时通过。
|
||||
|
||||
## 4. factory.py 推理依赖组装
|
||||
|
||||
### 4.1 解决的问题
|
||||
|
||||
目前 `Runner._make_tool_dispatch_fn()` 和 `_make_prompt_builder()` 都是 `raise NotImplementedError`,设计为"由 main.py 注入"。组装逻辑涉及 adapter 实例化 + app 组件串联,应提取为可复用的 factory 函数,避免在每个调用方(tools/ 脚本、未来 main.py)重复 wiring。
|
||||
|
||||
### 4.2 核心接口
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class InferenceDeps:
|
||||
"""跑一次推理所需的全套依赖(不含 HarnessLog,其生命周期由调用方管理)。"""
|
||||
llm: LLMProvider
|
||||
tool_dispatch_fn: Callable # SearchToolDispatcher.dispatch
|
||||
prompt_builder: Callable # PromptManager 的偏函数
|
||||
|
||||
def build_inference_deps(
|
||||
*,
|
||||
store_dir: Path,
|
||||
video_id: str,
|
||||
prompts_dir: Path,
|
||||
skills_dir: Path | None,
|
||||
skill_mode: str,
|
||||
embed_provider: EmbeddingProvider,
|
||||
llm: LLMProvider,
|
||||
vlm: VLMProvider,
|
||||
ocr: OCRProvider | None,
|
||||
verify_vision: bool,
|
||||
anchor: bool,
|
||||
assemble_mode: str,
|
||||
) -> InferenceDeps
|
||||
```
|
||||
|
||||
### 4.3 内部流程
|
||||
|
||||
```
|
||||
build_inference_deps()
|
||||
├── 加载 TreeIndex(store_dir/videos/{video_id}/tree.json)
|
||||
├── 构建 TreeEnvironment(index=tree, frames_dir=videos/{video_id}/frames)
|
||||
├── 构建 SkillRegistry(skills_dir,可选)
|
||||
├── 构建 SearchToolDispatcher(env, tool_llm, vlm, ocr, prompts_dir,
|
||||
│ skills, embed_fn, verify_vision, anchor, assemble_mode)
|
||||
├── 构建 PromptManager(prompts_dir)→ 偏函数化 prompt_builder(绑定 skill_mode)
|
||||
└── 返回 InferenceDeps
|
||||
```
|
||||
|
||||
注意:`HarnessLog` 不放入 `InferenceDeps`——其生命周期由调用方通过 `with HarnessLog(...) as log` 管理,作为参数传给 `run_inference`。
|
||||
|
||||
### 4.4 消费者
|
||||
|
||||
| 消费者 | 用法 |
|
||||
|--------|------|
|
||||
| `tools/generate_questions.py` calibrate | 按 video_id 分组题目,对每组调 `build_inference_deps` 构建对应视频树的依赖 → 分组 `run_inference` |
|
||||
| 未来 `main.py --mode infer` | CLI 参数映射到 factory 参数 |
|
||||
| `Runner` | `_make_tool_dispatch_fn` / `_make_prompt_builder` 改为委托 factory |
|
||||
|
||||
### 4.5 设计约束
|
||||
|
||||
- factory 只做**组装**,不持有状态——每次调用返回独立的 `InferenceDeps`
|
||||
- adapter 实例(LLM/VLM/Embedding)由调用方创建并传入,factory 不管 adapter 生命周期
|
||||
- 调用方自由决定 adapter 的复用策略(共享 vs 按需创建)
|
||||
|
||||
## 5. tools/generate_questions.py CLI 设计
|
||||
|
||||
### 5.1 子命令
|
||||
|
||||
```bash
|
||||
# 生成
|
||||
python tools/generate_questions.py generate \
|
||||
--store-dir store \
|
||||
--output-dir store/questions/generated/Video-MME \
|
||||
--per-type 20 \
|
||||
--similarity-threshold 0.85 \
|
||||
--max-retries 3 \
|
||||
--concurrency 8 \
|
||||
--seed 42
|
||||
|
||||
# 校准(生成题 vs benchmark 基线对比)
|
||||
python tools/generate_questions.py calibrate \
|
||||
--generated-dir store/questions/generated/Video-MME \
|
||||
--benchmark-dir store/questions/benchmarks/Video-MME \
|
||||
--store-dir store \
|
||||
--db-path results/calibrate.db \
|
||||
--prompts-dir store/prompts \
|
||||
--concurrency 4 \
|
||||
--max-steps 15 \
|
||||
--skill-mode auto \
|
||||
--tolerance 0.10 \
|
||||
--alpha 0.05 \
|
||||
--baseline-db <可选,已有基线 DB 路径> \
|
||||
--baseline-run-id <可选,已有基线 run_id>
|
||||
```
|
||||
|
||||
除 baseline 复用参数外均必传,无默认值(CLAUDE.md §4.5)。`--baseline-db` + `--baseline-run-id` 可选但必须成对出现:有则从 DB 读 benchmark 基线,无则自动跑一次 benchmark 推理。
|
||||
|
||||
### 5.2 generate 流程
|
||||
|
||||
```
|
||||
1. 加载 300 棵树的 video_id 列表
|
||||
2. 加载 benchmark 题目(作为 few-shot exemplar 来源)
|
||||
3. 初始化 embedding 池(benchmark 题 question text → embedding)
|
||||
4. 实例化 GovernedVLMClient + EmbeddingProvider
|
||||
5. 检查断点续跑文件(progress.json)
|
||||
6. 对 12 题型 × per_type:
|
||||
├── 跳过已完成的(断点续跑)
|
||||
├── 随机选视频 + 锚节点(同视频同题型不重复)
|
||||
├── asyncio.Semaphore(concurrency) 并发调 generate_one
|
||||
├── 成功 → 加入 embedding 池 + 追加到结果 + 更新 progress
|
||||
└── 连续耗尽重试 → 报错退出,输出已完成/未完成统计
|
||||
7. 按 video_id 分组写入 JSON
|
||||
8. 全部完成后删除 progress.json
|
||||
```
|
||||
|
||||
### 5.3 calibrate 流程
|
||||
|
||||
```
|
||||
1. load_benchmark 加载生成题和 benchmark 题
|
||||
2. 获取 benchmark 基线:
|
||||
├── 有 --baseline-db + --baseline-run-id → 从 DB 读 per_task_type accuracy
|
||||
└── 没有 → 按 video_id 分组 benchmark 题 → 每组 build_inference_deps
|
||||
→ 分组 run_inference → 汇总存 DB
|
||||
3. 按 video_id 分组生成题 → 每组 build_inference_deps → 分组 run_inference
|
||||
(每组使用对应视频的 TreeEnvironment,避免跨视频树错用)
|
||||
4. 汇总两组 per_task_type accuracy,对比(Fisher exact test)
|
||||
5. 输出对比表 + 判定结果
|
||||
6. 存在 FAIL → 退出码 1
|
||||
```
|
||||
|
||||
### 5.4 tools/ 脚本职责边界
|
||||
|
||||
脚本**只做**:argparse、adapter 实例化(读 `.env`)、Semaphore 并发、进度日志(loguru)、JSON 写入、calibrate 时调 factory + run_inference。
|
||||
|
||||
脚本**不做**:prompt 构造、节点采样、去重判定(synthesizer.py)、依赖组装逻辑(factory.py)。
|
||||
|
||||
## 6. 校准统计方法
|
||||
|
||||
### 6.1 问题
|
||||
|
||||
benchmark 题型分布极不均匀(Spatial Perception 仅 3 道 vs Object Reasoning 240 道),固定 10% 阈值对小样本题型会产生误判——单题翻转即 33% 波动。
|
||||
|
||||
### 6.2 组合判定:Fisher exact test + effect size
|
||||
|
||||
用 `scipy.stats.fisher_exact` 对每个题型构造 2×2 列联表:
|
||||
|
||||
| | 答对 | 答错 |
|
||||
|--|------|------|
|
||||
| Benchmark | a | b |
|
||||
| Generated | c | d |
|
||||
|
||||
判定规则:
|
||||
|
||||
| \|Δ\| > tolerance | p < α | 判定 | 含义 |
|
||||
|---|---|---|---|
|
||||
| ✗ | — | **PASS** | 差异在容忍范围内 |
|
||||
| ✓ | ✓ | **FAIL** | 差异大且统计显著——生成题难度确实偏了 |
|
||||
| ✓ | ✗ | **WARN** | 差异大但样本不足以确认——可能是噪声 |
|
||||
|
||||
### 6.3 优势
|
||||
|
||||
- 不需要 ad-hoc 的 `min_calibrate_size` 参数
|
||||
- 小样本题型自动降级为 WARN——Fisher test 的 p-value 天然反映样本量不足
|
||||
- CLI 只需两个语义清晰的统计参数:`--tolerance 0.10` + `--alpha 0.05`
|
||||
- 退出码只看是否存在 FAIL(WARN 不阻塞)
|
||||
|
||||
### 6.4 检测灵敏度与 per_type 的关系
|
||||
|
||||
| per_type | 可检出的最小差异(大样本 benchmark 侧) |
|
||||
|----------|---------------------------------------|
|
||||
| 20 | ~30%(仅极大差异) |
|
||||
| 50 | ~15%(中等差异) |
|
||||
|
||||
用户可根据需要的检测灵敏度选择 `--per-type`。
|
||||
|
||||
### 6.5 输出格式
|
||||
|
||||
```
|
||||
题型 | bench | gen | Δ | p-value | 判定
|
||||
-------------------|--------|--------|---------|---------|--------
|
||||
Spatial Perception | 66.7% | 40.0% | -26.7% | 0.590 | ⚠ WARN
|
||||
Action Reasoning | 72.2% | 68.0% | -4.2% | 0.712 | ✓ PASS
|
||||
Object Reasoning | 60.0% | 30.0% | -30.0% | 0.016 | ✗ FAIL
|
||||
```
|
||||
|
||||
## 7. 断点续跑
|
||||
|
||||
生成 240 道题可能中断(VLM 故障、手动 Ctrl-C),沿用项目已有的 progress.json 模式:
|
||||
|
||||
```json
|
||||
{
|
||||
"completed": {
|
||||
"Action Reasoning": ["gen-xyz-001", "gen-xyz-002"],
|
||||
"Object Recognition": ["gen-abc-001"]
|
||||
},
|
||||
"output_dir": "store/questions/generated/Video-MME"
|
||||
}
|
||||
```
|
||||
|
||||
- 启动时检查 `{output_dir}/progress.json`,跳过已完成的题
|
||||
- **恢复 embedding 池**:从已写出的 `{output_dir}/*.json` 重建已生成题的 embedding + `used_node_ids`,避免续跑后产生重复题
|
||||
- 每道题写入 JSON 后立即更新 progress
|
||||
- 全部完成后删除 progress.json
|
||||
|
||||
## 8. 输出格式
|
||||
|
||||
输出路径:`store/questions/generated/Video-MME/{video_id}.json`
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"question_id": "gen-{video_id}-{seq}",
|
||||
"task_type": "Action Reasoning",
|
||||
"question": "...",
|
||||
"options": ["A. ...", "B. ...", "C. ...", "D. ..."],
|
||||
"answer": "B",
|
||||
"source_nodes": ["L1_000_L2_003"],
|
||||
"difficulty": "medium"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
与 loader schema 兼容(额外 `source_nodes`/`difficulty` 字段用于溯源),`load_benchmark` 零改动直接加载。
|
||||
|
||||
**训练集成**:`--questions generated/Video-MME`。
|
||||
|
||||
## 9. 受影响的既有接口
|
||||
|
||||
| 接口 | 影响 | 适配 |
|
||||
|------|------|------|
|
||||
| `load_benchmark` | 无 | 输出与 loader schema 兼容(额外 source_nodes/difficulty 字段用于溯源) |
|
||||
| `RunConfig.questions` | 无 | 传 `generated/Video-MME` |
|
||||
| `build_or_load_pools` | 无 | 三池均来自生成题 |
|
||||
| `Runner._make_tool_dispatch_fn` | 改造 | 委托 factory.py |
|
||||
| `Runner._make_prompt_builder` | 改造 | 委托 factory.py |
|
||||
| `_VIDEO_MME_TASK_TYPE_COUNT` | **前置修复** | 从 11 改为 12(`app/harness/config.py:24`),影响验证池保底下限 |
|
||||
|
||||
## 10. 测试策略
|
||||
|
||||
### 10.1 synthesizer.py
|
||||
|
||||
| 测试 | 覆盖点 |
|
||||
|------|--------|
|
||||
| `test_sample_anchor` | 各层级题型正确采锚、同视频同题型不重复、树节点不足时报错 |
|
||||
| `test_build_generation_prompt` | messages 结构正确、exemplar 注入、图片路径列表、干扰项素材包含 |
|
||||
| `test_parse_vlm_response` | 正常解析、格式异常(缺字段/非法 JSON)报错 |
|
||||
| `test_is_duplicate` | 相似度 ≥ 阈值判重、< 阈值通过、空池不判重 |
|
||||
| `test_generate_one` | mock VLMProvider,验证重试+去重循环、耗尽重试返回 None |
|
||||
|
||||
### 10.2 factory.py
|
||||
|
||||
| 测试 | 覆盖点 |
|
||||
|------|--------|
|
||||
| `test_build_inference_deps` | fake LLM/VLM/Embedding 验证返回各字段非 None、类型正确 |
|
||||
| `test_missing_tree_file` | tree.json 不存在时报错 |
|
||||
|
||||
### 10.3 tools/generate_questions.py(集成级)
|
||||
|
||||
| 测试 | 覆盖点 |
|
||||
|------|--------|
|
||||
| `test_generate_smoke` | mock VLM + 1 棵真实树 + per_type=1,验证 JSON 输出格式 |
|
||||
| `test_progress_resume` | 中断后重启,跳过已完成题 |
|
||||
| `test_calibrate_pass_fail` | mock 两组 accuracy,验证 Fisher + tolerance 组合判定 |
|
||||
|
||||
真实 VLM 调用的 integration test 不在此次范围——依赖外部服务,不适合 CI。
|
||||
|
||||
## 11. 实现约束
|
||||
|
||||
- 完整类型注解 + 中文 Docstring(CLAUDE.md §4.2)
|
||||
- 禁用 `print()`,使用 loguru(CLAUDE.md §4.2)
|
||||
- 脚本放 `tools/`,不被其他模块 import(CLAUDE.md §5)
|
||||
- 并发模式:`asyncio.Semaphore`,CLI `--concurrency` 指定(沿用项目既有模式)
|
||||
- 所有 VLM 调用经过 `GovernedLLMClient` 治理栈(CLAUDE.md §4.9)
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
type: design
|
||||
node_id: design:question-gen-synth
|
||||
title: 赛题生成工具设计
|
||||
date: 2026-07-09
|
||||
---
|
||||
|
||||
# 赛题生成工具设计
|
||||
|
||||
Reference in New Issue
Block a user