feat: thread and persist sub_pattern into accepted questions

This commit is contained in:
2026-07-14 13:45:31 -04:00
parent 25f2a845ff
commit ae0a718f67
4 changed files with 98 additions and 25 deletions
+4
View File
@@ -251,6 +251,7 @@ def _to_generated_question(
family: str,
options: tuple[str, ...] | None = None,
answer: str | None = None,
sub_pattern: str | None = None,
) -> GeneratedQuestion:
"""将 CandidateQuestion 转换为 GeneratedQuestion。
@@ -259,6 +260,7 @@ def _to_generated_question(
family: 问题家族名称(如 "RETRIEVAL")。
options: 洗牌后的选项元组(若为 None 则使用 candidate 原始选项)。
answer: 重映射后的答案字母(若为 None 则使用 candidate 原始答案)。
sub_pattern: 出题子模式标识(AR 特化策略使用,None 表示无)。
返回:
GeneratedQuestion 实例(difficulty_steps 初始为 None)。
@@ -275,6 +277,7 @@ def _to_generated_question(
family=family,
skill_target=candidate.skill_target,
difficulty_steps=None,
sub_pattern=sub_pattern,
)
@@ -531,6 +534,7 @@ async def _process_one_slot(
family=strategy.strategy_name,
options=pp.options,
answer=pp.answer,
sub_pattern=sub_pattern.name if sub_pattern else None,
)
# 将题目 embedding 加入池(flatten 确保 1D
embed_pool.append(embed_fn(candidate.question).flatten())
+2
View File
@@ -48,6 +48,7 @@ class GeneratedQuestion:
family: 问题家族名称(如 "RETRIEVAL",v2 出题管线使用,None 表示未指定)。
skill_target: 目标技能标识(v2 出题管线使用,None 表示未指定)。
difficulty_steps: 推理步数估计(v2 出题管线使用,None 表示未指定)。
sub_pattern: 出题子模式标识(AR 特化策略使用,None 表示无)。
"""
question_id: str
@@ -61,6 +62,7 @@ class GeneratedQuestion:
family: str | None = field(default=None)
skill_target: str | None = field(default=None)
difficulty_steps: int | None = field(default=None)
sub_pattern: str | None = field(default=None)
@dataclass(frozen=True)
@@ -0,0 +1,73 @@
"""GeneratedQuestion.sub_pattern 字段 + _to_generated_question 透传。"""
from app.question_gen.generator_v2 import CandidateQuestion
from app.question_gen.pipeline_v2 import _to_generated_question
from core.types import GeneratedQuestion
def _candidate() -> CandidateQuestion:
return CandidateQuestion(
question_id="v1_Action Recognition_0001",
video_id="v1",
task_type="Action Recognition",
skill_target="M1_AR",
question="厨师最终采用了哪种烹饪方式?",
options=("A. 蒸", "B. 炒", "C. 煮", "D. 炸"),
answer="A",
source_nodes=("n1", "n2"),
difficulty="hard",
)
def test_generated_question_has_sub_pattern_default_none():
q = GeneratedQuestion(
question_id="q1", video_id="v1", task_type="Action Recognition",
question="?", options=("A. x",), answer="A",
source_nodes=("n1",), difficulty="easy",
)
assert q.sub_pattern is None
def test_to_generated_question_threads_sub_pattern():
q = _to_generated_question(
_candidate(), family="ACTION_RECOGNITION",
sub_pattern="premature_evidence_anchoring",
)
assert q.sub_pattern == "premature_evidence_anchoring"
def test_to_generated_question_sub_pattern_defaults_none():
q = _to_generated_question(_candidate(), family="RETRIEVAL")
assert q.sub_pattern is None
def test_question_to_entry_includes_sub_pattern():
from tools.generate_questions import _question_to_entry
q = GeneratedQuestion(
question_id="v1_Action Recognition_0001", video_id="v1",
task_type="Action Recognition", question="?",
options=("A. 蒸", "B. 炒", "C. 煮", "D. 炸"), answer="A",
source_nodes=("n1",), difficulty="hard",
family="ACTION_RECOGNITION", skill_target="M1_AR",
sub_pattern="temporal_reasoning_failure",
)
entry = _question_to_entry(q)
assert entry["sub_pattern"] == "temporal_reasoning_failure"
assert entry["question_id"] == "v1_Action Recognition_0001"
assert entry["options"] == ["A. 蒸", "B. 炒", "C. 煮", "D. 炸"]
def test_append_to_json_writes_sub_pattern(tmp_path):
from tools.generate_questions import _append_to_json
q = GeneratedQuestion(
question_id="v1_Action Recognition_0001", video_id="v1",
task_type="Action Recognition", question="?",
options=("A. 蒸", "B. 炒", "C. 煮", "D. 炸"), answer="A",
source_nodes=("n1",), difficulty="hard",
family="ACTION_RECOGNITION", skill_target="M1_AR",
sub_pattern="temporal_reasoning_failure",
)
_append_to_json(tmp_path, q)
import json
data = json.loads((tmp_path / "v1.json").read_text(encoding="utf-8"))
assert data[0]["sub_pattern"] == "temporal_reasoning_failure"
+19 -25
View File
@@ -323,6 +323,23 @@ def _build_embed_provider():
# ---------------------------------------------------------------------------
def _question_to_entry(question: GeneratedQuestion) -> dict:
"""将题目序列化为 JSON entry_append_to_json 与 _on_accept 共用)。"""
return {
"question_id": question.question_id,
"video_id": question.video_id,
"task_type": question.task_type,
"question": question.question,
"options": list(question.options),
"answer": question.answer,
"source_nodes": list(question.source_nodes),
"difficulty": question.difficulty,
"family": question.family,
"skill_target": question.skill_target,
"sub_pattern": question.sub_pattern,
}
def _append_to_json(output_dir: Path, question: GeneratedQuestion) -> None:
"""将生成的题目追加到对应 video_id 的 JSON 文件。
@@ -341,17 +358,7 @@ def _append_to_json(output_dir: Path, question: GeneratedQuestion) -> None:
logger.warning("读取 {} 失败,覆盖写入", json_path)
existing = []
entry = {
"question_id": question.question_id,
"task_type": question.task_type,
"question": question.question,
"options": list(question.options),
"answer": question.answer,
"source_nodes": list(question.source_nodes),
"difficulty": question.difficulty,
"family": question.family,
"skill_target": question.skill_target,
}
entry = _question_to_entry(question)
existing.append(entry)
# 原子写入
@@ -1120,20 +1127,7 @@ async def _run_generate_v2(args: argparse.Namespace) -> None:
except (json.JSONDecodeError, OSError):
existing = []
existing.append(
{
"question_id": q.question_id,
"video_id": q.video_id,
"task_type": q.task_type,
"question": q.question,
"options": list(q.options),
"answer": q.answer,
"source_nodes": list(q.source_nodes),
"difficulty": q.difficulty,
"family": q.family,
"skill_target": q.skill_target,
}
)
existing.append(_question_to_entry(q))
tmp_path = output_path.with_suffix(".tmp")
tmp_path.write_text(