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) <noreply@anthropic.com>
This commit is contained in:
@@ -220,6 +220,65 @@ def _extract_json_from_text(raw: str) -> str:
|
|||||||
return content
|
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(
|
def _parse_v2_response(
|
||||||
raw: str,
|
raw: str,
|
||||||
video_id: str,
|
video_id: str,
|
||||||
@@ -233,7 +292,7 @@ def _parse_v2_response(
|
|||||||
流程:
|
流程:
|
||||||
1. 提取 JSON(处理 markdown 包裹)。
|
1. 提取 JSON(处理 markdown 包裹)。
|
||||||
2. json_repair 修复常见格式错误。
|
2. json_repair 修复常见格式错误。
|
||||||
3. 解析并校验必填字段。
|
3. 校验必填字段(委托 _validate_parsed_fields)。
|
||||||
4. 构造 CandidateQuestion 实例。
|
4. 构造 CandidateQuestion 实例。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
@@ -250,13 +309,11 @@ def _parse_v2_response(
|
|||||||
异常:
|
异常:
|
||||||
ValueError: JSON 解析失败或缺少必填字段或字段值非法。
|
ValueError: JSON 解析失败或缺少必填字段或字段值非法。
|
||||||
"""
|
"""
|
||||||
# Phase 1: 提取 JSON 文本
|
# Phase 1: 提取 + 修复 JSON
|
||||||
json_text = _extract_json_from_text(raw)
|
json_text = _extract_json_from_text(raw)
|
||||||
|
|
||||||
# Phase 2: json_repair 修复
|
|
||||||
repaired = repair_json(json_text, return_objects=False)
|
repaired = repair_json(json_text, return_objects=False)
|
||||||
|
|
||||||
# Phase 3: 解析
|
# Phase 2: 解析为字典
|
||||||
try:
|
try:
|
||||||
data = json.loads(repaired)
|
data = json.loads(repaired)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
@@ -267,38 +324,10 @@ def _parse_v2_response(
|
|||||||
msg = f"VLM 响应顶层不是 JSON 对象: type={type(data).__name__}"
|
msg = f"VLM 响应顶层不是 JSON 对象: type={type(data).__name__}"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Phase 4: 校验必填字段
|
# Phase 3: 字段校验
|
||||||
missing = [f for f in ("question", "options", "answer", "difficulty") if f not in data]
|
fields = _validate_parsed_fields(data)
|
||||||
if missing:
|
|
||||||
msg = f"VLM 响应缺少必填字段: {', '.join(missing)}"
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
question_text = str(data["question"])
|
# Phase 4: 构造 CandidateQuestion
|
||||||
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
|
|
||||||
question_id = f"{video_id}_{task_type}_{seq:04d}"
|
question_id = f"{video_id}_{task_type}_{seq:04d}"
|
||||||
|
|
||||||
return CandidateQuestion(
|
return CandidateQuestion(
|
||||||
@@ -306,11 +335,11 @@ def _parse_v2_response(
|
|||||||
video_id=video_id,
|
video_id=video_id,
|
||||||
task_type=task_type,
|
task_type=task_type,
|
||||||
skill_target=skill_target,
|
skill_target=skill_target,
|
||||||
question=question_text,
|
question=fields.question,
|
||||||
options=options,
|
options=fields.options,
|
||||||
answer=answer,
|
answer=fields.answer,
|
||||||
source_nodes=source_nodes,
|
source_nodes=source_nodes,
|
||||||
difficulty=difficulty,
|
difficulty=fields.difficulty,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user