plan(tree/repair): 三项改造实现计划(遥测加固+断点续跑+并发)

6 个 Task: telemetry 防御加固 → call_id 根因修复 → detector L2/L1 扩展
→ progress 管理 → 并发编排+CLI → lint+全量测试
This commit is contained in:
2026-07-09 00:08:23 -04:00
parent 8a49ef18e6
commit dbc9d38cd7
9 changed files with 1915 additions and 3 deletions
@@ -0,0 +1,180 @@
---
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 当错题、对题在前返回顺序、题型保底、池不足报错、种子可复现 |
@@ -0,0 +1,9 @@
---
type: design
node_id: design:tree-repair-resilience
title: "建树修复管线:熔断根因修复 + 断点续跑 + 并发改造"
date: 2026-07-09
---
# 建树修复管线:熔断根因修复 + 断点续跑 + 并发改造