"""模型生命周期管理服务(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"