diff --git a/_conf_schema.json b/_conf_schema.json index 7e9fe9d..3584d9d 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -81,6 +81,24 @@ "default": 35, "hint": "⚠️ 警告:增加此值会导致大量Token消耗!默认值35轮(有一定消耗)。设置过高可能导致LLM费用激增" }, + "llm_timeout": { + "type": "int", + "description": "LLM 请求超时时间(秒)", + "default": 30, + "hint": "LLM 单次请求的超时时间,单位秒。可根据模型响应速度适当调整,增大可减少超时失败,但会占用更多等待时间。" + }, + "llm_retries": { + "type": "int", + "description": "LLM 请求重试次数", + "default": 2, + "hint": "当请求超时或失败时自动重试的次数,建议设置为1~3之间。" + }, + "llm_backoff": { + "type": "int", + "description": "LLM 请求重试退避基值(秒)", + "default": 2, + "hint": "重试之间的基准等待时间(秒),实际等待时间为基值乘以尝试次数。" + }, "pdf_output_dir": { "type": "string", "description": "PDF输出目录", diff --git a/src/analysis/llm_analyzer.py b/src/analysis/llm_analyzer.py index bb91437..f7c2689 100644 --- a/src/analysis/llm_analyzer.py +++ b/src/analysis/llm_analyzer.py @@ -6,6 +6,7 @@ LLM分析器模块 import json import re from datetime import datetime +import asyncio from typing import List, Dict, Tuple from astrbot.api import logger from ...src.models.data_models import SummaryTopic, UserTitle, GoldenQuote, TokenUsage @@ -18,6 +19,33 @@ class LLMAnalyzer: self.context = context self.config_manager = config_manager + async def _call_provider_with_retry(self, provider, prompt: str, max_tokens: int, temperature: float): + """调用LLM提供者,带超时、重试与退避。""" + + timeout = self.config_manager.get_llm_timeout() + retries = self.config_manager.get_llm_retries() + backoff = self.config_manager.get_llm_backoff() + + last_exc = None + for attempt in range(1, retries + 1): + try: + coro = provider.text_chat(prompt=prompt, max_tokens=max_tokens, temperature=temperature) + return await asyncio.wait_for(coro, timeout=timeout) + except asyncio.TimeoutError as e: + last_exc = e + logger.warning(f"LLM请求超时: 第{attempt}次, timeout={timeout}s") + except Exception as e: + last_exc = e + logger.warning(f"LLM请求失败: 第{attempt}次, 错误: {e}") + + # 若非最后一次,等待退避后重试 + if attempt < retries: + await asyncio.sleep(backoff * attempt) + + # 最终仍失败,记录错误并返回 None 由调用方处理降级,避免抛出异常 + logger.error(f"LLM请求全部重试失败: {last_exc}") + return None + async def analyze_topics(self, messages: List[Dict]) -> Tuple[List[SummaryTopic], TokenUsage]: """使用LLM分析话题""" try: @@ -108,11 +136,10 @@ class LLMAnalyzer: logger.warning("未配置LLM提供商,跳过话题分析") return [], TokenUsage() - response = await provider.text_chat( - prompt=prompt, - max_tokens=10000, - temperature=0.6 - ) + response = await self._call_provider_with_retry(provider, prompt, max_tokens=10000, temperature=0.6) + if response is None: + logger.error("话题分析调用LLM失败: provider返回None(重试失败)") + return [], TokenUsage() # 提取token使用统计 token_usage = TokenUsage() @@ -336,11 +363,10 @@ class LLMAnalyzer: logger.warning("未配置LLM提供商,跳过用户称号分析") return [], TokenUsage() - response = await provider.text_chat( - prompt=prompt, - max_tokens=1500, - temperature=0.5 - ) + response = await self._call_provider_with_retry(provider, prompt, max_tokens=1500, temperature=0.5) + if response is None: + logger.error("用户称号分析调用LLM失败: provider返回None(重试失败)") + return [], TokenUsage() # 提取token使用统计 token_usage = TokenUsage() @@ -440,11 +466,10 @@ class LLMAnalyzer: logger.warning("未配置LLM提供商,跳过金句分析") return [], TokenUsage() - response = await provider.text_chat( - prompt=prompt, - max_tokens=1500, - temperature=0.7 - ) + response = await self._call_provider_with_retry(provider, prompt, max_tokens=1500, temperature=0.7) + if response is None: + logger.error("金句分析调用LLM失败: provider返回None(重试失败)") + return [], TokenUsage() # 提取token使用统计 token_usage = TokenUsage() diff --git a/src/core/config.py b/src/core/config.py index 7bb908b..0243ac3 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -71,6 +71,18 @@ class ConfigManager: """获取最大查询轮数""" return self.config.get("max_query_rounds", 35) + def get_llm_timeout(self) -> int: + """获取LLM请求超时时间(秒)""" + return self.config.get("llm_timeout", 30) + + def get_llm_retries(self) -> int: + """获取LLM请求重试次数""" + return self.config.get("llm_retries", 2) + + def get_llm_backoff(self) -> int: + """获取LLM请求重试退避基值(秒),实际退避会乘以尝试次数""" + return self.config.get("llm_backoff", 2) + def get_pdf_output_dir(self) -> str: """获取PDF输出目录""" return self.config.get("pdf_output_dir", "data/plugins/astrbot-qq-group-daily-analysis/reports")