fix(question_gen): resolve pipeline integration issues from final review

1. Apply postprocess shuffle result (pp.options, pp.answer) to final
   GeneratedQuestion output instead of using original candidate values.

2. Record dedup rejection in store via new mark_item_rejected() method,
   preventing items from staying as 'accepted' after dedup rejects them.

3. Add .flatten() to embed_fn outputs in _is_duplicate and embed_pool
   append to handle 2D (1,D) arrays from embedding implementations.

4. Validate exactly 4 options in _validate_parsed_fields (was >= 2),
   matching the A-D answer constraint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 00:14:12 -04:00
parent eecb86e27a
commit 4fb7a61f8b
4 changed files with 86 additions and 14 deletions
+16 -8
View File
@@ -245,7 +245,7 @@ def _is_duplicate(
if not embed_pool:
return False
query_vec = embed_fn(question_text)
query_vec = embed_fn(question_text).flatten()
query_norm = np.linalg.norm(query_vec)
if query_norm == 0:
return False
@@ -266,11 +266,18 @@ def _is_duplicate(
# ---------------------------------------------------------------------------
def _to_generated_question(candidate: CandidateQuestion) -> GeneratedQuestion:
def _to_generated_question(
candidate: CandidateQuestion,
*,
options: tuple[str, ...] | None = None,
answer: str | None = None,
) -> GeneratedQuestion:
"""将 CandidateQuestion 转换为 GeneratedQuestion。
参数:
candidate: 门控通过的候选题目。
options: 洗牌后的选项元组(若为 None 则使用 candidate 原始选项)。
answer: 重映射后的答案字母(若为 None 则使用 candidate 原始答案)。
返回:
GeneratedQuestion 实例(difficulty_steps 初始为 None)。
@@ -280,8 +287,8 @@ def _to_generated_question(candidate: CandidateQuestion) -> GeneratedQuestion:
video_id=candidate.video_id,
task_type=candidate.task_type,
question=candidate.question,
options=candidate.options,
answer=candidate.answer,
options=options if options is not None else candidate.options,
answer=answer if answer is not None else candidate.answer,
source_nodes=candidate.source_nodes,
difficulty=candidate.difficulty,
skill_target=candidate.skill_target,
@@ -464,6 +471,7 @@ async def _process_one_slot(
# Phase 7: 去重检测
if _is_duplicate(candidate.question, embed_pool, embed_fn, config.dedup_threshold):
prev_reason = "duplicate detected by embedding similarity"
store.mark_item_rejected(item_id, prev_reason)
logger.info(
"slot {} 重复题被拒绝 (attempt {}/{})",
slot.slot_id,
@@ -472,10 +480,10 @@ async def _process_one_slot(
)
continue
# Phase 8: 通过全部检查 → 接受
result = _to_generated_question(candidate)
# 将题目 embedding 加入池
embed_pool.append(embed_fn(candidate.question))
# Phase 8: 通过全部检查 → 接受(使用洗牌后的选项和答案)
result = _to_generated_question(candidate, options=pp.options, answer=pp.answer)
# 将题目 embedding 加入池flatten 确保 1D
embed_pool.append(embed_fn(candidate.question).flatten())
# 标记使用的节点
used_node_ids.update(candidate.source_nodes)