主要修复: 1. 创建统一Redis连接管理器 (src/core/redis_manager.py) - 单例模式管理所有Redis连接 - 懒加载连接,避免重复初始化 - 线程安全的连接管理 2. 更新所有Redis使用模块 - TokenMonitor: 使用统一Redis管理器 - AISuccessMonitor: 移除重复Redis连接代码 - SystemOptimizer: 统一Redis连接管理 - ConversationHistoryManager: 使用统一Redis管理器 3. 修复DialogueManager重复初始化 - 使用懒加载属性(@property)避免重复创建监控器 - 只有在实际使用时才创建实例 4. 优化启动性能 - 避免重复的Redis连接创建 - 消除重复的TSP助手初始化 - 减少启动时的日志输出 技术改进: - 单例模式Redis管理器 - 懒加载组件初始化 - 统一连接管理 - 线程安全设计 解决启动卡顿问题,提升系统响应速度
493 lines
17 KiB
Python
493 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Token消耗监控模块
|
||
监控AI调用的Token使用情况和成本
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
from typing import Dict, List, Optional, Any, Tuple
|
||
from datetime import datetime, timedelta
|
||
from dataclasses import dataclass
|
||
from collections import defaultdict
|
||
from ..core.database import db_manager
|
||
from ..core.models import Conversation
|
||
from ..core.redis_manager import redis_manager
|
||
from ..config.config import Config
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
@dataclass
|
||
class TokenUsage:
|
||
"""Token使用记录"""
|
||
timestamp: datetime
|
||
user_id: str
|
||
work_order_id: Optional[int]
|
||
model_name: str
|
||
input_tokens: int
|
||
output_tokens: int
|
||
total_tokens: int
|
||
cost: float
|
||
response_time: float
|
||
success: bool
|
||
error_message: Optional[str] = None
|
||
|
||
class TokenMonitor:
|
||
"""Token消耗监控器"""
|
||
|
||
def __init__(self):
|
||
# 使用统一的Redis管理器
|
||
|
||
# Token价格配置(每1000个token的价格,单位:元)
|
||
self.token_prices = {
|
||
"qwen-plus-latest": {
|
||
"input": 0.002, # 输入token价格
|
||
"output": 0.006 # 输出token价格
|
||
},
|
||
"qwen-turbo": {
|
||
"input": 0.0008,
|
||
"output": 0.002
|
||
},
|
||
"qwen-max": {
|
||
"input": 0.02,
|
||
"output": 0.06
|
||
}
|
||
}
|
||
|
||
# 监控阈值
|
||
self.thresholds = {
|
||
"daily_cost_limit": 100.0, # 每日成本限制(元)
|
||
"hourly_cost_limit": 20.0, # 每小时成本限制(元)
|
||
"token_limit_per_request": 10000, # 单次请求token限制
|
||
"error_rate_threshold": 0.1 # 错误率阈值
|
||
}
|
||
|
||
def _get_redis_client(self):
|
||
"""获取Redis客户端"""
|
||
return redis_manager.get_connection()
|
||
|
||
def record_token_usage(
|
||
self,
|
||
user_id: str,
|
||
work_order_id: Optional[int],
|
||
model_name: str,
|
||
input_tokens: int,
|
||
output_tokens: int,
|
||
response_time: float,
|
||
success: bool = True,
|
||
error_message: Optional[str] = None
|
||
) -> TokenUsage:
|
||
"""记录Token使用情况"""
|
||
try:
|
||
total_tokens = input_tokens + output_tokens
|
||
|
||
# 计算成本
|
||
cost = self._calculate_cost(model_name, input_tokens, output_tokens)
|
||
|
||
# 创建使用记录
|
||
usage = TokenUsage(
|
||
timestamp=datetime.now(),
|
||
user_id=user_id,
|
||
work_order_id=work_order_id,
|
||
model_name=model_name,
|
||
input_tokens=input_tokens,
|
||
output_tokens=output_tokens,
|
||
total_tokens=total_tokens,
|
||
cost=cost,
|
||
response_time=response_time,
|
||
success=success,
|
||
error_message=error_message
|
||
)
|
||
|
||
# 保存到Redis
|
||
self._save_to_redis(usage)
|
||
|
||
# 检查阈值
|
||
self._check_thresholds(usage)
|
||
|
||
logger.info(f"Token使用记录: {total_tokens} tokens, 成本: {cost:.4f}元")
|
||
return usage
|
||
|
||
except Exception as e:
|
||
logger.error(f"记录Token使用失败: {e}")
|
||
return None
|
||
|
||
def _calculate_cost(self, model_name: str, input_tokens: int, output_tokens: int) -> float:
|
||
"""计算Token成本"""
|
||
if model_name not in self.token_prices:
|
||
model_name = "qwen-plus-latest" # 默认模型
|
||
|
||
prices = self.token_prices[model_name]
|
||
input_cost = (input_tokens / 1000) * prices["input"]
|
||
output_cost = (output_tokens / 1000) * prices["output"]
|
||
|
||
return input_cost + output_cost
|
||
|
||
def _save_to_redis(self, usage: TokenUsage):
|
||
"""保存到Redis"""
|
||
redis_client = self._get_redis_client()
|
||
|
||
if not redis_client:
|
||
return
|
||
|
||
try:
|
||
# 保存到时间序列
|
||
timestamp = usage.timestamp.timestamp()
|
||
usage_data = {
|
||
"user_id": usage.user_id,
|
||
"work_order_id": usage.work_order_id,
|
||
"model_name": usage.model_name,
|
||
"input_tokens": usage.input_tokens,
|
||
"output_tokens": usage.output_tokens,
|
||
"total_tokens": usage.total_tokens,
|
||
"cost": usage.cost,
|
||
"response_time": usage.response_time,
|
||
"success": usage.success,
|
||
"error_message": usage.error_message
|
||
}
|
||
|
||
# 保存到多个键
|
||
redis_client.zadd(
|
||
"token_usage:daily",
|
||
{json.dumps(usage_data, ensure_ascii=False): timestamp}
|
||
)
|
||
|
||
redis_client.zadd(
|
||
f"token_usage:user:{usage.user_id}",
|
||
{json.dumps(usage_data, ensure_ascii=False): timestamp}
|
||
)
|
||
|
||
if usage.work_order_id:
|
||
redis_client.zadd(
|
||
f"token_usage:work_order:{usage.work_order_id}",
|
||
{json.dumps(usage_data, ensure_ascii=False): timestamp}
|
||
)
|
||
|
||
# 设置过期时间(保留30天)
|
||
redis_client.expire("token_usage:daily", 30 * 24 * 3600)
|
||
|
||
except Exception as e:
|
||
logger.error(f"保存Token使用到Redis失败: {e}")
|
||
|
||
def _check_thresholds(self, usage: TokenUsage):
|
||
"""检查阈值并触发预警"""
|
||
try:
|
||
# 检查单次请求token限制
|
||
if usage.total_tokens > self.thresholds["token_limit_per_request"]:
|
||
self._trigger_alert(
|
||
"high_token_usage",
|
||
f"单次请求Token使用过多: {usage.total_tokens}",
|
||
"warning"
|
||
)
|
||
|
||
# 检查今日成本
|
||
daily_cost = self.get_daily_cost(usage.timestamp.date())
|
||
if daily_cost > self.thresholds["daily_cost_limit"]:
|
||
self._trigger_alert(
|
||
"daily_cost_exceeded",
|
||
f"今日成本超限: {daily_cost:.2f}元",
|
||
"critical"
|
||
)
|
||
|
||
# 检查每小时成本
|
||
hourly_cost = self.get_hourly_cost(usage.timestamp)
|
||
if hourly_cost > self.thresholds["hourly_cost_limit"]:
|
||
self._trigger_alert(
|
||
"hourly_cost_exceeded",
|
||
f"每小时成本超限: {hourly_cost:.2f}元",
|
||
"warning"
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"检查阈值失败: {e}")
|
||
|
||
def _trigger_alert(self, alert_type: str, message: str, severity: str):
|
||
"""触发预警"""
|
||
try:
|
||
from ..core.models import Alert
|
||
|
||
with db_manager.get_session() as session:
|
||
alert = Alert(
|
||
rule_name=f"Token监控_{alert_type}",
|
||
alert_type=alert_type,
|
||
level=severity,
|
||
severity=severity,
|
||
message=message,
|
||
is_active=True,
|
||
created_at=datetime.now()
|
||
)
|
||
session.add(alert)
|
||
session.commit()
|
||
|
||
logger.warning(f"Token监控预警: {message}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"触发Token监控预警失败: {e}")
|
||
|
||
def get_daily_cost(self, date: datetime.date) -> float:
|
||
"""获取指定日期的成本"""
|
||
try:
|
||
redis_client = self._get_redis_client()
|
||
|
||
if not redis_client:
|
||
return 0.0
|
||
|
||
start_time = datetime.combine(date, datetime.min.time()).timestamp()
|
||
end_time = datetime.combine(date, datetime.max.time()).timestamp()
|
||
|
||
# 从Redis获取当日数据
|
||
usage_records = redis_client.zrangebyscore(
|
||
"token_usage:daily",
|
||
start_time,
|
||
end_time,
|
||
withscores=True
|
||
)
|
||
|
||
total_cost = 0.0
|
||
for record_data, _ in usage_records:
|
||
try:
|
||
record = json.loads(record_data)
|
||
total_cost += record.get("cost", 0)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
return total_cost
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取日成本失败: {e}")
|
||
return 0.0
|
||
|
||
def get_hourly_cost(self, timestamp: datetime) -> float:
|
||
"""获取指定小时的成本"""
|
||
try:
|
||
redis_client = self._get_redis_client()
|
||
|
||
if not redis_client:
|
||
return 0.0
|
||
|
||
# 获取当前小时的数据
|
||
hour_start = timestamp.replace(minute=0, second=0, microsecond=0)
|
||
hour_end = hour_start + timedelta(hours=1)
|
||
|
||
start_time = hour_start.timestamp()
|
||
end_time = hour_end.timestamp()
|
||
|
||
usage_records = redis_client.zrangebyscore(
|
||
"token_usage:daily",
|
||
start_time,
|
||
end_time,
|
||
withscores=True
|
||
)
|
||
|
||
total_cost = 0.0
|
||
for record_data, _ in usage_records:
|
||
try:
|
||
record = json.loads(record_data)
|
||
total_cost += record.get("cost", 0)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
return total_cost
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取小时成本失败: {e}")
|
||
return 0.0
|
||
|
||
def get_user_token_stats(self, user_id: str, days: int = 7) -> Dict[str, Any]:
|
||
"""获取用户Token使用统计"""
|
||
try:
|
||
redis_client = self._get_redis_client()
|
||
|
||
if not redis_client:
|
||
return {}
|
||
|
||
end_time = datetime.now().timestamp()
|
||
start_time = (datetime.now() - timedelta(days=days)).timestamp()
|
||
|
||
usage_records = redis_client.zrangebyscore(
|
||
f"token_usage:user:{user_id}",
|
||
start_time,
|
||
end_time,
|
||
withscores=True
|
||
)
|
||
|
||
stats = {
|
||
"total_tokens": 0,
|
||
"total_cost": 0.0,
|
||
"total_requests": 0,
|
||
"successful_requests": 0,
|
||
"failed_requests": 0,
|
||
"avg_response_time": 0.0,
|
||
"model_usage": defaultdict(int),
|
||
"daily_usage": defaultdict(lambda: {"tokens": 0, "cost": 0})
|
||
}
|
||
|
||
response_times = []
|
||
|
||
for record_data, timestamp in usage_records:
|
||
try:
|
||
record = json.loads(record_data)
|
||
|
||
stats["total_tokens"] += record.get("total_tokens", 0)
|
||
stats["total_cost"] += record.get("cost", 0)
|
||
stats["total_requests"] += 1
|
||
|
||
if record.get("success", True):
|
||
stats["successful_requests"] += 1
|
||
else:
|
||
stats["failed_requests"] += 1
|
||
|
||
model_name = record.get("model_name", "unknown")
|
||
stats["model_usage"][model_name] += 1
|
||
|
||
if record.get("response_time"):
|
||
response_times.append(record["response_time"])
|
||
|
||
# 按日期统计
|
||
date_str = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
|
||
stats["daily_usage"][date_str]["tokens"] += record.get("total_tokens", 0)
|
||
stats["daily_usage"][date_str]["cost"] += record.get("cost", 0)
|
||
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
# 计算平均响应时间
|
||
if response_times:
|
||
stats["avg_response_time"] = sum(response_times) / len(response_times)
|
||
|
||
# 计算成功率
|
||
if stats["total_requests"] > 0:
|
||
stats["success_rate"] = stats["successful_requests"] / stats["total_requests"]
|
||
else:
|
||
stats["success_rate"] = 0
|
||
|
||
return dict(stats)
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取用户Token统计失败: {e}")
|
||
return {}
|
||
|
||
def get_system_token_stats(self, days: int = 7) -> Dict[str, Any]:
|
||
"""获取系统Token使用统计"""
|
||
try:
|
||
redis_client = self._get_redis_client()
|
||
|
||
if not redis_client:
|
||
return {}
|
||
|
||
end_time = datetime.now().timestamp()
|
||
start_time = (datetime.now() - timedelta(days=days)).timestamp()
|
||
|
||
usage_records = redis_client.zrangebyscore(
|
||
"token_usage:daily",
|
||
start_time,
|
||
end_time,
|
||
withscores=True
|
||
)
|
||
|
||
stats = {
|
||
"total_tokens": 0,
|
||
"total_cost": 0.0,
|
||
"total_requests": 0,
|
||
"successful_requests": 0,
|
||
"failed_requests": 0,
|
||
"unique_users": set(),
|
||
"model_usage": defaultdict(int),
|
||
"daily_usage": defaultdict(lambda: {"tokens": 0, "cost": 0, "requests": 0})
|
||
}
|
||
|
||
for record_data, timestamp in usage_records:
|
||
try:
|
||
record = json.loads(record_data)
|
||
|
||
stats["total_tokens"] += record.get("total_tokens", 0)
|
||
stats["total_cost"] += record.get("cost", 0)
|
||
stats["total_requests"] += 1
|
||
|
||
if record.get("success", True):
|
||
stats["successful_requests"] += 1
|
||
else:
|
||
stats["failed_requests"] += 1
|
||
|
||
stats["unique_users"].add(record.get("user_id", ""))
|
||
|
||
model_name = record.get("model_name", "unknown")
|
||
stats["model_usage"][model_name] += 1
|
||
|
||
# 按日期统计
|
||
date_str = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d")
|
||
stats["daily_usage"][date_str]["tokens"] += record.get("total_tokens", 0)
|
||
stats["daily_usage"][date_str]["cost"] += record.get("cost", 0)
|
||
stats["daily_usage"][date_str]["requests"] += 1
|
||
|
||
except json.JSONDecodeError:
|
||
continue
|
||
|
||
# 计算成功率
|
||
if stats["total_requests"] > 0:
|
||
stats["success_rate"] = stats["successful_requests"] / stats["total_requests"]
|
||
else:
|
||
stats["success_rate"] = 0
|
||
|
||
stats["unique_users"] = len(stats["unique_users"])
|
||
|
||
return dict(stats)
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取系统Token统计失败: {e}")
|
||
return {}
|
||
|
||
def get_cost_trend(self, days: int = 30) -> List[Dict[str, Any]]:
|
||
"""获取成本趋势"""
|
||
try:
|
||
trend_data = []
|
||
|
||
for i in range(days):
|
||
date = datetime.now().date() - timedelta(days=i)
|
||
daily_cost = self.get_daily_cost(date)
|
||
|
||
trend_data.append({
|
||
"date": date.isoformat(),
|
||
"cost": daily_cost
|
||
})
|
||
|
||
return list(reversed(trend_data))
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取成本趋势失败: {e}")
|
||
return []
|
||
|
||
def cleanup_old_data(self, days: int = 30) -> int:
|
||
"""清理旧数据"""
|
||
try:
|
||
redis_client = self._get_redis_client()
|
||
|
||
if not redis_client:
|
||
return 0
|
||
|
||
cutoff_time = (datetime.now() - timedelta(days=days)).timestamp()
|
||
|
||
# 清理每日数据
|
||
removed_count = redis_client.zremrangebyscore(
|
||
"token_usage:daily",
|
||
0,
|
||
cutoff_time
|
||
)
|
||
|
||
# 清理用户数据
|
||
user_keys = redis_client.keys("token_usage:user:*")
|
||
for key in user_keys:
|
||
redis_client.zremrangebyscore(key, 0, cutoff_time)
|
||
|
||
# 清理工单数据
|
||
work_order_keys = redis_client.keys("token_usage:work_order:*")
|
||
for key in work_order_keys:
|
||
redis_client.zremrangebyscore(key, 0, cutoff_time)
|
||
|
||
logger.info(f"清理Token监控数据成功: 数量={removed_count}")
|
||
return removed_count
|
||
|
||
except Exception as e:
|
||
logger.error(f"清理Token监控数据失败: {e}")
|
||
return 0
|