From 6d6eb8e3a39eaef532151e4ff0aa22fa9350625b Mon Sep 17 00:00:00 2001 From: iomgaa Date: Sat, 11 Jul 2026 23:43:35 -0400 Subject: [PATCH] refactor(question_gen): extract _validate_parsed_fields to reduce CC Split field validation logic out of _parse_v2_response into a dedicated _validate_parsed_fields helper. This brings _parse_v2_response from CC=11 (grade C) down to CC=3 (grade A). The extracted validator is CC=9 (grade B). No grade-C functions remain. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/question_gen/generator_v2.py | 109 +++++++++++++++++++------------ 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/app/question_gen/generator_v2.py b/app/question_gen/generator_v2.py index 3ed60b6..ad113ef 100644 --- a/app/question_gen/generator_v2.py +++ b/app/question_gen/generator_v2.py @@ -220,6 +220,65 @@ def _extract_json_from_text(raw: str) -> str: return content +@dataclass(frozen=True) +class _ValidatedFields: + """字段校验通过后的中间结构。""" + + question: str + options: tuple[str, ...] + answer: str + difficulty: str + + +def _validate_parsed_fields(data: dict) -> _ValidatedFields: + """校验 VLM 响应 JSON 的必填字段并规范化。 + + 参数: + data: 已解析的 JSON 字典。 + + 返回: + _ValidatedFields 实例(字段已规范化)。 + + 异常: + ValueError: 缺少必填字段或字段值非法。 + """ + missing = [f for f in ("question", "options", "answer", "difficulty") if f not in data] + if missing: + msg = f"VLM 响应缺少必填字段: {', '.join(missing)}" + raise ValueError(msg) + + question_text = str(data["question"]) + options_raw = data["options"] + answer = str(data["answer"]).strip().upper() + difficulty = str(data["difficulty"]).strip().lower() + + # 校验 options + if not isinstance(options_raw, list) or len(options_raw) < 2: + msg = f"options 字段必须是至少 2 个选项的列表,实际: {options_raw}" + raise ValueError(msg) + + # 校验 answer + if answer not in _VALID_ANSWERS: + msg = f"answer 字段值 '{answer}' 非法,必须为 A/B/C/D 之一" + raise ValueError(msg) + + # 校验 difficulty(宽容处理:非法值回退为 medium) + if difficulty not in _VALID_DIFFICULTIES: + logger.warning( + "difficulty '{}' 不在预设范围 {},回退为 'medium'", + difficulty, + _VALID_DIFFICULTIES, + ) + difficulty = "medium" + + return _ValidatedFields( + question=question_text, + options=tuple(str(o) for o in options_raw), + answer=answer, + difficulty=difficulty, + ) + + def _parse_v2_response( raw: str, video_id: str, @@ -233,7 +292,7 @@ def _parse_v2_response( 流程: 1. 提取 JSON(处理 markdown 包裹)。 2. json_repair 修复常见格式错误。 - 3. 解析并校验必填字段。 + 3. 校验必填字段(委托 _validate_parsed_fields)。 4. 构造 CandidateQuestion 实例。 参数: @@ -250,13 +309,11 @@ def _parse_v2_response( 异常: ValueError: JSON 解析失败或缺少必填字段或字段值非法。 """ - # Phase 1: 提取 JSON 文本 + # Phase 1: 提取 + 修复 JSON json_text = _extract_json_from_text(raw) - - # Phase 2: json_repair 修复 repaired = repair_json(json_text, return_objects=False) - # Phase 3: 解析 + # Phase 2: 解析为字典 try: data = json.loads(repaired) except json.JSONDecodeError as e: @@ -267,38 +324,10 @@ def _parse_v2_response( msg = f"VLM 响应顶层不是 JSON 对象: type={type(data).__name__}" raise ValueError(msg) - # Phase 4: 校验必填字段 - missing = [f for f in ("question", "options", "answer", "difficulty") if f not in data] - if missing: - msg = f"VLM 响应缺少必填字段: {', '.join(missing)}" - raise ValueError(msg) + # Phase 3: 字段校验 + fields = _validate_parsed_fields(data) - question_text = str(data["question"]) - options_raw = data["options"] - answer = str(data["answer"]).strip().upper() - difficulty = str(data["difficulty"]).strip().lower() - - # 校验 options - if not isinstance(options_raw, list) or len(options_raw) < 2: - msg = f"options 字段必须是至少 2 个选项的列表,实际: {options_raw}" - raise ValueError(msg) - options = tuple(str(o) for o in options_raw) - - # 校验 answer - if answer not in _VALID_ANSWERS: - msg = f"answer 字段值 '{answer}' 非法,必须为 A/B/C/D 之一" - raise ValueError(msg) - - # 校验 difficulty - if difficulty not in _VALID_DIFFICULTIES: - logger.warning( - "difficulty '{}' 不在预设范围 {},回退为 'medium'", - difficulty, - _VALID_DIFFICULTIES, - ) - difficulty = "medium" - - # Phase 5: 构造 CandidateQuestion + # Phase 4: 构造 CandidateQuestion question_id = f"{video_id}_{task_type}_{seq:04d}" return CandidateQuestion( @@ -306,11 +335,11 @@ def _parse_v2_response( video_id=video_id, task_type=task_type, skill_target=skill_target, - question=question_text, - options=options, - answer=answer, + question=fields.question, + options=fields.options, + answer=fields.answer, source_nodes=source_nodes, - difficulty=difficulty, + difficulty=fields.difficulty, )