"""
多模型LLM服务 - 支持智谱AI和DeepSeek
"""
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
from app.config import get_settings
from app.utils.logger import logger
# 智谱AI
try:
from zhipuai import ZhipuAI
ZHIPUAI_AVAILABLE = True
except ImportError:
ZHIPUAI_AVAILABLE = False
logger.warning("zhipuai包未安装")
# DeepSeek (使用OpenAI兼容接口)
try:
from openai import OpenAI
from openai import APIStatusError, APIError
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
logger.warning("openai包未安装")
class MultiLLMService:
"""多模型LLM服务类"""
# 余额错误通知冷却时间(秒)
BALANCE_ERROR_COOLDOWN = 3600 # 1小时内只通知一次
def __init__(self):
"""初始化多模型LLM服务"""
settings = get_settings()
self.clients = {}
self.current_model = None
self.model_info = {}
# 余额错误通知时间记录
self._balance_error_notified = {} # {provider: last_notified_time}
# 初始化智谱AI
if ZHIPUAI_AVAILABLE and settings.zhipuai_api_key:
try:
api_key = settings.zhipuai_api_key.strip()
if '.' in api_key and len(api_key) > 10:
self.clients['zhipu'] = ZhipuAI(api_key=api_key)
self.model_info['zhipu'] = {
'name': 'GLM-4-Flash',
'model_id': 'glm-4-flash',
'provider': 'zhipu',
'available': True
}
logger.info("智谱AI初始化成功 (使用模型: glm-4-flash)")
except Exception as e:
logger.error(f"智谱AI初始化失败: {e}")
# 初始化DeepSeek
if OPENAI_AVAILABLE and settings.deepseek_api_key:
try:
self.clients['deepseek'] = OpenAI(
api_key=settings.deepseek_api_key,
base_url="https://api.deepseek.com"
)
self.model_info['deepseek'] = {
'name': 'DeepSeek',
'model_id': 'deepseek-chat',
'provider': 'deepseek',
'available': True
}
logger.info("DeepSeek初始化成功")
except Exception as e:
logger.error(f"DeepSeek初始化失败: {e}")
# 设置默认模型(优先使用配置文件中的设置)
preferred_model = getattr(settings, 'crypto_agent_model', None)
if preferred_model and preferred_model in self.clients:
self.current_model = preferred_model
logger.info(f"使用配置的模型: {preferred_model}")
elif 'deepseek' in self.clients:
self.current_model = 'deepseek'
elif 'zhipu' in self.clients:
self.current_model = 'zhipu'
if self.current_model:
logger.info(f"当前使用模型: {self.model_info[self.current_model]['name']}")
else:
logger.warning("没有可用的LLM模型")
def get_available_models(self) -> List[Dict[str, Any]]:
"""获取所有可用的模型列表"""
return [info for info in self.model_info.values() if info['available']]
def get_current_model_info(self) -> Optional[Dict[str, Any]]:
"""获取当前使用的模型信息"""
if self.current_model:
return self.model_info[self.current_model]
return None
def switch_model(self, provider: str) -> bool:
"""
切换模型
Args:
provider: 模型提供商 ('zhipu' 或 'deepseek')
Returns:
是否切换成功
"""
if provider in self.clients:
self.current_model = provider
logger.info(f"切换到模型: {self.model_info[provider]['name']}")
return True
else:
logger.error(f"模型不可用: {provider}")
return False
def _is_balance_error(self, error: Exception, provider: str) -> bool:
"""
检查错误是否是余额不足错误
Args:
error: 异常对象
provider: LLM提供商
Returns:
是否是余额不足错误
"""
error_str = str(error).lower()
error_type = type(error).__name__
# DeepSeek 余额错误
if provider == 'deepseek':
# APIStatusError: Error code: 402 - {'error': {'message': 'Insufficient Balance'
if '402' in error_str and 'insufficient balance' in error_str:
return True
if 'balance' in error_str and 'insufficient' in error_str:
return True
# 智谱AI 余额错误
elif provider == 'zhipu':
# 常见错误信息
if '余额' in error_str or 'balance' in error_str:
if 'insufficient' in error_str or '不足' in error_str:
return True
if error_type == 'APIError' and '130' in error_str: # 智谱错误码130表示余额不足
return True
return False
async def _notify_balance_error(self, provider: str, error: Exception):
"""
发送余额不足的通知(Telegram + 飞书)
Args:
provider: LLM提供商
error: 异常对象
"""
# 检查冷却时间
now = datetime.now()
last_notified = self._balance_error_notified.get(provider)
if last_notified:
time_since_last = (now - last_notified).total_seconds()
if time_since_last < self.BALANCE_ERROR_COOLDOWN:
logger.info(f"{provider} 余额错误通知冷却中,剩余 {int(self.BALANCE_ERROR_COOLDOWN - time_since_last)} 秒")
return
# 发送通知
try:
from app.services.telegram_service import get_telegram_service
from app.services.feishu_service import get_feishu_service
telegram = get_telegram_service()
feishu = get_feishu_service()
provider_name = {
'zhipu': '智谱AI (GLM-4)',
'deepseek': 'DeepSeek'
}.get(provider, provider)
# Telegram 通知
telegram_message = f"""🚨 LLM API 余额不足警告
━━━━━━━━━━━━━━━━━━━━
📊 服务商: {provider_name}
⚠️ 错误类型: 余额不足 (Insufficient Balance)
🔍 错误信息: {str(error)[:200]}
━━━━━━━━━━━━━━━━━━━━
请及时充值,否则智能体将无法正常工作"""
await telegram.send_message(telegram_message, parse_mode="HTML")
logger.info(f"已发送 {provider} 余额不足 Telegram 通知")
# 飞书通知
feishu_message = f"""🚨 **LLM API 余额不足警告**
**服务商**: {provider_name}
**错误类型**: 余额不足 (Insufficient Balance)
**错误信息**: {str(error)[:200]}
**时间**: {now.strftime('%Y-%m-%d %H:%M:%S')}
⚠️ 请及时充值,否则智能体将无法正常工作"""
await feishu.send_text(feishu_message)
logger.info(f"已发送 {provider} 余额不足飞书通知")
# 记录通知时间
self._balance_error_notified[provider] = now
except Exception as e:
logger.error(f"发送余额不足通知失败: {e}")
def chat(
self,
messages: List[Dict[str, str]],
temperature: float = 0.7,
max_tokens: int = 2000,
model_override: Optional[str] = None
) -> Optional[str]:
"""
调用LLM进行对话
Args:
messages: 消息列表
temperature: 温度参数
max_tokens: 最大token数
model_override: 临时覆盖使用的模型
Returns:
LLM响应文本
"""
provider = model_override or self.current_model
if not provider or provider not in self.clients:
logger.error("没有可用的LLM客户端")
return None
try:
client = self.clients[provider]
model_id = self.model_info[provider]['model_id']
logger.info(f"调用LLM: provider={provider}, model={model_id}, messages={len(messages)}条")
if provider == 'zhipu':
# 智谱AI调用
# Zhipu对参数更严格,temperature范围是0.0-1.0
safe_temperature = max(0.0, min(1.0, temperature))
logger.debug(f"智谱AI请求参数: model={model_id}, temperature={safe_temperature}, max_tokens={max_tokens}")
logger.debug(f"消息内容: {messages[-1]['content'][:200] if messages else 'empty'}...")
response = client.chat.completions.create(
model=model_id,
messages=messages,
temperature=safe_temperature,
max_tokens=max_tokens
)
logger.debug(f"智谱AI原始响应: {response}")
elif provider == 'deepseek':
# DeepSeek调用(OpenAI兼容)
# DeepSeek对参数更严格,确保temperature在有效范围内
safe_temperature = max(0.0, min(2.0, temperature))
response = client.chat.completions.create(
model=model_id,
messages=messages,
temperature=safe_temperature,
max_tokens=max_tokens
)
else:
logger.error(f"未知的模型提供商: {provider}")
return None
# 详细日志记录响应结构
logger.debug(f"响应对象类型: {type(response)}")
if hasattr(response, 'choices') and response.choices:
choice = response.choices[0]
logger.debug(f"Choice类型: {type(choice)}, 索引: {getattr(choice, 'index', 'N/A')}")
message = choice.message
logger.debug(f"Message类型: {type(message)}")
# 智谱AI可能使用不同的字段
content = getattr(message, 'content', None)
if content and content.strip():
logger.info(f"LLM响应成功,长度: {len(content)}")
return content
else:
logger.warning(f"LLM响应content为空或空白: content={repr(content)}")
# 尝试从其他字段获取内容(智谱AI可能使用不同字段)
for attr in ['reasoning_content', 'text', 'result']:
if hasattr(message, attr):
alt_content = getattr(message, attr)
if alt_content and alt_content.strip():
logger.info(f"从 {attr} 获取内容,长度: {len(alt_content)}")
return alt_content
# 打印完整的message对象用于调试
logger.debug(f"完整Message对象: {message}")
if hasattr(message, '__dict__'):
logger.debug(f"Message属性: {message.__dict__}")
return None
else:
logger.warning(f"LLM响应中没有choices。响应: {response}")
# 检查是否有其他可能的响应格式
if hasattr(response, 'content'):
content = response.content
if content and content.strip():
logger.info(f"从response.content获取内容,长度: {len(content)}")
return content
return None
except Exception as e:
logger.error(f"LLM调用失败: {type(e).__name__}: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
# 检查是否是余额错误,发送Telegram通知
if self._is_balance_error(e, provider):
import asyncio
try:
# 在新的事件循环中运行(避免嵌套事件循环问题)
loop = asyncio.get_event_loop()
if loop.is_running():
# 如果在异步上下文中,创建任务
asyncio.create_task(self._notify_balance_error(provider, e))
else:
# 如果没有运行的循环,直接运行
asyncio.run(self._notify_balance_error(provider, e))
except Exception as notify_error:
logger.error(f"发送余额通知异常: {notify_error}")
return None
def chat_stream(
self,
messages: List[Dict[str, str]],
temperature: float = 0.7,
max_tokens: int = 2000,
model_override: Optional[str] = None
):
"""
流式调用LLM进行对话
Args:
messages: 消息列表
temperature: 温度参数
max_tokens: 最大token数
model_override: 临时覆盖使用的模型
Yields:
LLM响应的文本片段
"""
provider = model_override or self.current_model
if not provider or provider not in self.clients:
logger.error("没有可用的LLM客户端")
return
try:
client = self.clients[provider]
model_id = self.model_info[provider]['model_id']
logger.info(f"流式调用LLM: provider={provider}, model={model_id}, messages={len(messages)}条")
if provider == 'zhipu':
# 智谱AI流式调用
# Zhipu对参数更严格,temperature范围是0.0-1.0
safe_temperature = max(0.0, min(1.0, temperature))
response = client.chat.completions.create(
model=model_id,
messages=messages,
temperature=safe_temperature,
max_tokens=max_tokens,
stream=True
)
for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
elif provider == 'deepseek':
# DeepSeek流式调用(OpenAI兼容)
# DeepSeek对参数更严格,确保temperature在有效范围内
safe_temperature = max(0.0, min(2.0, temperature))
response = client.chat.completions.create(
model=model_id,
messages=messages,
temperature=safe_temperature,
max_tokens=max_tokens,
stream=True
)
for chunk in response:
if chunk.choices and chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
else:
logger.error(f"未知的模型提供商: {provider}")
return
logger.info("LLM流式响应完成")
except Exception as e:
logger.error(f"LLM流式调用失败: {type(e).__name__}: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
# 检查是否是余额错误,发送Telegram通知
if self._is_balance_error(e, provider):
import asyncio
try:
# 在新的事件循环中运行(避免嵌套事件循环问题)
loop = asyncio.get_event_loop()
if loop.is_running():
# 如果在异步上下文中,创建任务
asyncio.create_task(self._notify_balance_error(provider, e))
else:
# 如果没有运行的循环,直接运行
asyncio.run(self._notify_balance_error(provider, e))
except Exception as notify_error:
logger.error(f"发送余额通知异常: {notify_error}")
return
# 创建全局实例
multi_llm_service = MultiLLMService()