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

212 lines
8.0 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.

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