PMFlow/backend/app/services/model_capability/mixin_cost_tracking.py

261 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from .shared import *
class ModelCapabilityCostTrackingMixin:
async def track_cost_and_roi(
self,
truth_node_id: int,
period: str = "weekly",
tenant_id: int = 1,
) -> Dict[str, Any]:
"""算力成本跟踪与 ROI 核算
Args:
truth_node_id: 宪章(需求)ID
period: 周期类型(daily, weekly, monthly, project)
tenant_id: 租户 ID
Returns:
包含成本明细、ROI 分析等信息的字典
"""
# 获取相关任务和模型绑定
tasks = await self._get_truth_node_tasks(truth_node_id, tenant_id)
bindings = await self._get_truth_node_bindings(truth_node_id, tenant_id)
if not tasks:
return {
"total_tokens": 0,
"total_cost_usd": 0.0,
"task_breakdown": [],
"roi_analysis": {},
"cost_variance": 0.0,
}
# 汇总 token 使用量和成本
total_tokens = 0
total_cost = 0.0
task_costs = []
for task in tasks:
# 从 AI 调用日志统计 token
task_tokens = await self._get_task_token_usage(task.id)
task_cost = await self._calculate_task_cost(task, bindings)
total_tokens += task_tokens
total_cost += task_cost
task_costs.append({
"task_id": task.id,
"task_name": task.title,
"tokens": task_tokens,
"cost_usd": round(task_cost, 4),
})
# 获取或创建成本跟踪记录
cost_tracking = await self._get_or_create_cost_tracking(
truth_node_id=truth_node_id,
period=period,
tenant_id=tenant_id,
bindings=bindings,
)
# 更新实际数据
cost_tracking.actual_tokens = total_tokens
cost_tracking.actual_cost_usd = round(total_cost, 4)
# AI 分析 ROI
truth_node = await self._get_truth_node(truth_node_id, tenant_id)
roi_analysis = await self._analyze_cost_roi(
total_cost=total_cost,
total_tokens=total_tokens,
task_count=len(tasks),
truth_node_content=str(truth_node.content or truth_node.title or "") if truth_node else "",
tenant_id=tenant_id,
)
cost_tracking.roi_score = roi_analysis.get("roi_score")
cost_tracking.roi_analysis = roi_analysis
# 生成优化建议
optimization = await self._generate_cost_optimization(
truth_node_id=truth_node_id,
tenant_id=tenant_id,
total_cost=total_cost,
)
cost_tracking.optimization_suggestions = optimization
await self.db.commit()
await self.db.refresh(cost_tracking)
# 计算成本偏差
cost_variance = total_cost - (cost_tracking.estimated_cost_usd or 0)
return {
"total_tokens": total_tokens,
"total_cost_usd": round(total_cost, 4),
"task_breakdown": task_costs,
"roi_analysis": roi_analysis,
"cost_variance": round(cost_variance, 4),
"optimization_suggestions": optimization,
}
async def _get_truth_node_tasks(
self, truth_node_id: int, tenant_id: int
) -> List[Task]:
"""获取宪章下的所有任务"""
result = await self.db.execute(
select(Task).where(
and_(
Task.truth_node_id == truth_node_id,
Task.tenant_id == tenant_id,
)
)
)
return list(result.scalars().all())
async def _get_truth_node_bindings(
self, truth_node_id: int, tenant_id: int
) -> List[ModelRequirementBinding]:
"""获取宪章的模型绑定"""
result = await self.db.execute(
select(ModelRequirementBinding).where(
and_(
ModelRequirementBinding.truth_node_id == truth_node_id,
ModelRequirementBinding.tenant_id == tenant_id,
)
)
)
return list(result.scalars().all())
async def _get_task_token_usage(self, task_id: int) -> int:
"""从 AI 调用日志统计任务的 token 使用量"""
# 注意:需要根据实际日志记录方式调整
# 这里假设日志中有 task_id 或相关标识
result = await self.db.execute(
select(func.sum(AICallLog.total_tokens)).where(
AICallLog.function_name.contains(f"task_{task_id}")
)
)
total = result.scalar() or 0
return int(total)
async def _calculate_task_cost(
self, task: Task, bindings: List[ModelRequirementBinding]
) -> float:
"""计算任务成本"""
# 如果有绑定记录,使用绑定的预估成本
if bindings:
return sum(float(b.estimated_cost or 0) for b in bindings) / max(len(bindings), 1)
return 0.0
async def _get_or_create_cost_tracking(
self,
truth_node_id: int,
period: str,
tenant_id: int,
bindings: List[ModelRequirementBinding],
) -> CostTracking:
"""获取或创建成本跟踪记录"""
# 查找现有记录
result = await self.db.execute(
select(CostTracking).where(
and_(
CostTracking.truth_node_id == truth_node_id,
CostTracking.period == period,
CostTracking.tenant_id == tenant_id,
)
).order_by(desc(CostTracking.created_at))
)
tracking = result.scalar_one_or_none()
if not tracking:
# 创建新记录
tracking = CostTracking(
tenant_id=tenant_id,
truth_node_id=truth_node_id,
model_version_id=bindings[0].model_version_id if bindings else None,
period=period,
estimated_tokens=0,
actual_tokens=0,
estimated_cost_usd=sum(float(b.estimated_cost or 0) for b in bindings),
actual_cost_usd=0.0,
roi_score=None,
roi_analysis={},
optimization_suggestions={},
)
self.db.add(tracking)
await self.db.commit()
await self.db.refresh(tracking)
return tracking
async def _analyze_cost_roi(
self,
total_cost: float,
total_tokens: int,
task_count: int,
truth_node_content: str,
tenant_id: int,
) -> Dict[str, Any]:
"""AI 分析成本效益"""
from ...config.ai_prompts import PROMPT_ANALYZE_COST_ROI
prompt = PROMPT_ANALYZE_COST_ROI.format(
total_cost=f"${total_cost:.2f}",
total_tokens=f"{total_tokens:,}",
task_count=task_count,
truth_node_content=truth_node_content[:2000], # 限制长度
)
try:
text = await self.ai_service._invoke_with_retry(
prompt,
db=self.db,
tenant_id=tenant_id,
function_name="analyze_cost_roi",
)
return self.ai_service._parse_json(text)
except Exception:
# 失败时返回默认分析
return {
"roi_score": 5.0,
"cost_efficiency": "fair",
"cost_per_task": round(total_cost / max(task_count, 1), 2),
"optimization_suggestions": [],
"benchmark_comparison": "无法分析",
}
async def _generate_cost_optimization(
self, truth_node_id: int, tenant_id: int, total_cost: float
) -> Dict[str, Any]:
"""生成成本优化建议"""
# 简单的优化建议逻辑
suggestions = []
# 检查是否可以使用更便宜的模型
bindings = await self._get_truth_node_bindings(truth_node_id, tenant_id)
if bindings:
for binding in bindings:
model = await self._get_model_version(binding.model_version_id)
if model and model.cost_per_1k_tokens and model.cost_per_1k_tokens > 0.001:
suggestions.append({
"type": "model_downgrade",
"priority": "medium",
"description": f"考虑切换到更便宜的模型替代 {model.name}",
"estimated_savings_percent": 30,
})
# 检查是否可以启用缓存
suggestions.append({
"type": "cache_optimization",
"priority": "high",
"description": "启用 Redis 缓存可减少 20-40% 重复调用",
"estimated_savings_percent": 30,
})
return {
"suggestions": suggestions,
"total_potential_savings_percent": 30 if suggestions else 0,
}