From 30c044394f02932489c77498e209a4bc6a291039 Mon Sep 17 00:00:00 2001 From: xiamuceer Date: Wed, 31 Dec 2025 11:59:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=E7=AB=A0=E8=8A=82=E5=88=86=E6=9E=90?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E6=B7=BB=E5=8A=A0=E9=87=8D=E8=AF=95=E6=9C=BA?= =?UTF-8?q?=E5=88=B6=EF=BC=88=E6=9C=80=E5=A4=9A3=E6=AC=A1=EF=BC=8C?= =?UTF-8?q?=E5=B8=A6=E6=8C=87=E6=95=B0=E9=80=80=E9=81=BF=E7=AD=89=E5=BE=85?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/plot_analyzer.py | 150 ++++++++++++++++++-------- 1 file changed, 103 insertions(+), 47 deletions(-) diff --git a/backend/app/services/plot_analyzer.py b/backend/app/services/plot_analyzer.py index 89441b8..0b44a13 100644 --- a/backend/app/services/plot_analyzer.py +++ b/backend/app/services/plot_analyzer.py @@ -6,6 +6,7 @@ from app.services.prompt_service import prompt_service, PromptService from app.logger import get_logger import json import re +import asyncio logger = get_logger(__name__) @@ -30,10 +31,11 @@ class PlotAnalyzer: content: str, word_count: int, user_id: str = None, - db: AsyncSession = None + db: AsyncSession = None, + max_retries: int = 3 ) -> Optional[Dict[str, Any]]: """ - 分析单章内容 + 分析单章内容(带重试机制) Args: chapter_number: 章节号 @@ -42,62 +44,116 @@ class PlotAnalyzer: word_count: 字数 user_id: 用户ID(用于获取自定义提示词) db: 数据库会话(用于查询自定义提示词) + max_retries: 最大重试次数,默认3次 Returns: 分析结果字典,失败返回None """ + logger.info(f"🔍 开始分析第{chapter_number}章: {title}") + + # 如果内容过长,截取前8000字(避免超token) + analysis_content = content[:8000] if len(content) > 8000 else content + + # 获取自定义提示词模板 try: - logger.info(f"🔍 开始分析第{chapter_number}章: {title}") - - # 如果内容过长,截取前8000字(避免超token) - analysis_content = content[:8000] if len(content) > 8000 else content - - # 获取自定义提示词模板 if user_id and db: template = await PromptService.get_template("PLOT_ANALYSIS", user_id, db) else: # 降级到系统默认模板 template = PromptService.PLOT_ANALYSIS - - # 格式化提示词 - prompt = PromptService.format_prompt( - template, - chapter_number=chapter_number, - title=title, - word_count=word_count, - content=analysis_content - ) - - # 调用AI进行分析 - # 注意:不指定max_tokens,使用用户在设置中配置的值 - logger.info(f" 调用AI分析(内容长度: {len(analysis_content)}字)...") - accumulated_text = "" - async for chunk in self.ai_service.generate_text_stream( - prompt=prompt, - temperature=0.3 # 降低温度以获得更稳定的JSON输出 - ): - accumulated_text += chunk - - # 提取内容 - response_text = accumulated_text - - # 解析JSON结果 - analysis_result = self._parse_analysis_response(response_text) - - if analysis_result: - logger.info(f"✅ 第{chapter_number}章分析完成") - logger.info(f" - 钩子: {len(analysis_result.get('hooks', []))}个") - logger.info(f" - 伏笔: {len(analysis_result.get('foreshadows', []))}个") - logger.info(f" - 情节点: {len(analysis_result.get('plot_points', []))}个") - logger.info(f" - 整体评分: {analysis_result.get('scores', {}).get('overall', 'N/A')}") - return analysis_result - else: - logger.error(f"❌ 第{chapter_number}章分析失败: JSON解析错误") - return None - except Exception as e: - logger.error(f"❌ 章节分析异常: {str(e)}") - return None + logger.warning(f"⚠️ 获取提示词模板失败,使用默认模板: {str(e)}") + template = PromptService.PLOT_ANALYSIS + + # 格式化提示词 + prompt = PromptService.format_prompt( + template, + chapter_number=chapter_number, + title=title, + word_count=word_count, + content=analysis_content + ) + + last_error = None + + for attempt in range(1, max_retries + 1): + try: + # 调用AI进行分析 + logger.info(f" 📡 调用AI分析(内容长度: {len(analysis_content)}字, 尝试 {attempt}/{max_retries})...") + accumulated_text = "" + + try: + async for chunk in self.ai_service.generate_text_stream( + prompt=prompt, + temperature=0.3 # 降低温度以获得更稳定的JSON输出 + ): + accumulated_text += chunk + except GeneratorExit: + # 流式响应被中断 + logger.warning(f"⚠️ 流式响应被中断(GeneratorExit),已累积 {len(accumulated_text)} 字符") + # 如果已经累积了足够内容,继续尝试解析 + if len(accumulated_text) < 100: + raise Exception("流式响应中断,内容不足") + except Exception as stream_error: + logger.error(f"❌ 流式生成出错: {str(stream_error)}") + raise + + # 检查响应是否为空 + if not accumulated_text or len(accumulated_text.strip()) < 10: + logger.warning(f"⚠️ AI响应为空或过短(长度: {len(accumulated_text)}), 尝试 {attempt}/{max_retries}") + last_error = "AI响应为空或过短" + if attempt < max_retries: + wait_time = min(2 ** attempt, 10) + logger.info(f" ⏳ 等待 {wait_time} 秒后重试...") + await asyncio.sleep(wait_time) + continue + else: + logger.error(f"❌ 第{chapter_number}章分析失败: AI响应为空,已达最大重试次数") + return None + + # 提取内容 + response_text = accumulated_text + logger.debug(f" 收到AI响应,长度: {len(response_text)} 字符") + + # 解析JSON结果 + analysis_result = self._parse_analysis_response(response_text) + + if analysis_result: + logger.info(f"✅ 第{chapter_number}章分析完成 (尝试 {attempt}/{max_retries})") + logger.info(f" - 钩子: {len(analysis_result.get('hooks', []))}个") + logger.info(f" - 伏笔: {len(analysis_result.get('foreshadows', []))}个") + logger.info(f" - 情节点: {len(analysis_result.get('plot_points', []))}个") + logger.info(f" - 整体评分: {analysis_result.get('scores', {}).get('overall', 'N/A')}") + return analysis_result + else: + # JSON解析失败,重试 + logger.warning(f"⚠️ JSON解析失败, 尝试 {attempt}/{max_retries}") + last_error = "JSON解析失败" + if attempt < max_retries: + wait_time = min(2 ** attempt, 10) + logger.info(f" ⏳ 等待 {wait_time} 秒后重试...") + await asyncio.sleep(wait_time) + continue + else: + logger.error(f"❌ 第{chapter_number}章分析失败: JSON解析错误,已达最大重试次数") + return None + + except Exception as e: + last_error = str(e) + logger.error(f"❌ 章节分析异常(尝试 {attempt}/{max_retries}): {last_error}") + + if attempt < max_retries: + wait_time = min(2 ** attempt, 10) + logger.info(f" ⏳ 等待 {wait_time} 秒后重试...") + await asyncio.sleep(wait_time) + continue + else: + logger.error(f"❌ 第{chapter_number}章分析失败: {last_error},已达最大重试次数") + return None + + # 不应该到达这里,但作为安全措施 + logger.error(f"❌ 第{chapter_number}章分析失败: {last_error}") + return None def _parse_analysis_response(self, response: str) -> Optional[Dict[str, Any]]: """