refactor:章节分析服务添加重试机制(最多3次,带指数退避等待)
This commit is contained in:
@@ -6,6 +6,7 @@ from app.services.prompt_service import prompt_service, PromptService
|
|||||||
from app.logger import get_logger
|
from app.logger import get_logger
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import asyncio
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -30,10 +31,11 @@ class PlotAnalyzer:
|
|||||||
content: str,
|
content: str,
|
||||||
word_count: int,
|
word_count: int,
|
||||||
user_id: str = None,
|
user_id: str = None,
|
||||||
db: AsyncSession = None
|
db: AsyncSession = None,
|
||||||
|
max_retries: int = 3
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
分析单章内容
|
分析单章内容(带重试机制)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
chapter_number: 章节号
|
chapter_number: 章节号
|
||||||
@@ -42,62 +44,116 @@ class PlotAnalyzer:
|
|||||||
word_count: 字数
|
word_count: 字数
|
||||||
user_id: 用户ID(用于获取自定义提示词)
|
user_id: 用户ID(用于获取自定义提示词)
|
||||||
db: 数据库会话(用于查询自定义提示词)
|
db: 数据库会话(用于查询自定义提示词)
|
||||||
|
max_retries: 最大重试次数,默认3次
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
分析结果字典,失败返回None
|
分析结果字典,失败返回None
|
||||||
"""
|
"""
|
||||||
|
logger.info(f"🔍 开始分析第{chapter_number}章: {title}")
|
||||||
|
|
||||||
|
# 如果内容过长,截取前8000字(避免超token)
|
||||||
|
analysis_content = content[:8000] if len(content) > 8000 else content
|
||||||
|
|
||||||
|
# 获取自定义提示词模板
|
||||||
try:
|
try:
|
||||||
logger.info(f"🔍 开始分析第{chapter_number}章: {title}")
|
|
||||||
|
|
||||||
# 如果内容过长,截取前8000字(避免超token)
|
|
||||||
analysis_content = content[:8000] if len(content) > 8000 else content
|
|
||||||
|
|
||||||
# 获取自定义提示词模板
|
|
||||||
if user_id and db:
|
if user_id and db:
|
||||||
template = await PromptService.get_template("PLOT_ANALYSIS", user_id, db)
|
template = await PromptService.get_template("PLOT_ANALYSIS", user_id, db)
|
||||||
else:
|
else:
|
||||||
# 降级到系统默认模板
|
# 降级到系统默认模板
|
||||||
template = PromptService.PLOT_ANALYSIS
|
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:
|
except Exception as e:
|
||||||
logger.error(f"❌ 章节分析异常: {str(e)}")
|
logger.warning(f"⚠️ 获取提示词模板失败,使用默认模板: {str(e)}")
|
||||||
return None
|
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]]:
|
def _parse_analysis_response(self, response: str) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user