|
|
@@ -13,6 +13,7 @@ from src.model.qwen_vl import QWenVLParser
|
|
|
from src.conf.settings import model_settings
|
|
|
from src.common.logging_config import get_logger
|
|
|
from src.utils.async_utils import ThreadPoolManager
|
|
|
+from src.utils.json_utils import parse_qa_response
|
|
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
@@ -69,39 +70,36 @@ class QAGenerateNode(BaseNode):
|
|
|
"""
|
|
|
prompt = f"""
|
|
|
# Role
|
|
|
- 你是一位深耕 0-14 岁儿童教育与少儿出版行业 的资深知识萃取专家。你具备儿童心理学、教育学(如皮亚杰认知发展理论、维果茨基最近发展区)以及全球童书出版趋势的深厚洞察力。
|
|
|
-
|
|
|
+ 你是一位拥有“魔法大脑”的儿童教育专家。你深知皮亚杰的认知阶段理论,能把深奥的行业文本转化为 0-14 岁孩子爱听、爱想、爱做的趣味问答。
|
|
|
# Task
|
|
|
- 请阅读文本内容:{chunk},生成 {count} 个高质量问答对。
|
|
|
-
|
|
|
- # Target Audience
|
|
|
- 0-14 岁儿童(提问语气要亲切、好奇、具有代入感)。
|
|
|
+ 阅读文本 {chunk},从中萃取 {count} 个问答对。请严格遵守以下**“提问配方”**:
|
|
|
+ 【40% 内容捕快】:针对文本中的基础事实提问。
|
|
|
+ - 目标:让孩子像玩侦探游戏一样找回文中的关键信息。
|
|
|
+ 【30% 脑洞探险家】:基于文本底层逻辑进行高阶提问。
|
|
|
+ - 目标:深挖“为什么”、分析“优劣势”或“实际怎么做”,启发孩子思考事物的本质。
|
|
|
+ 【30% 知识桥梁家】:跳出文本,向科普、艺术、教育或社会素养进行跨界延伸。
|
|
|
+ - 目标:将书本知识与现实世界(STEAM、审美、SEL)连接。
|
|
|
|
|
|
- # Requirements
|
|
|
- 提问视角(For Kids):
|
|
|
- - 禁止说教。使用“如果你是...”、“你猜...”、“为什么...会这样”等激发好奇心的问句。
|
|
|
- - 问题要能关联孩子的生活经验或想象力。
|
|
|
-
|
|
|
- 回答要求(Double-Layer & Slim):
|
|
|
- - 基础事实 + 深度启发:先用一句话讲清事实,再用一句话点破底层逻辑或引导实践。
|
|
|
- - 字数铁律:每个答案严禁超过 50 字。
|
|
|
+ # Rules for Interaction
|
|
|
+ 提问视角(For Kids Only):
|
|
|
+ - 使用“猜猜看”、“如果你是...”、“你有没有发现...”等语气。
|
|
|
+ - 禁令:严禁出现“根据文中描述”、“请分析...”等成人化、考试化的术语。
|
|
|
|
|
|
- 扩展维度:
|
|
|
- - 好奇心钩子:为什么这个知识很酷?
|
|
|
- - 生活实验室:你现在可以试着做什么?
|
|
|
- - 情绪/逻辑种子:这背后的道理是什么?
|
|
|
+ # Answer炼金术(Double-Layer & Slim):
|
|
|
+ - 结构:1句事实要点 + 1句思维火花(点破逻辑、情绪种子或生活实验建议)。
|
|
|
+ - 字数:每个回答必须在 50 字以内,语言生动、干脆。
|
|
|
|
|
|
# Output Standards
|
|
|
- 格式:必须以完整的 JSON 数组格式输出。
|
|
|
- - 严禁:任何多余的开场白或解释文字。
|
|
|
+ - 格式:仅输出完整的 JSON 数组。
|
|
|
+ - 严禁:任何多余的开场白、分类标题或总结。
|
|
|
|
|
|
# Output Format (JSON Only)
|
|
|
JSON格式如下:
|
|
|
[
|
|
|
- {{
|
|
|
- "question": "(面向孩子的好奇心提问)",
|
|
|
- "answer": "(事实要点+深度启发,50字以内)"
|
|
|
- }}
|
|
|
+ {{
|
|
|
+ "question": "(面向孩子的好奇心提问)",
|
|
|
+ "answer": "(事实+启发,50字以内)"
|
|
|
+ }}
|
|
|
]
|
|
|
"""
|
|
|
|
|
|
@@ -111,196 +109,14 @@ class QAGenerateNode(BaseNode):
|
|
|
parser = QWenVLParser(self.model_name)
|
|
|
result = parser.chat(prompt)
|
|
|
|
|
|
- # 解析JSON响应
|
|
|
- qa_pairs = self._parse_qa_response(result)
|
|
|
+ # 解析JSON响应(使用通用工具函数)
|
|
|
+ qa_pairs = parse_qa_response(result)
|
|
|
logger.debug(f"第 {chunk_index + 1} 块生成 {len(qa_pairs)} 个QA对")
|
|
|
return qa_pairs
|
|
|
except Exception as e:
|
|
|
logger.error(f"第 {chunk_index + 1} 块QA生成失败: {str(e)}")
|
|
|
return []
|
|
|
|
|
|
- def _parse_qa_response(self, response: str) -> List[Dict[str, str]]:
|
|
|
- """
|
|
|
- 解析QA响应
|
|
|
-
|
|
|
- Args:
|
|
|
- response: 模型响应文本
|
|
|
-
|
|
|
- Returns:
|
|
|
- QA对列表
|
|
|
- """
|
|
|
- import re
|
|
|
-
|
|
|
- if not response:
|
|
|
- return []
|
|
|
-
|
|
|
- # 清理响应文本
|
|
|
- cleaned_response = response.strip()
|
|
|
-
|
|
|
- # 移除 BOM 标记
|
|
|
- if cleaned_response.startswith('\ufeff'):
|
|
|
- cleaned_response = cleaned_response[1:]
|
|
|
-
|
|
|
- # 1. 尝试直接解析JSON
|
|
|
- try:
|
|
|
- return json.loads(cleaned_response)
|
|
|
- except json.JSONDecodeError as e:
|
|
|
- logger.debug(f"直接解析失败: {str(e)}")
|
|
|
- pass
|
|
|
-
|
|
|
- # 1.5. 尝试使用 raw_decode 解析(可以跳过前面的非JSON文本)
|
|
|
- try:
|
|
|
- decoder = json.JSONDecoder()
|
|
|
- result, idx = decoder.raw_decode(cleaned_response)
|
|
|
- if isinstance(result, list):
|
|
|
- return result
|
|
|
- except (json.JSONDecodeError, ValueError) as e:
|
|
|
- logger.debug(f"raw_decode 解析失败: {str(e)}")
|
|
|
- pass
|
|
|
-
|
|
|
- # 2. 尝试去除 markdown 代码块标记
|
|
|
- # 匹配 ```json ... ``` 或 ``` ... ```
|
|
|
- code_block_pattern = r'```(?:json)?\s*\n?(.*?)\n?```'
|
|
|
- code_block_match = re.search(code_block_pattern, cleaned_response, re.DOTALL)
|
|
|
- if code_block_match:
|
|
|
- try:
|
|
|
- json_content = code_block_match.group(1).strip()
|
|
|
- return json.loads(json_content)
|
|
|
- except json.JSONDecodeError:
|
|
|
- pass
|
|
|
-
|
|
|
- # 3. 尝试提取第一个完整的 JSON 数组
|
|
|
- # 使用括号匹配算法,正确处理嵌套的 [] 和 {}
|
|
|
- bracket_count = 0
|
|
|
- brace_count = 0
|
|
|
- start_idx = -1
|
|
|
- in_string = False
|
|
|
- escape_next = False
|
|
|
-
|
|
|
- for i, char in enumerate(cleaned_response):
|
|
|
- if escape_next:
|
|
|
- escape_next = False
|
|
|
- continue
|
|
|
-
|
|
|
- if char == '\\':
|
|
|
- escape_next = True
|
|
|
- continue
|
|
|
-
|
|
|
- if char == '"' and not escape_next:
|
|
|
- in_string = not in_string
|
|
|
- continue
|
|
|
-
|
|
|
- if in_string:
|
|
|
- continue
|
|
|
-
|
|
|
- if char == '[':
|
|
|
- if start_idx == -1:
|
|
|
- start_idx = i
|
|
|
- bracket_count += 1
|
|
|
- elif char == ']':
|
|
|
- bracket_count -= 1
|
|
|
- if bracket_count == 0 and brace_count == 0 and start_idx != -1:
|
|
|
- try:
|
|
|
- json_content = cleaned_response[start_idx:i+1]
|
|
|
- return json.loads(json_content)
|
|
|
- except json.JSONDecodeError:
|
|
|
- # 继续尝试下一个匹配
|
|
|
- start_idx = -1
|
|
|
- bracket_count = 0
|
|
|
- brace_count = 0
|
|
|
- elif char == '{':
|
|
|
- if start_idx != -1:
|
|
|
- brace_count += 1
|
|
|
- elif char == '}':
|
|
|
- if start_idx != -1:
|
|
|
- brace_count -= 1
|
|
|
-
|
|
|
- # 4. 尝试使用正则提取 JSON 数组(更宽松的方式)
|
|
|
- json_array_pattern = r'\[\s*(?:\{[^}]*\}(?:\s*,\s*\{[^}]*\})*)?\s*\]'
|
|
|
- json_match = re.search(json_array_pattern, cleaned_response, re.DOTALL)
|
|
|
- if json_match:
|
|
|
- try:
|
|
|
- return json.loads(json_match.group())
|
|
|
- except json.JSONDecodeError:
|
|
|
- pass
|
|
|
-
|
|
|
- # 5. 尝试逐行查找 JSON 数组
|
|
|
- lines = cleaned_response.split('\n')
|
|
|
- json_lines = []
|
|
|
- in_json = False
|
|
|
- bracket_count = 0
|
|
|
-
|
|
|
- for line in lines:
|
|
|
- stripped_line = line.strip()
|
|
|
- if not stripped_line:
|
|
|
- continue
|
|
|
-
|
|
|
- # 检查是否包含 JSON 数组的开始
|
|
|
- if '[' in stripped_line and not in_json:
|
|
|
- in_json = True
|
|
|
- json_lines = [stripped_line]
|
|
|
- bracket_count = stripped_line.count('[') - stripped_line.count(']')
|
|
|
- elif in_json:
|
|
|
- json_lines.append(stripped_line)
|
|
|
- bracket_count += stripped_line.count('[') - stripped_line.count(']')
|
|
|
-
|
|
|
- if bracket_count == 0:
|
|
|
- try:
|
|
|
- json_content = '\n'.join(json_lines)
|
|
|
- return json.loads(json_content)
|
|
|
- except json.JSONDecodeError:
|
|
|
- in_json = False
|
|
|
- json_lines = []
|
|
|
- bracket_count = 0
|
|
|
-
|
|
|
- # 如果收集到了 JSON 行但还没闭合,尝试解析
|
|
|
- if json_lines:
|
|
|
- try:
|
|
|
- json_content = '\n'.join(json_lines)
|
|
|
- return json.loads(json_content)
|
|
|
- except json.JSONDecodeError:
|
|
|
- pass
|
|
|
-
|
|
|
- # 6. 最后尝试:查找所有可能的 JSON 对象并组合成数组
|
|
|
- try:
|
|
|
- # 查找所有 { ... } 模式的对象
|
|
|
- json_objects = re.findall(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', cleaned_response, re.DOTALL)
|
|
|
- if json_objects:
|
|
|
- parsed_objects = []
|
|
|
- for obj_str in json_objects:
|
|
|
- try:
|
|
|
- parsed_obj = json.loads(obj_str)
|
|
|
- if isinstance(parsed_obj, dict) and 'question' in parsed_obj and 'answer' in parsed_obj:
|
|
|
- parsed_objects.append(parsed_obj)
|
|
|
- except json.JSONDecodeError:
|
|
|
- continue
|
|
|
- if parsed_objects:
|
|
|
- logger.info(f"通过对象提取方式解析到 {len(parsed_objects)} 个QA对")
|
|
|
- return parsed_objects
|
|
|
- except Exception as e:
|
|
|
- logger.debug(f"对象提取方式失败: {str(e)}")
|
|
|
-
|
|
|
- # 所有方法都失败
|
|
|
- # 记录更详细的错误信息用于调试
|
|
|
- error_info = {
|
|
|
- "response_length": len(cleaned_response),
|
|
|
- "first_100_chars": repr(cleaned_response[:100]),
|
|
|
- "last_100_chars": repr(cleaned_response[-100:]) if len(cleaned_response) > 100 else "",
|
|
|
- "has_bracket": '[' in cleaned_response,
|
|
|
- "has_brace": '{' in cleaned_response,
|
|
|
- }
|
|
|
- logger.warning(f"无法解析QA响应为JSON: {error_info}")
|
|
|
-
|
|
|
- # 尝试最后一次:如果响应看起来像 JSON 数组,尝试修复常见问题
|
|
|
- if cleaned_response.startswith('[') and cleaned_response.endswith(']'):
|
|
|
- try:
|
|
|
- # 尝试修复常见的 JSON 问题:替换中文引号
|
|
|
- fixed_response = cleaned_response.replace('"', '"').replace('"', '"').replace(''', "'").replace(''', "'")
|
|
|
- return json.loads(fixed_response)
|
|
|
- except json.JSONDecodeError:
|
|
|
- pass
|
|
|
-
|
|
|
- return []
|
|
|
|
|
|
def execute(self, state: BaseState) -> Dict[str, Any]:
|
|
|
"""
|