diff --git a/yuthon_ai_agent/models/ai_agent.py b/yuthon_ai_agent/models/ai_agent.py index cee5c8336..30340f155 100644 --- a/yuthon_ai_agent/models/ai_agent.py +++ b/yuthon_ai_agent/models/ai_agent.py @@ -48,8 +48,8 @@ class AiProvider(models.Model): temperature = fields.Float(string='温度', default=0.7) timeout = fields.Integer( string='超时(秒)', - default=20, - help='AI 单次请求的最大等待秒数,超过后返回超时错误。默认 20 秒。' + default=60, + help='AI 单次请求的最大等待秒数(读取超时),超过后返回超时错误。连接超时固定 10 秒。默认 60 秒。' ) active = fields.Boolean(string='启用', default=True) enable_tools = fields.Boolean( @@ -521,9 +521,9 @@ class AiConversation(models.Model): 'message': f'报表生成失败,请稍后重试或联系管理员检查服务端日志。' } - def _call_chat_completion(self, provider, messages, tools=None): + def _call_chat_completion(self, provider, messages, tools=None, retry=2): """封装一次 HTTP 调用,返回 (assistant_message_dict, error_str)。 - 同时记录 Token 使用量。 + 同时记录 Token 使用量。支持自动重试瞬态网络错误。 """ payload = { 'model': provider.model_name, @@ -534,41 +534,66 @@ class AiConversation(models.Model): if tools: payload['tools'] = tools payload['tool_choice'] = 'auto' - try: - resp = requests.post( - f"{provider.api_base}/chat/completions", - headers={ - 'Authorization': f'Bearer {provider.api_key}', - 'Content-Type': 'application/json', - }, - json=payload, - timeout=max(provider.timeout or 20, 1), - ) - if not resp.ok: - _logger.error( - f"DeepSeek API 返回 {resp.status_code}: {resp.text[:1000]}" + + read_timeout = max(provider.timeout or 60, 10) + last_error = None + + for attempt in range(retry + 1): + try: + resp = requests.post( + f"{provider.api_base}/chat/completions", + headers={ + 'Authorization': f'Bearer {provider.api_key}', + 'Content-Type': 'application/json', + }, + json=payload, + timeout=(10, read_timeout), # (connect_timeout, read_timeout) ) - resp.raise_for_status() - data = resp.json() - # 记录 Token 使用量 - usage = data.get('usage') or {} - if usage: - self.env['ai.token.usage'].sudo().create({ - 'user_id': self.env.uid, - 'conversation_id': self.id, - 'provider_id': provider.id, - 'model_name': provider.model_name, - 'prompt_tokens': usage.get('prompt_tokens', 0), - 'completion_tokens': usage.get('completion_tokens', 0), - 'total_tokens': usage.get('total_tokens', 0), - }) - return data['choices'][0]['message'], None - except requests.exceptions.Timeout: - return None, f'错误:AI 响应超时(超过 {provider.timeout or 20} 秒),请重试或在服务商配置中调高超时。' - except requests.exceptions.RequestException as e: - return None, f'错误:API 调用失败 — {e}' - except (KeyError, IndexError, ValueError) as e: - return None, f'错误:解析 AI 响应失败 — {e}' + if not resp.ok: + _logger.error( + "AI API 返回 %s: %s", resp.status_code, resp.text[:500] + ) + resp.raise_for_status() + data = resp.json() + # 记录 Token 使用量 + usage = data.get('usage') or {} + if usage: + self.env['ai.token.usage'].sudo().create({ + 'user_id': self.env.uid, + 'conversation_id': self.id, + 'provider_id': provider.id, + 'model_name': provider.model_name, + 'prompt_tokens': usage.get('prompt_tokens', 0), + 'completion_tokens': usage.get('completion_tokens', 0), + 'total_tokens': usage.get('total_tokens', 0), + }) + return data['choices'][0]['message'], None + + except requests.exceptions.Timeout: + last_error = f'AI 响应超时(超过 {read_timeout} 秒)' + if attempt < retry: + _logger.warning( + '%s,第 %s/%s 次重试...', last_error, attempt + 1, retry + ) + continue + return None, f'错误:{last_error},已重试 {retry} 次仍失败。请在服务商配置中调高超时时间或检查网络。' + + except requests.exceptions.ConnectionError as e: + last_error = f'无法连接到 AI 服务 ({e})' + if attempt < retry: + _logger.warning( + '%s,第 %s/%s 次重试...', last_error, attempt + 1, retry + ) + continue + return None, f'错误:{last_error},已重试 {retry} 次仍失败。请检查服务器能否访问外网 API。' + + except requests.exceptions.RequestException as e: + return None, f'错误:API 调用失败 — {e}' + + except (KeyError, IndexError, ValueError) as e: + return None, f'错误:解析 AI 响应失败 — {e}' + + return None, f'错误:{last_error}' def _convert_records_timezone(self, records, model_name, fields_list): """将查询结果中的 datetime 字段从 UTC 转换为用户时区"""