refactor(question_gen): adapt generator/gates/store signatures for strategy

- generator_v2: _load_prompt_template takes template_name str instead of
  QuestionFamilySpec; _build_v2_prompt takes prompt_template + strategy_name
  + sub_pattern_instruction; generate_one_v2 takes discrete params
  (prompt_template, strategy_name, skill_target, sub_pattern_instruction)
- gates: _gate_leak_test and run_gates take leak_probe_template str
  instead of QuestionFamilySpec
- run_store: add sub_pattern column to DDL + idempotent migration;
  record_item accepts optional sub_pattern param
- Remove QuestionFamilySpec imports from generator_v2 and gates modules
- Update test call sites accordingly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 05:50:47 -04:00
parent c49d0ff12f
commit afa77173e3
4 changed files with 155 additions and 76 deletions
+39 -22
View File
@@ -1,6 +1,6 @@
"""v2 生成器 — 基于家族特化 prompt 模板的单题 VLM 出题模块。
"""v2 生成器 — 基于策略特化 prompt 模板的单题 VLM 出题模块。
使用 VLMProvider 接口调用视觉语言模型,结合 per-family prompt 模板
使用 VLMProvider 接口调用视觉语言模型,结合 per-strategy prompt 模板
和 MaterialContext 素材上下文,生成一道四选一候选题。
典型调用路径::
@@ -9,10 +9,12 @@
vlm=vlm_client,
tree=tree_index,
material=material_ctx,
family_spec=RETRIEVAL_FAMILY,
task_type="Action Reasoning",
seq=1,
video_id="vid_001",
prompt_template="retrieval.md",
strategy_name="RETRIEVAL",
skill_target="M1",
session_id="sess_001",
)
"""
@@ -28,7 +30,6 @@ from json_repair import repair_json
from loguru import logger
if TYPE_CHECKING:
from app.question_gen.families import QuestionFamilySpec
from app.question_gen.sampler_v2 import MaterialContext
from app.tree.index import TreeIndex
from core.protocols import VLMProvider
@@ -85,11 +86,11 @@ class CandidateQuestion:
# ---------------------------------------------------------------------------
def _load_prompt_template(family_spec: QuestionFamilySpec) -> str:
"""加载家族对应的 prompt 模板文件。
def _load_prompt_template(template_name: str) -> str:
"""加载 prompt 模板文件。
参数:
family_spec: 问题家族规格(含 prompt_template 文件名
template_name: store/prompts/question_gen/ 下的模板文件名。
返回:
模板内容字符串。
@@ -97,9 +98,9 @@ def _load_prompt_template(family_spec: QuestionFamilySpec) -> str:
异常:
FileNotFoundError: 模板文件不存在。
"""
path = _PROMPTS_DIR / family_spec.prompt_template
path = _PROMPTS_DIR / template_name
if not path.exists():
msg = f"家族 prompt 模板文件不存在: {path}"
msg = f"Prompt 模板文件不存在: {path}"
raise FileNotFoundError(msg)
return path.read_text(encoding="utf-8")
@@ -110,36 +111,40 @@ def _load_prompt_template(family_spec: QuestionFamilySpec) -> str:
def _build_v2_prompt(
family_spec: QuestionFamilySpec,
prompt_template: str,
strategy_name: str,
material: MaterialContext,
task_type: str,
seq: int,
*,
reject_reason: str | None = None,
sub_pattern_instruction: str | None = None,
) -> tuple[list[dict[str, str]], list[str]]:
"""构建 VLM 出题调用的 messages 和帧路径列表。
参数:
family_spec: 问题家族规格
prompt_template: prompt 模板文件名(store/prompts/question_gen/ 下)
strategy_name: 策略名称(如 "RETRIEVAL")。
material: 采样素材上下文。
task_type: 任务类型字符串。
seq: 当前序号。
reject_reason: 上一次被门控拒绝的原因(用于引导 VLM 避免相同错误)。
sub_pattern_instruction: 子模式特殊指令(如有)。
返回:
二元组:
- messages: 适配 VLMProvider 的 message 列表(system + user)。
- frame_paths: 需发送给 VLM 的帧路径列表。
"""
# Phase 1: 加载家族模板作为 system prompt
template_content = _load_prompt_template(family_spec)
# Phase 1: 加载策略模板作为 system prompt
template_content = _load_prompt_template(prompt_template)
system_message = template_content
# Phase 2: 构建 user prompt — 聚合素材信息
user_parts: list[str] = []
user_parts.append(f"## Task Type: {task_type}")
user_parts.append(f"## Question Family: {family_spec.name}")
user_parts.append(f"## Question Family: {strategy_name}")
user_parts.append(f"## Sequence: #{seq}")
# 字幕素材
@@ -167,6 +172,10 @@ def _build_v2_prompt(
f"Please generate a NEW question that avoids this issue."
)
# 子模式特殊指令注入
if sub_pattern_instruction is not None:
user_parts.append(f"\n## Special Focus:\n{sub_pattern_instruction}")
# 输出格式指令
user_parts.append(
"\n## Output Format:\n"
@@ -352,18 +361,21 @@ async def generate_one_v2(
vlm: VLMProvider,
tree: TreeIndex,
material: MaterialContext,
family_spec: QuestionFamilySpec,
task_type: str,
seq: int,
*,
video_id: str,
prompt_template: str,
strategy_name: str,
skill_target: str,
reject_reason: str | None = None,
sub_pattern_instruction: str | None = None,
session_id: str,
) -> CandidateQuestion:
"""调用 VLM 生成一道候选题目。
流程:
1. 构建 per-family prompt + 帧路径。
1. 构建 per-strategy prompt + 帧路径。
2. 调用 VLMProvider.chat_with_images。
3. 解析响应为 CandidateQuestion。
4. 附加素材验证信息(subtitle_sentences、frame_paths)。
@@ -372,11 +384,14 @@ async def generate_one_v2(
vlm: VLM 调用端口。
tree: 视频树索引(当前未直接使用,预留后续扩展)。
material: 采样素材上下文。
family_spec: 问题家族规格。
task_type: 任务类型字符串。
seq: 当前序号。
video_id: 视频标识。
prompt_template: prompt 模板文件名。
strategy_name: 策略名称(如 "RETRIEVAL")。
skill_target: 目标失败机制编号(M1-M5)。
reject_reason: 上一次被门控拒绝的原因。
sub_pattern_instruction: 子模式特殊指令(如有)。
session_id: 会话 ID(遥测关联)。
返回:
@@ -384,21 +399,23 @@ async def generate_one_v2(
异常:
ValueError: VLM 响应解析失败。
FileNotFoundError: 家族 prompt 模板不存在。
FileNotFoundError: prompt 模板不存在。
"""
# Phase 1: 构建 prompt
messages, frame_paths = _build_v2_prompt(
family_spec=family_spec,
prompt_template=prompt_template,
strategy_name=strategy_name,
material=material,
task_type=task_type,
seq=seq,
reject_reason=reject_reason,
sub_pattern_instruction=sub_pattern_instruction,
)
# Phase 2: 调用 VLM
logger.debug(
"generate_one_v2: family={}, task_type={}, seq={}, frames={}",
family_spec.name,
"generate_one_v2: strategy={}, task_type={}, seq={}, frames={}",
strategy_name,
task_type,
seq,
len(frame_paths),
@@ -415,7 +432,7 @@ async def generate_one_v2(
raw=response.content,
video_id=video_id,
task_type=task_type,
skill_target=family_spec.skill_target,
skill_target=skill_target,
seq=seq,
source_nodes=material.source_nodes,
)