212 lines
8.0 KiB
Python
212 lines
8.0 KiB
Python
"""模型生命周期管理服务(P1)"""
|
||
|
||
from typing import Any, Dict, List, Optional
|
||
|
||
from sqlalchemy import and_, select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from ...models import (
|
||
ModelRequirementBinding,
|
||
ModelUpdateImpact,
|
||
ModelVersion,
|
||
)
|
||
|
||
|
||
class ModelLifecycleService:
|
||
"""模型更新影响评估与迁移任务生成。"""
|
||
|
||
def __init__(self, db: AsyncSession):
|
||
self.db = db
|
||
|
||
async def assess_model_update_impact(
|
||
self,
|
||
old_model_version_id: int,
|
||
new_model_version_id: int,
|
||
tenant_id: int,
|
||
) -> Dict[str, Any]:
|
||
old_model = await self._get_model(old_model_version_id)
|
||
new_model = await self._get_model(new_model_version_id)
|
||
if not old_model or not new_model:
|
||
raise ValueError("模型版本不存在")
|
||
|
||
affected = await self._get_affected_truth_nodes(old_model_version_id, tenant_id)
|
||
cost_change = self._calc_cost_change_percent(old_model, new_model)
|
||
performance_change = self._infer_performance_change(old_model, new_model)
|
||
risks = self._infer_compatibility_risks(old_model, new_model)
|
||
recommendation = self._recommend(cost_change, performance_change, risks)
|
||
|
||
impact = ModelUpdateImpact(
|
||
tenant_id=tenant_id,
|
||
old_model_version_id=old_model_version_id,
|
||
new_model_version_id=new_model_version_id,
|
||
affected_truth_nodes=affected,
|
||
cost_change_percentage=cost_change,
|
||
performance_change=performance_change,
|
||
compatibility_risks=risks,
|
||
migration_tasks={},
|
||
recommendation=recommendation,
|
||
status="pending_review",
|
||
)
|
||
self.db.add(impact)
|
||
await self.db.commit()
|
||
await self.db.refresh(impact)
|
||
|
||
return {
|
||
"impact_id": impact.id,
|
||
"affected_truth_nodes": affected,
|
||
"cost_change_percentage": cost_change,
|
||
"performance_change": performance_change,
|
||
"compatibility_risks": risks,
|
||
"recommendation": recommendation,
|
||
"status": impact.status,
|
||
}
|
||
|
||
async def generate_migration_tasks(
|
||
self,
|
||
impact_id: int,
|
||
tenant_id: int,
|
||
max_tasks: int = 5,
|
||
) -> Dict[str, Any]:
|
||
impact = await self._get_impact(impact_id, tenant_id)
|
||
if not impact:
|
||
raise ValueError("影响评估记录不存在")
|
||
|
||
tasks: List[Dict[str, Any]] = []
|
||
affected_map = impact.affected_truth_nodes or {}
|
||
for truth_node_id, score in list(affected_map.items())[:max_tasks]:
|
||
tasks.append(
|
||
{
|
||
"title": f"评估并迁移宪章 {truth_node_id} 到新模型",
|
||
"type": "ai",
|
||
"priority": "high" if float(score) >= 0.7 else "medium",
|
||
"estimated_hours": 2.0 if float(score) >= 0.7 else 1.0,
|
||
"notes": f"旧模型 -> 新模型影响分: {score}",
|
||
}
|
||
)
|
||
|
||
if impact.compatibility_risks:
|
||
tasks.append(
|
||
{
|
||
"title": "执行回归验证(模型升级)",
|
||
"type": "backend",
|
||
"priority": "high",
|
||
"estimated_hours": 3.0,
|
||
"notes": "覆盖接口行为、输出一致性、预算消耗变化",
|
||
}
|
||
)
|
||
|
||
impact.migration_tasks = {"tasks": tasks[:max_tasks]}
|
||
impact.status = "in_progress"
|
||
await self.db.commit()
|
||
await self.db.refresh(impact)
|
||
|
||
return {
|
||
"impact_id": impact.id,
|
||
"status": impact.status,
|
||
"migration_tasks": impact.migration_tasks,
|
||
}
|
||
|
||
async def list_impacts(self, tenant_id: int, limit: int = 50) -> Dict[str, Any]:
|
||
result = await self.db.execute(
|
||
select(ModelUpdateImpact)
|
||
.where(ModelUpdateImpact.tenant_id == tenant_id)
|
||
.order_by(ModelUpdateImpact.created_at.desc())
|
||
.limit(limit)
|
||
)
|
||
impacts = result.scalars().all()
|
||
return {
|
||
"items": [
|
||
{
|
||
"id": i.id,
|
||
"old_model_version_id": i.old_model_version_id,
|
||
"new_model_version_id": i.new_model_version_id,
|
||
"cost_change_percentage": i.cost_change_percentage,
|
||
"recommendation": i.recommendation,
|
||
"status": i.status,
|
||
"created_at": i.created_at.isoformat() if i.created_at else None,
|
||
}
|
||
for i in impacts
|
||
]
|
||
}
|
||
|
||
async def _get_model(self, model_version_id: int) -> Optional[ModelVersion]:
|
||
result = await self.db.execute(
|
||
select(ModelVersion).where(ModelVersion.id == model_version_id)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
async def _get_impact(self, impact_id: int, tenant_id: int) -> Optional[ModelUpdateImpact]:
|
||
result = await self.db.execute(
|
||
select(ModelUpdateImpact).where(
|
||
and_(ModelUpdateImpact.id == impact_id, ModelUpdateImpact.tenant_id == tenant_id)
|
||
)
|
||
)
|
||
return result.scalar_one_or_none()
|
||
|
||
async def _get_affected_truth_nodes(
|
||
self, old_model_version_id: int, tenant_id: int
|
||
) -> Dict[str, float]:
|
||
result = await self.db.execute(
|
||
select(ModelRequirementBinding).where(
|
||
and_(
|
||
ModelRequirementBinding.model_version_id == old_model_version_id,
|
||
ModelRequirementBinding.tenant_id == tenant_id,
|
||
)
|
||
)
|
||
)
|
||
bindings = result.scalars().all()
|
||
|
||
affected: Dict[str, float] = {}
|
||
for b in bindings:
|
||
# 兼容度越低,升级影响越高。
|
||
impact_score = round(max(0.1, (100 - int(b.compatibility_score)) / 100), 2)
|
||
affected[str(b.truth_node_id)] = impact_score
|
||
|
||
# 如果当前没有绑定,返回空映射。
|
||
return affected
|
||
|
||
def _calc_cost_change_percent(self, old_model: ModelVersion, new_model: ModelVersion) -> float:
|
||
old_cost = float(old_model.cost_per_1k_tokens or 0.0)
|
||
new_cost = float(new_model.cost_per_1k_tokens or 0.0)
|
||
if old_cost <= 0:
|
||
return 0.0
|
||
return round(((new_cost - old_cost) / old_cost) * 100, 2)
|
||
|
||
def _infer_performance_change(
|
||
self, old_model: ModelVersion, new_model: ModelVersion
|
||
) -> Dict[str, float]:
|
||
old_caps = old_model.capability_boundaries or {}
|
||
new_caps = new_model.capability_boundaries or {}
|
||
old_ctx = float(old_caps.get("context_window", 0) or 0)
|
||
new_ctx = float(new_caps.get("context_window", 0) or 0)
|
||
ctx_change = 0.0 if old_ctx <= 0 else round(((new_ctx - old_ctx) / old_ctx) * 100, 2)
|
||
quality_change = round(10.0 if len(new_caps.keys()) > len(old_caps.keys()) else 0.0, 2)
|
||
return {
|
||
"context_window_change_percent": ctx_change,
|
||
"quality_change_percent": quality_change,
|
||
}
|
||
|
||
def _infer_compatibility_risks(
|
||
self, old_model: ModelVersion, new_model: ModelVersion
|
||
) -> Dict[str, Any]:
|
||
risks: List[Dict[str, str]] = []
|
||
if old_model.provider != new_model.provider:
|
||
risks.append({"type": "provider_change", "severity": "high"})
|
||
if old_model.name != new_model.name:
|
||
risks.append({"type": "model_family_change", "severity": "medium"})
|
||
return {"risks": risks, "count": len(risks)}
|
||
|
||
def _recommend(
|
||
self,
|
||
cost_change: float,
|
||
performance_change: Dict[str, float],
|
||
risks: Dict[str, Any],
|
||
) -> str:
|
||
if risks.get("count", 0) > 0 and performance_change.get("quality_change_percent", 0) <= 0:
|
||
return "evaluate"
|
||
if cost_change <= 10 and performance_change.get("quality_change_percent", 0) >= 5:
|
||
return "immediate_upgrade"
|
||
if cost_change > 30 and performance_change.get("quality_change_percent", 0) < 5:
|
||
return "defer"
|
||
return "evaluate"
|