目 录CONTENT

文章目录

从0到1:AI算法平台后端的DDD实践

PySuper
2025-08-30 / 0 评论 / 0 点赞 / 2 阅读 / 0 字
温馨提示:
本文最后更新于2026-05-26,若内容或图片失效,请留言反馈。 所有牛逼的人都有一段苦逼的岁月。 但是你只要像SB一样去坚持,终将牛逼!!! ✊✊✊

一、引言:为什么AI平台需要DDD

领域驱动设计(Domain-Driven Design,DDD)作为一种成熟的软件设计方法论,在处理复杂业务领域时展现出独特的优势。AI算法平台恰恰是一个业务复杂度极高的领域:模型管理、训练调度、推理服务、数据处理、权限控制等核心领域相互交织,传统的三层架构(Controller-Service-DAO)往往难以清晰地表达业务意图,导致代码随着业务增长而逐渐腐化。

我所在团队在从零构建AI算法平台的过程中,选择了DDD作为核心架构方法。经过一年多的实践,我们深刻体会到DDD在处理复杂业务边界、统一语言、保证系统可演化性方面的价值。本文将详细记录我们在AI算法平台上应用DDD的完整实践,包括限界上下文划分、领域模型设计、领域事件机制、CQRS应用等核心内容。

二、DDD基础概念回顾

2.1 战略设计与限界上下文

DDD分为战略设计和战术设计两个层面。战略设计关注"做什么",战术设计关注"怎么做"。

战略设计的核心是限界上下文(Bounded Context)。限界上下文是一个业务领域的边界,在这个边界内有一套统一的术语、业务规则和数据模型。限界上下文之间的交互通过上下文映射(Context Mapping)来定义。

┌─────────────────────────────────────────────────────────────────────────┐
│                         AI算法平台限界上下文划分                         │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────────┐                                                   │
│  │   用户管理上下文   │  User Context                                    │
│  │  ┌────────────┐  │                                                   │
│  │  │  用户      │  │                                                   │
│  │  │  角色      │  │                                                   │
│  │  │  权限      │  │                                                   │
│  │  └────────────┘  │                                                   │
│  └────────┬─────────┘                                                   │
│           │  │                                                         │
│           │  │ ACL (防腐层)                                             │
│           ▼  ▼                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                         核心业务领域                               │   │
│  │                                                                     │   │
│  │  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐ │   │
│  │  │   模型管理上下文   │  │   训练管理上下文   │  │   推理服务上下文   │ │   │
│  │  │  ┌────────────┐  │  │  ┌────────────┐  │  │  ┌────────────┐  │ │   │
│  │  │  │ 模型注册  │  │  │  │ 训练任务  │  │  │  │ 推理服务  │  │ │   │
│  │  │  │ 模型版本  │  │  │  │ 数据集   │  │  │  │ 部署配置  │  │ │   │
│  │  │  │ 模型元数据│  │  │  │ 训练配置  │  │  │  │ 调用记录  │  │ │   │
│  │  │  │ 模型评测  │  │  │  │ 资源调度  │  │  │  │ 性能监控  │  │ │   │
│  │  │  └────────────┘  │  │  └────────────┘  │  │  └────────────┘  │ │   │
│  │  └──────────────────┘  └────────┬─────────┘  └──────────────────┘ │   │
│  │                                 │                                   │   │
│  │  ┌──────────────────────────────┴──────────────────────────────┐ │   │
│  │  │                    领域事件总线 (Domain Event Bus)             │ │   │
│  │  │   ModelRegistered | TrainingCompleted | DeployPromoted        │ │   │
│  │  └───────────────────────────────────────────────────────────────┘ │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2.2 战术设计:聚合根、实体、值对象

战术设计关注领域模型的具体实现,包括:

  • 聚合根(Aggregate Root):聚合的入口点,负责维护聚合内的一致性边界。一个聚合只有一个聚合根。

  • 实体(Entity):具有唯一标识的对象,其生命周期内标识不变。

  • 值对象(Value Object):没有唯一标识,仅通过属性值定义的对象,不可变。

┌─────────────────────────────────────────────────────────────────────────┐
│                         聚合设计示例:训练任务                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    TrainingTask (聚合根)                         │   │
│  │  ┌───────────────────────────────────────────────────────────┐  │   │
│  │  │ id: UUID                    # 聚合根标识                    │  │   │
│  │  │ name: str                  # 任务名称                      │  │   │
│  │  │ status: TaskStatus         # 任务状态(实体)              │  │   │
│  │  │ config: TrainingConfig     # 训练配置(值对象)            │  │   │
│  │  │ resources: ResourceSpec    # 资源规格(值对象)            │  │   │
│  │  │ metrics: TrainingMetrics   # 训练指标(实体)              │  │   │
│  │  └───────────────────────────────────────────────────────────┘  │   │
│  │                              │                                   │   │
│  │          ┌───────────────────┼───────────────────┐              │   │
│  │          ▼                   ▼                   ▼              │   │
│  │  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐          │   │
│  │  │ Checkpoint   │   │ TrainingLog │   │ Hyperparam   │          │   │
│  │  │ (实体)        │   │ (实体)        │   │ (值对象)      │          │   │
│  │  │ - id         │   │ - id         │   │ - lr         │          │   │
│  │  │ - path       │   │ - lines      │   │ - batch_size │          │   │
│  │  │ - step       │   │ - timestamp  │   │ - epochs     │          │   │
│  │  └──────────────┘   └──────────────┘   └──────────────┘          │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  业务规则:                                                              │
│  - 只有处于PENDING状态的训练任务才能启动                                  │
│  - Checkpoint只能由TrainingTask创建和管理                               │
│  - 状态变更必须通过聚合根的方法完成                                       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

三、限界上下文划分详解

3.1 模型管理上下文(Model Management Context)

模型管理上下文负责模型的注册、版本管理、元数据存储和评测。这是AI平台的核心上下文之一,与训练和推理上下文都有密切交互。

统一语言(Ubiquitous Language)

  • 模型(Model):一个可部署的AI模型

  • 模型版本(Model Version):模型的特定版本

  • 基座模型(Base Model):用于微调的基础模型

  • 评测任务(Evaluation Task):评估模型性能的测试任务

  • 评测指标(Metrics):衡量模型性能的具体指标

3.2 训练管理上下文(Training Context)

训练管理上下文处理模型训练的全生命周期,包括数据准备、训练配置、任务调度、 checkpoint管理和训练监控。

统一语言

  • 训练任务(Training Task):一次完整的训练作业

  • 数据集(Dataset):训练用的数据集

  • 超参数(Hyperparameters):训练超参数配置

  • Checkpoint:训练过程中的模型快照

  • 训练指标(Training Metrics):Loss、Accuracy等训练过程指标

3.3 推理服务上下文(Inference Context)

推理服务上下文管理模型的部署、推理API和性能监控。

统一语言

  • 推理服务(Inference Service):对外提供推理能力的Service

  • 部署配置(Deployment Config):部署规格配置

  • 推理请求(Inference Request):一次推理调用

  • 推理响应(Inference Response):推理返回结果

  • 服务指标(Service Metrics):QPS、延迟等监控指标

3.4 数据管理上下文(Data Context)

数据管理上下文处理数据集的上传、存储、版本化和数据质量控制。

统一语言

  • 数据集(Dataset):数据集合

  • 数据版本(Data Version):数据集的特定版本

  • 数据质量(Data Quality):数据质量评估结果

  • 数据血缘(Data Lineage):数据来源和加工关系

四、领域模型设计

4.1 模型管理领域模型

以下是模型管理上下文中核心领域模型的Python实现:

# domain/models/model_management.py
# 模型管理领域模型 - 展示DDD战术设计实践
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import List, Optional, Dict, Any


class ModelStatus(Enum):
    """模型状态枚举"""
    DRAFT = "draft"           # 草稿
    VALIDATING = "validating" # 验证中
    READY = "ready"           # 就绪
    DEPLOYED = "deployed"      # 已部署
    DEPRECATED = "deprecated" # 已废弃


class QuantizationType(Enum):
    """量化类型枚举"""
    FP16 = "fp16"
    FP32 = "fp32"
    INT8 = "int8"
    INT4 = "int4"
    AWQ = "awq"
    GPTQ = "gptq"


@dataclass(frozen=True)
class ModelId:
    """模型唯一标识(值对象)"""
    value: str
    
    def __post_init__(self):
        if not self.value:
            raise ValueError("模型标识不能为空")
    
    def __str__(self) -> str:
        return self.value


@dataclass(frozen=True)
class ModelVersionId:
    """模型版本唯一标识(值对象)"""
    value: str
    
    def __post_init__(self):
        if not self.value:
            raise ValueError("版本标识不能为空")


@dataclass(frozen=True)
class QuantizationConfig:
    """量化配置(值对象)"""
    quantization_type: QuantizationType
    calibration_data_size: int = 1000
    bits: int = 4
    
    def __post_init__(self):
        if self.quantization_type in (QuantizationType.INT4, QuantizationType.INT8):
            if self.bits not in (4, 8):
                raise ValueError(f"量化位数必须是4或8,实际为{self.bits}")


@dataclass
class ModelMetrics:
    """模型评测指标(实体)"""
    bleu_score: Optional[float] = None
    rouge_score: Optional[float] = None
    accuracy: Optional[float] = None
    loss: Optional[float] = None
    latency_p50_ms: Optional[float] = None
    latency_p99_ms: Optional[float] = None
    custom_metrics: Dict[str, float] = field(default_factory=dict)
    
    def add_metric(self, name: str, value: float) -> None:
        """添加自定义指标"""
        self.custom_metrics[name] = value
    
    def get_metric(self, name: str) -> Optional[float]:
        """获取指标值"""
        return self.custom_metrics.get(name)


@dataclass
class Model:
    """
    模型聚合根
    
    聚合 invariants(不变式):
    1. 模型名称在租户内唯一
    2. 至少包含一个版本才能标记为 READY
    3. 只有 READY 状态的模型才能部署
    """
    # 基础属性
    id: ModelId
    tenant_id: str
    name: str
    description: str
    
    # 版本相关
    _versions: List[ModelVersion] = field(default_factory=list)
    
    # 元数据
    base_model_type: Optional[str] = None
    framework: str = "pytorch"
    tags: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)
    
    # 状态
    status: ModelStatus = ModelStatus.DRAFT
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)
    created_by: str = ""
    
    # 聚合版本控制(用于乐观锁)
    _version: int = 0
    
    def register_version(self, version: ModelVersion) -> None:
        """
        注册新版本
        
        业务规则:
        1. 版本号必须唯一
        2. 如果是第一个版本,模型状态变为 READY
        """
        # 检查版本号唯一性
        if any(v.version == version.version for v in self._versions):
            raise ValueError(f"版本号 {version.version} 已存在")
        
        self._versions.append(version)
        self.updated_at = datetime.now()
        
        # 如果是第一个版本,更新模型状态
        if len(self._versions) == 1:
            self.status = ModelStatus.READY
    
    def get_latest_version(self) -> Optional[ModelVersion]:
        """获取最新版本"""
        if not self._versions:
            return None
        return max(self._versions, key=lambda v: v.created_at)
    
    def deprecate(self) -> None:
        """
        废弃模型
        
        业务规则:
        1. 已部署的模型不能直接废弃,需先下线
        """
        if self.status == ModelStatus.DEPLOYED:
            raise ValueError("已部署的模型需先下线才能废弃")
        self.status = ModelStatus.DEPRECATED
        self.updated_at = datetime.now()
    
    def can_deploy(self) -> bool:
        """检查是否可以部署"""
        return (
            self.status == ModelStatus.READY and 
            len(self._versions) > 0
        )


@dataclass
class ModelVersion:
    """
    模型版本(实体)
    
    作为 Model 聚合内的实体,由聚合根管理其生命周期
    """
    id: ModelVersionId
    model_id: ModelId
    version: str
    model_path: str
    
    # 模型规格
    parameter_count: int
    context_length: int = 4096
    
    # 量化配置
    quantization: Optional[QuantizationConfig] = None
    
    # 存储信息
    size_bytes: int
    checkpoint_path: Optional[str] = None
    
    # 配置
    config: Dict[str, Any] = field(default_factory=dict)
    requirements: List[str] = field(default_factory=list)
    
    # 评测指标
    metrics: ModelMetrics = field(default_factory=ModelMetrics)
    
    # 状态
    status: ModelStatus = ModelStatus.VALIDATING
    created_at: datetime = field(default_factory=datetime.now)
    created_by: str = ""
    
    def validate(self) -> None:
        """验证模型文件"""
        # TODO: 实现模型文件验证逻辑
        self.status = ModelStatus.READY
    
    def record_metrics(self, metrics: ModelMetrics) -> None:
        """记录评测指标"""
        self.metrics = metrics


@dataclass
class EvaluationTask:
    """
    评测任务(聚合根)
    
    管理模型评测的完整生命周期
    """
    class Status(Enum):
        PENDING = "pending"
        RUNNING = "running"
        COMPLETED = "completed"
        FAILED = "failed"
    
    class Type(Enum):
        AUTOMATIC = "automatic"      # 自动评测
        HUMAN = "human"              # 人工评测
        LLM_JUDGE = "llm_judge"      # LLM作为裁判
    
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    tenant_id: str = ""
    model_version_id: Optional[ModelVersionId] = None
    
    # 评测配置
    task_type: Type = Type.AUTOMATIC
    benchmark_id: str = ""
    prompt_template: str = ""
    evaluation_config: Dict[str, Any] = field(default_factory=dict)
    
    # 状态
    status: Status = Status.PENDING
    progress: float = 0.0
    
    # 结果
    results: Optional[ModelMetrics] = None
    error_message: Optional[str] = None
    
    # 时间戳
    created_at: datetime = field(default_factory=datetime.now)
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None

4.2 训练管理领域模型

# domain/models/training.py
# 训练管理领域模型
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import List, Optional, Dict, Any
import uuid


class TrainingStatus(Enum):
    """训练任务状态"""
    PENDING = "pending"           # 等待调度
    QUEUED = "queued"             # 已进入队列
    RUNNING = "running"           # 运行中
    PAUSED = "paused"             # 暂停
    SUCCEEDED = "succeeded"       # 成功完成
    FAILED = "failed"             # 失败
    CANCELLED = "cancelled"       # 已取消


@dataclass(frozen=True)
class GPUResourceSpec:
    """GPU资源规格(值对象)"""
    gpu_count: int
    gpu_type: str  # 如 "A100", "V100", "A10"
    memory_per_gpu_gb: int
    
    def total_memory_gb(self) -> int:
        return self.gpu_count * self.memory_per_gpu_gb
    
    def __post_init__(self):
        if self.gpu_count <= 0:
            raise ValueError("GPU数量必须大于0")
        if self.gpu_type not in ("A100", "V100", "A10", "H100", "L40"):
            raise ValueError(f"不支持的GPU类型: {self.gpu_type}")


@dataclass(frozen=True)
class TrainingHyperparameters:
    """训练超参数(值对象)"""
    learning_rate: float
    batch_size: int
    epochs: int
    warmup_steps: int = 0
    weight_decay: float = 0.01
    gradient_accumulation_steps: int = 1
    
    # LLM特定参数
    max_seq_length: int = 2048
    use_fp16: bool = True
    use_gradient_checkpointing: bool = False
    
    def __post_init__(self):
        if self.learning_rate <= 0:
            raise ValueError("学习率必须大于0")
        if self.batch_size <= 0:
            raise ValueError("批量大小必须大于0")
        if self.epochs <= 0:
            raise ValueError("训练轮数必须大于0")


@dataclass
class Checkpoint:
    """
    训练Checkpoint(实体)
    
    由 TrainingTask 聚合根管理
    """
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    training_task_id: str = ""
    
    # Checkpoint信息
    step: int = 0
    epoch: int = 0
    path: str = ""
    size_bytes: int = 0
    
    # 指标快照
    global_step: int = 0
    loss: Optional[float] = None
    learning_rate: Optional[float] = None
    metrics: Dict[str, float] = field(default_factory=dict)
    
    created_at: datetime = field(default_factory=datetime.now)
    
    def is_complete(self) -> bool:
        """检查Checkpoint是否完整"""
        return self.path and self.size_bytes > 0


@dataclass
class TrainingTask:
    """
    训练任务(聚合根)
    
    管理训练任务的完整生命周期和内部实体
    
    聚合 invariants:
    1. 只有 PENDING 状态才能启动
    2. Checkpoint只能通过 save_checkpoint() 方法创建
    3. 状态变更必须遵循合法转换路径
    """
    
    # 基本信息
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    tenant_id: str = ""
    name: str = ""
    description: str = ""
    
    # 关联资源
    dataset_id: str = ""
    base_model_id: str = ""
    output_model_name: str = ""
    
    # 配置
    hyperparameters: Optional[TrainingHyperparameters] = None
    resource_spec: Optional[GPUResourceSpec] = None
    
    # 训练配置
    entry_point: str = "train.py"
    python_packages: List[str] = field(default_factory=list)
    environment_vars: Dict[str, str] = field(default_factory=dict)
    
    # 状态
    status: TrainingStatus = TrainingStatus.PENDING
    progress: float = 0.0
    
    # 内部实体
    _checkpoints: List[Checkpoint] = field(default_factory=list)
    _current_step: int = 0
    
    # 结果
    final_metrics: Optional[Dict[str, float]] = None
    error_message: Optional[str] = None
    
    # Kubernetes相关信息
    kubernetes_namespace: Optional[str] = None
    kubernetes_job_name: Optional[str] = None
    
    # 时间戳
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    created_by: str = ""
    
    # 乐观锁版本
    _version: int = 0
    
    # 允许的状态转换映射
    _allowed_transitions: Dict[TrainingStatus, List[TrainingStatus]] = {
        TrainingStatus.PENDING: [TrainingStatus.QUEUED, TrainingStatus.CANCELLED],
        TrainingStatus.QUEUED: [TrainingStatus.RUNNING, TrainingStatus.CANCELLED],
        TrainingStatus.RUNNING: [
            TrainingStatus.PAUSED, 
            TrainingStatus.SUCCEEDED, 
            TrainingStatus.FAILED,
            TrainingStatus.CANCELLED
        ],
        TrainingStatus.PAUSED: [TrainingStatus.RUNNING, TrainingStatus.CANCELLED],
        TrainingStatus.FAILED: [],  # 终态
        TrainingStatus.SUCCEEDED: [],  # 终态
        TrainingStatus.CANCELLED: [],  # 终态
    }
    
    def start(self) -> None:
        """启动训练任务"""
        self._validate_transition(TrainingStatus.RUNNING)
        self.status = TrainingStatus.RUNNING
        self.started_at = datetime.now()
        self.updated_at = datetime.now()
    
    def pause(self) -> None:
        """暂停训练任务"""
        self._validate_transition(TrainingStatus.PAUSED)
        self.status = TrainingStatus.PAUSED
        self.updated_at = datetime.now()
    
    def resume(self) -> None:
        """恢复训练任务"""
        self._validate_transition(TrainingStatus.RUNNING)
        self.status = TrainingStatus.RUNNING
        self.updated_at = datetime.now()
    
    def complete(self, final_metrics: Dict[str, float]) -> None:
        """
        完成任务
        
        Args:
            final_metrics: 最终训练指标
        """
        self._validate_transition(TrainingStatus.SUCCEEDED)
        self.status = TrainingStatus.SUCCEEDED
        self.final_metrics = final_metrics
        self.progress = 1.0
        self.completed_at = datetime.now()
        self.updated_at = datetime.now()
    
    def fail(self, error_message: str) -> None:
        """标记任务失败"""
        self._validate_transition(TrainingStatus.FAILED)
        self.status = TrainingStatus.FAILED
        self.error_message = error_message
        self.completed_at = datetime.now()
        self.updated_at = datetime.now()
    
    def cancel(self) -> None:
        """取消任务"""
        self._validate_transition(TrainingStatus.CANCELLED)
        self.status = TrainingStatus.CANCELLED
        self.completed_at = datetime.now()
        self.updated_at = datetime.now()
    
    def update_progress(self, step: int, metrics: Dict[str, float]) -> None:
        """更新训练进度"""
        if self.status != TrainingStatus.RUNNING:
            raise ValueError(f"当前状态 {self.status} 不能更新进度")
        
        self._current_step = step
        self._checkpoints.append(Checkpoint(
            training_task_id=self.id,
            step=step,
            path=f"/checkpoints/{self.id}/step_{step}",
            global_step=step,
            loss=metrics.get("loss"),
            learning_rate=metrics.get("learning_rate"),
            metrics=metrics
        ))
        self.updated_at = datetime.now()
    
    def save_checkpoint(self, step: int, metrics: Dict[str, float]) -> Checkpoint:
        """
        保存Checkpoint
        
        这是创建Checkpoint的唯一入口
        """
        if self.status != TrainingStatus.RUNNING:
            raise ValueError("只有运行中的任务才能保存Checkpoint")
        
        checkpoint = Checkpoint(
            training_task_id=self.id,
            step=step,
            path=f"/checkpoints/{self.id}/step_{step}",
            global_step=step,
            loss=metrics.get("loss"),
            learning_rate=metrics.get("learning_rate"),
            metrics=metrics
        )
        self._checkpoints.append(checkpoint)
        return checkpoint
    
    def get_latest_checkpoint(self) -> Optional[Checkpoint]:
        """获取最新Checkpoint"""
        if not self._checkpoints:
            return None
        return max(self._checkpoints, key=lambda c: c.step)
    
    def _validate_transition(self, new_status: TrainingStatus) -> None:
        """验证状态转换是否合法"""
        allowed = self._allowed_transitions.get(self.status, [])
        if new_status not in allowed:
            raise ValueError(
                f"不允许的状态转换: {self.status} -> {new_status}"
            )

五、领域事件设计

5.1 领域事件概述

领域事件(Domain Event)是DDD中非常重要的概念,它描述了领域中发生的、对业务有意义的事件。通过领域事件,不同限界上下文可以解耦地通信,实现最终一致性。

┌─────────────────────────────────────────────────────────────────────────┐
│                         领域事件流转架构                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────────────┐                                                  │
│  │  模型管理上下文    │                                                  │
│  │                   │                                                  │
│  │  DomainEvent      │                                                  │
│  │  ├─ ModelRegisted │                                                  │
│  │  └─ ModelValidated│                                                  │
│  └────────┬─────────┘                                                  │
│           │ publish                                                    │
│           ▼                                                             │
│  ┌──────────────────────────────────────────────────────────────────┐ │
│  │                      领域事件总线 (Event Bus)                       │ │
│  │  ┌─────────────────────────────────────────────────────────────┐  │ │
│  │  │  Event Store (持久化)                                        │  │ │
│  │  │  - 所有发布的事件都会被记录                                  │  │ │
│  │  │  - 支持事件回放和重试                                        │  │ │
│  │  └─────────────────────────────────────────────────────────────┘  │ │
│  └──────────────────────────────────────────────────────────────────┘ │
│           │ dispatch                                                   │
│           ▼                                                             │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐       │
│  │  训练管理上下文    │  │  推理服务上下文    │  │  通知上下文      │       │
│  │                   │  │                   │  │                  │       │
│  │  EventHandler     │  │  EventHandler     │  │  EventHandler   │       │
│  │  └─ 启动训练任务   │  │  └─ 更新模型列表   │  │  └─ 发送通知    │       │
│  └──────────────────┘  └──────────────────┘  └──────────────────┘       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

5.2 领域事件实现

# domain/events.py
# 领域事件定义和发布机制
from __future__ import annotations
import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, Any, Optional, Callable, List
from abc import ABC, abstractmethod
import logging

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class DomainEvent:
    """
    领域事件基类
    
    所有领域事件都应该:
    1. 继承此基类
    2. 是不可变的(frozen=True)
    3. 包含事件发生的时间戳
    4. 包含事件的唯一标识
    """
    event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    occurred_on: datetime = field(default_factory=datetime.now)
    tenant_id: str = ""
    
    @property
    @abstractmethod
    def event_type(self) -> str:
        """事件类型,必须在子类中定义"""
        pass
    
    def to_dict(self) -> Dict[str, Any]:
        """序列化为字典"""
        return {
            'event_id': self.event_id,
            'event_type': self.event_type,
            'occurred_on': self.occurred_on.isoformat(),
            'tenant_id': self.tenant_id,
            'data': self._extract_data()
        }
    
    @abstractmethod
    def _extract_data(self) -> Dict[str, Any]:
        """提取事件数据"""
        pass


# ==================== 模型管理领域事件 ====================

@dataclass(frozen=True)
class ModelRegistered(DomainEvent):
    """
    模型注册事件
    
    当新模型被注册到平台时触发
    """
    model_id: str = ""
    model_name: str = ""
    base_model: str = ""
    created_by: str = ""
    
    @property
    def event_type(self) -> str:
        return "model.registered"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'model_id': self.model_id,
            'model_name': self.model_name,
            'base_model': self.base_model,
            'created_by': self.created_by
        }


@dataclass(frozen=True)
class ModelVersionRegistered(DomainEvent):
    """
    模型版本注册事件
    
    当新版本被添加到模型时触发
    """
    model_id: str = ""
    version_id: str = ""
    version: str = ""
    parameter_count: int = 0
    size_bytes: int = 0
    
    @property
    def event_type(self) -> str:
        return "model.version.registered"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'model_id': self.model_id,
            'version_id': self.version_id,
            'version': self.version,
            'parameter_count': self.parameter_count,
            'size_bytes': self.size_bytes
        }


@dataclass(frozen=True)
class ModelValidated(DomainEvent):
    """模型验证完成事件"""
    model_id: str = ""
    version_id: str = ""
    validation_passed: bool = False
    validation_errors: List[str] = field(default_factory=list)
    
    @property
    def event_type(self) -> str:
        return "model.validated"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'model_id': self.model_id,
            'version_id': self.version_id,
            'validation_passed': self.validation_passed,
            'validation_errors': self.validation_errors
        }


@dataclass(frozen=True)
class EvaluationCompleted(DomainEvent):
    """评测完成事件"""
    evaluation_id: str = ""
    model_id: str = ""
    model_version_id: str = ""
    benchmark_id: str = ""
    metrics: Dict[str, float] = field(default_factory=dict)
    passed: bool = True
    
    @property
    def event_type(self) -> str:
        return "evaluation.completed"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'evaluation_id': self.evaluation_id,
            'model_id': self.model_id,
            'model_version_id': self.model_version_id,
            'benchmark_id': self.benchmark_id,
            'metrics': self.metrics,
            'passed': self.passed
        }


# ==================== 训练管理领域事件 ====================

@dataclass(frozen=True)
class TrainingStarted(DomainEvent):
    """训练任务启动事件"""
    training_task_id: str = ""
    model_id: str = ""
    dataset_id: str = ""
    resource_spec: Dict[str, Any] = field(default_factory=dict)
    
    @property
    def event_type(self) -> str:
        return "training.started"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'training_task_id': self.training_task_id,
            'model_id': self.model_id,
            'dataset_id': self.dataset_id,
            'resource_spec': self.resource_spec
        }


@dataclass(frozen=True)
class TrainingCompleted(DomainEvent):
    """
    训练完成事件
    
    这是最重要的领域事件之一,会触发多个下游操作:
    1. 模型注册
    2. 触发评测
    3. 发送通知
    """
    training_task_id: str = ""
    model_id: str = ""
    output_model_version: str = ""
    final_metrics: Dict[str, float] = field(default_factory=dict)
    checkpoint_path: str = ""
    duration_seconds: int = 0
    
    @property
    def event_type(self) -> str:
        return "training.completed"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'training_task_id': self.training_task_id,
            'model_id': self.model_id,
            'output_model_version': self.output_model_version,
            'final_metrics': self.final_metrics,
            'checkpoint_path': self.checkpoint_path,
            'duration_seconds': self.duration_seconds
        }


@dataclass(frozen=True)
class TrainingFailed(DomainEvent):
    """训练失败事件"""
    training_task_id: str = ""
    model_id: str = ""
    error_message: str = ""
    failed_at_step: int = 0
    
    @property
    def event_type(self) -> str:
        return "training.failed"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'training_task_id': self.training_task_id,
            'model_id': self.model_id,
            'error_message': self.error_message,
            'failed_at_step': self.failed_at_step
        }


@dataclass(frozen=True)
class CheckpointCreated(DomainEvent):
    """Checkpoint创建事件"""
    training_task_id: str = ""
    checkpoint_id: str = ""
    step: int = 0
    path: str = ""
    size_bytes: int = 0
    
    @property
    def event_type(self) -> str:
        return "training.checkpoint.created"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'training_task_id': self.training_task_id,
            'checkpoint_id': self.checkpoint_id,
            'step': self.step,
            'path': self.path,
            'size_bytes': self.size_bytes
        }


# ==================== 推理服务领域事件 ====================

@dataclass(frozen=True)
class DeployPromoted(DomainEvent):
    """
    模型部署事件
    
    当模型被部署到生产环境时触发
    """
    model_id: str = ""
    version_id: str = ""
    deployment_id: str = ""
    endpoint: str = ""
    replica_count: int = 1
    
    @property
    def event_type(self) -> str:
        return "inference.deployment.promoted"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'model_id': self.model_id,
            'version_id': self.version_id,
            'deployment_id': self.deployment_id,
            'endpoint': self.endpoint,
            'replica_count': self.replica_count
        }


@dataclass(frozen=True)
class InferenceAnomalyDetected(DomainEvent):
    """推理异常检测事件"""
    deployment_id: str = ""
    anomaly_type: str = ""  # high_latency, high_error_rate, etc.
    metric_value: float = 0.0
    threshold: float = 0.0
    
    @property
    def event_type(self) -> str:
        return "inference.anomaly.detected"
    
    def _extract_data(self) -> Dict[str, Any]:
        return {
            'deployment_id': self.deployment_id,
            'anomaly_type': self.anomaly_type,
            'metric_value': self.metric_value,
            'threshold': self.threshold
        }


# ==================== 事件发布器 ====================

class EventPublisher:
    """
    领域事件发布器
    
    负责:
    1. 发布事件到事件总线
    2. 持久化事件到事件存储
    3. 处理发布失败的情况
    """
    
    def __init__(self):
        self._handlers: Dict[str, List[Callable]] = {}
        self._event_store: List[DomainEvent] = []
    
    def subscribe(self, event_type: str, handler: Callable) -> None:
        """订阅事件"""
        if event_type not in self._handlers:
            self._handlers[event_type] = []
        self._handlers[event_type].append(handler)
        logger.info(f"订阅事件: {event_type}")
    
    def unsubscribe(self, event_type: str, handler: Callable) -> None:
        """取消订阅"""
        if event_type in self._handlers:
            self._handlers[event_type].remove(handler)
    
    def publish(self, event: DomainEvent) -> None:
        """
        发布事件
        
        1. 持久化到事件存储
        2. 同步调用所有处理器
        3. 处理失败情况
        """
        event_type = event.event_type
        
        # 1. 持久化事件
        self._event_store.append(event)
        logger.info(f"发布领域事件: {event_type} [id={event.event_id}]")
        
        # 2. 调用处理器
        if event_type in self._handlers:
            for handler in self._handlers[event_type]:
                try:
                    handler(event)
                except Exception as e:
                    logger.error(
                        f"事件处理器执行失败: {event_type}, "
                        f"handler={handler.__name__}, error={e}"
                    )
                    # TODO: 实现重试机制
    
    def get_events(
        self, 
        event_type: Optional[str] = None,
        since: Optional[datetime] = None,
        limit: int = 100
    ) -> List[DomainEvent]:
        """查询历史事件"""
        events = self._event_store
        
        if event_type:
            events = [e for e in events if e.event_type == event_type]
        
        if since:
            events = [e for e in events if e.occurred_on >= since]
        
        return events[-limit:]


# 全局事件发布器实例
event_publisher = EventPublisher()

5.3 事件处理器实现

# domain/event_handlers.py
# 领域事件处理器实现
from domain.events import (
    event_publisher,
    TrainingCompleted,
    ModelRegistered,
    EvaluationCompleted,
    DeployPromoted,
)
import logging

logger = logging.getLogger(__name__)


# ==================== 训练完成事件处理器 ====================

def handle_training_completed_training(event: TrainingCompleted) -> None:
    """
    训练完成 -> 启动模型注册流程
    
    当训练完成时:
    1. 创建新的模型版本记录
    2. 触发模型验证
    """
    logger.info(
        f"处理训练完成事件: task_id={event.training_task_id}, "
        f"output={event.output_model_version}"
    )
    
    # TODO: 实现模型注册逻辑
    # model_version = ModelVersion(
    #     model_id=event.model_id,
    #     version=event.output_model_version,
    #     checkpoint_path=event.checkpoint_path,
    #     metrics=event.final_metrics
    # )
    # model_repository.save(model_version)
    # event_publisher.publish(ModelVersionRegistered(...))
    
    logger.info(f"模型注册流程已启动: {event.output_model_version}")


def handle_training_completed_notification(event: TrainingCompleted) -> None:
    """
    训练完成 -> 发送通知
    
    当训练完成时通知相关人员
    """
    logger.info(f"发送训练完成通知: task_id={event.training_task_id}")
    
    # TODO: 实现通知逻辑
    # notification_service.send(
    #     recipient=training_task.created_by,
    #     title="训练任务完成",
    #     content=f"任务 {training_task.name} 已完成,最终指标: {event.final_metrics}"
    # )


def handle_training_completed_evaluation(event: TrainingCompleted) -> None:
    """
    训练完成 -> 触发自动评测
    
    当训练完成时,自动触发模型评测
    """
    logger.info(f"触发自动评测: model={event.output_model_version}")
    
    # TODO: 实现评测触发逻辑
    # evaluation_service.create_task(
    #     model_version_id=model_version.id,
    #     benchmark_id="default",
    #     task_type=EvaluationTask.Type.AUTOMATIC
    # )


# ==================== 模型注册事件处理器 ====================

def handle_model_registered_inference(event: ModelRegistered) -> None:
    """
    模型注册 -> 更新推理服务模型列表
    
    当新模型注册时,更新推理服务的可用模型列表
    """
    logger.info(f"更新推理服务模型列表: model={event.model_name}")
    
    # TODO: 实现推理服务更新逻辑
    # inference_service.refresh_available_models()


# ==================== 部署事件处理器 ====================

def handle_deploy_promoted_monitoring(event: DeployPromoted) -> None:
    """
    模型部署 -> 启动监控
    
    当模型部署时,设置监控告警
    """
    logger.info(f"启动模型监控: deployment_id={event.deployment_id}")
    
    # TODO: 实现监控配置逻辑
    # monitoring_service.setup_deployment_monitoring(
    #     deployment_id=event.deployment_id,
    #     endpoint=event.endpoint
    # )


def handle_deploy_promoted_notification(event: DeployPromoted) -> None:
    """
    模型部署 -> 发送通知
    """
    logger.info(f"发送部署完成通知: deployment_id={event.deployment_id}")
    
    # TODO: 实现通知逻辑


# ==================== 事件订阅初始化 ====================

def initialize_event_handlers() -> None:
    """
    初始化事件处理器订阅
    
    在应用启动时调用此函数注册所有事件处理器
    """
    # 训练完成事件 -> 多个处理器
    event_publisher.subscribe(
        "training.completed",
        handle_training_completed_training
    )
    event_publisher.subscribe(
        "training.completed",
        handle_training_completed_notification
    )
    event_publisher.subscribe(
        "training.completed",
        handle_training_completed_evaluation
    )
    
    # 模型注册事件
    event_publisher.subscribe(
        "model.registered",
        handle_model_registered_inference
    )
    
    # 部署完成事件
    event_publisher.subscribe(
        "inference.deployment.promoted",
        handle_deploy_promoted_monitoring
    )
    event_publisher.subscribe(
        "inference.deployment.promoted",
        handle_deploy_promoted_notification
    )
    
    logger.info("领域事件处理器初始化完成")

六、应用服务层设计

6.1 应用服务架构

应用服务层是DDD中连接领域模型和外部世界的桥梁,它负责协调多个领域对象完成业务操作,但不包含业务逻辑。

┌─────────────────────────────────────────────────────────────────────────┐
│                         应用服务层架构                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                      应用服务层 (Application Services)           │   │
│  │                                                                     │   │
│  │  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐ │   │
│  │  │ ModelAppService │  │TrainingAppService│ │InferenceAppSvc │ │   │
│  │  │                 │  │                  │ │                 │ │   │
│  │  │ - register()   │  │ - create_task()  │ │ - deploy()     │ │   │
│  │  │ - evaluate()   │  │ - start()        │ │ - predict()    │ │   │
│  │  │ - deprecate()  │  │ - cancel()       │ │ - rollback()   │ │   │
│  │  └────────┬────────┘  └────────┬─────────┘  └────────┬────────┘ │   │
│  │           │                    │                    │           │   │
│  │           ▼                    ▼                    ▼           │   │
│  │  ┌─────────────────────────────────────────────────────────┐   │   │
│  │  │              领域服务层 (Domain Services)                  │   │   │
│  │  │  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐ │   │   │
│  │  │  │ModelValidator │  │TrainingScheduler│ │InferenceRouter│ │   │   │
│  │  │  └───────────────┘  └───────────────┘  └───────────────┘ │   │   │
│  │  └─────────────────────────────────────────────────────────┘   │   │
│  │                               │                                  │   │
│  │                               ▼                                  │   │
│  │  ┌─────────────────────────────────────────────────────────┐   │   │
│  │  │              基础设施层 (Infrastructure)                  │   │   │
│  │  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐      │   │   │
│  │  │  │Repository   │  │EventPublisher│ │K8sClient    │      │   │   │
│  │  │  │(MySQL/Redis)│  │              │ │             │      │   │   │
│  │  │  └─────────────┘  └─────────────┘  └─────────────┘      │   │   │
│  │  └─────────────────────────────────────────────────────────┘   │   │
│  │                                                                     │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

6.2 应用服务实现

# application/training_service.py
# 训练管理应用服务
from typing import Optional, List, Dict, Any
from dataclasses import dataclass
import logging

from domain.models.training import (
    TrainingTask,
    TrainingStatus,
    TrainingHyperparameters,
    GPUResourceSpec,
    Checkpoint,
)
from domain.events import (
    event_publisher,
    TrainingStarted,
    TrainingCompleted,
    TrainingFailed,
    CheckpointCreated,
)
from infrastructure.repositories import TrainingTaskRepository
from infrastructure.k8s import KubernetesClient

logger = logging.getLogger(__name__)


@dataclass
class CreateTrainingTaskCommand:
    """创建训练任务的命令"""
    tenant_id: str
    name: str
    description: str
    dataset_id: str
    base_model_id: str
    output_model_name: str
    hyperparameters: Dict[str, Any]
    gpu_count: int
    gpu_type: str
    created_by: str


@dataclass
class TrainingTaskDTO:
    """训练任务数据传输对象"""
    id: str
    tenant_id: str
    name: str
    status: str
    progress: float
    created_at: str
    started_at: Optional[str]
    completed_at: Optional[str]
    final_metrics: Optional[Dict[str, float]]


class TrainingApplicationService:
    """
    训练管理应用服务
    
    负责协调训练任务的创建、启动、监控等操作
    """
    
    def __init__(
        self,
        task_repository: TrainingTaskRepository,
        k8s_client: KubernetesClient,
    ):
        self.task_repository = task_repository
        self.k8s_client = k8s_client
    
    def create_task(self, command: CreateTrainingTaskCommand) -> TrainingTaskDTO:
        """
        创建训练任务
        
        流程:
        1. 验证输入参数
        2. 创建领域模型
        3. 持久化到数据库
        """
        # 1. 验证数据集和模型是否存在
        # dataset = self.dataset_repository.find_by_id(command.dataset_id)
        # if not dataset:
        #     raise ValueError(f"数据集不存在: {command.dataset_id}")
        
        # 2. 创建领域模型
        hyperparameters = TrainingHyperparameters(
            learning_rate=command.hyperparameters.get('learning_rate', 1e-5),
            batch_size=command.hyperparameters.get('batch_size', 8),
            epochs=command.hyperparameters.get('epochs', 3),
            max_seq_length=command.hyperparameters.get('max_seq_length', 2048),
            warmup_steps=command.hyperparameters.get('warmup_steps', 100),
            weight_decay=command.hyperparameters.get('weight_decay', 0.01),
        )
        
        resource_spec = GPUResourceSpec(
            gpu_count=command.gpu_count,
            gpu_type=command.gpu_type,
            memory_per_gpu_gb=80 if command.gpu_type == 'A100' else 32
        )
        
        task = TrainingTask(
            tenant_id=command.tenant_id,
            name=command.name,
            description=command.description,
            dataset_id=command.dataset_id,
            base_model_id=command.base_model_id,
            output_model_name=command.output_model_name,
            hyperparameters=hyperparameters,
            resource_spec=resource_spec,
            created_by=command.created_by,
        )
        
        # 3. 持久化
        self.task_repository.save(task)
        
        logger.info(f"训练任务创建成功: {task.id}")
        
        return self._to_dto(task)
    
    def start_task(self, task_id: str) -> TrainingTaskDTO:
        """
        启动训练任务
        
        流程:
        1. 获取任务
        2. 验证状态
        3. 提交到K8s
        4. 更新状态
        5. 发布事件
        """
        task = self.task_repository.find_by_id(task_id)
        if not task:
            raise ValueError(f"任务不存在: {task_id}")
        
        # 验证状态
        if task.status != TrainingStatus.PENDING:
            raise ValueError(f"任务状态不允许启动: {task.status}")
        
        # 提交到Kubernetes
        job_name = self._submit_to_kubernetes(task)
        
        # 更新任务状态
        task.kubernetes_job_name = job_name
        task.start()
        self.task_repository.save(task)
        
        # 发布领域事件
        event_publisher.publish(TrainingStarted(
            tenant_id=task.tenant_id,
            training_task_id=task.id,
            model_id=task.base_model_id,
            dataset_id=task.dataset_id,
            resource_spec={
                'gpu_count': task.resource_spec.gpu_count,
                'gpu_type': task.resource_spec.gpu_type,
            } if task.resource_spec else {}
        ))
        
        logger.info(f"训练任务已启动: {task_id}, job={job_name}")
        
        return self._to_dto(task)
    
    def handle_task_completed(
        self, 
        task_id: str, 
        final_metrics: Dict[str, float]
    ) -> None:
        """
        处理任务完成回调
        
        由Kubernetes Operator或定时任务调用
        """
        task = self.task_repository.find_by_id(task_id)
        if not task:
            logger.error(f"任务不存在: {task_id}")
            return
        
        # 更新任务状态
        task.complete(final_metrics)
        self.task_repository.save(task)
        
        # 发布完成事件
        event_publisher.publish(TrainingCompleted(
            tenant_id=task.tenant_id,
            training_task_id=task.id,
            model_id=task.base_model_id,
            output_model_version=task.output_model_name,
            final_metrics=final_metrics,
            checkpoint_path=task.get_latest_checkpoint().path if task.get_latest_checkpoint() else "",
            duration_seconds=int(
                (task.completed_at - task.started_at).total_seconds()
            ) if task.started_at and task.completed_at else 0
        ))
        
        logger.info(f"训练任务完成处理: {task_id}")
    
    def cancel_task(self, task_id: str) -> TrainingTaskDTO:
        """取消训练任务"""
        task = self.task_repository.find_by_id(task_id)
        if not task:
            raise ValueError(f"任务不存在: {task_id}")
        
        # 取消K8s Job
        if task.kubernetes_job_name:
            self.k8s_client.delete_job(
                namespace=task.kubernetes_namespace or task.tenant_id,
                job_name=task.kubernetes_job_name
            )
        
        # 更新状态
        task.cancel()
        self.task_repository.save(task)
        
        logger.info(f"训练任务已取消: {task_id}")
        
        return self._to_dto(task)
    
    def get_task(self, task_id: str) -> Optional[TrainingTaskDTO]:
        """获取任务详情"""
        task = self.task_repository.find_by_id(task_id)
        return self._to_dto(task) if task else None
    
    def list_tasks(
        self, 
        tenant_id: str, 
        status: Optional[str] = None,
        limit: int = 20,
        offset: int = 0
    ) -> List[TrainingTaskDTO]:
        """查询任务列表"""
        tasks = self.task_repository.find_by_tenant(
            tenant_id=tenant_id,
            status=status,
            limit=limit,
            offset=offset
        )
        return [self._to_dto(task) for task in tasks]
    
    def _submit_to_kubernetes(self, task: TrainingTask) -> str:
        """提交训练任务到Kubernetes"""
        job_manifest = {
            'apiVersion': 'batch/v1',
            'kind': 'Job',
            'metadata': {
                'name': f'training-{task.id[:8]}',
                'namespace': task.tenant_id
            },
            'spec': {
                'backoffLimit': 3,
                'template': {
                    'spec': {
                        'restartPolicy': 'OnFailure',
                        'containers': [{
                            'name': 'training',
                            'image': 'your-registry.com/training:latest',
                            'env': [
                                {'name': 'TASK_ID', 'value': task.id},
                                {'name': 'DATASET_ID', 'value': task.dataset_id},
                                {'name': 'BASE_MODEL_ID', 'value': task.base_model_id},
                            ],
                            'resources': {
                                'limits': {
                                    'nvidia.com/gpu': str(
                                        task.resource_spec.gpu_count
                                    ) if task.resource_spec else '1'
                                }
                            }
                        }]
                    }
                }
            }
        }
        
        # 使用K8s Client提交
        # result = self.k8s_client.create_job(job_manifest)
        # return result['metadata']['name']
        
        # 模拟返回
        return f'training-{task.id[:8]}'
    
    def _to_dto(self, task: TrainingTask) -> TrainingTaskDTO:
        """领域模型转DTO"""
        return TrainingTaskDTO(
            id=task.id,
            tenant_id=task.tenant_id,
            name=task.name,
            status=task.status.value,
            progress=task.progress,
            created_at=task.created_at.isoformat(),
            started_at=task.started_at.isoformat() if task.started_at else None,
            completed_at=task.completed_at.isoformat() if task.completed_at else None,
            final_metrics=task.final_metrics
        )

七、CQRS在查询场景的应用

7.1 为什么AI平台需要CQRS

在AI平台中,命令操作(写)和查询操作(读)有明显的特点差异:

  • 命令操作:模型注册、训练任务创建、部署等,写入频率相对较低,但一致性要求高

  • 查询操作:模型列表、训练历史、监控指标等,读取频率很高,需要快速响应

CQRS(Command Query Responsibility Segregation)将命令和查询分离,使用不同的数据模型和数据存储来优化各自的场景。

┌─────────────────────────────────────────────────────────────────────────┐
│                         CQRS 架构                                       │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                          命令端 (Command)                         │   │
│  │                                                                     │   │
│  │  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐          │   │
│  │  │ Model       │    │ Training    │    │ Inference   │          │   │
│  │  │ Command     │    │ Command     │    │ Command     │          │   │
│  │  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘          │   │
│  │         │                  │                  │                 │   │
│  │         ▼                  ▼                  ▼                 │   │
│  │  ┌─────────────────────────────────────────────────────────┐   │   │
│  │  │                    领域模型 (写入模型)                      │   │   │
│  │  │  - 聚合根管理业务规则                                       │   │   │
│  │  │  - 丰富的领域逻辑                                          │   │   │
│  │  │  - 强一致性                                                │   │   │
│  │  └─────────────────────────────┬───────────────────────────┘   │   │
│  │                                │                                 │   │
│  │                    write       │                                  │   │
│  │                                ▼                                  │   │
│  │  ┌─────────────────────────────────────────────────────────┐   │   │
│  │  │                 MySQL (写库)                              │   │   │
│  │  │  - 范式化设计                                              │   │   │
│  │  │  - 支持事务                                                │   │   │
│  │  │  - 复杂查询支持                                            │   │   │
│  │  └─────────────────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
│  │  event │                                                           │
│  │        │ 同步/异步                                                  │
│  │        ▼                                                           │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                          查询端 (Query)                           │   │
│  │                                                                     │   │
│  │  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐          │   │
│  │  │ Model       │    │ Training    │    │ Dashboard   │          │   │
│  │  │ Query       │    │ Query       │    │ Query       │          │   │
│  │  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘          │   │
│  │         │                  │                  │                 │   │
│  │         ▼                  ▼                  ▼                 │   │
│  │  ┌─────────────────────────────────────────────────────────┐   │   │
│  │  │                    Read Model (读取模型)                   │   │   │
│  │  │  - 反范式化设计                                            │   │   │
│  │  │  - 针对查询优化                                            │   │   │
│  │  │  - 可用性优先                                              │   │   │
│  │  └─────────────────────────────┬───────────────────────────┘   │   │
│  │                                │                                 │   │
│  │  ┌─────────────────────────────────────────────────────────┐   │   │
│  │  │                 Elasticsearch/Redis (读库)                 │   │   │
│  │  │  - 全文搜索支持                                            │   │   │
│  │  │  - 高速缓存                                                │   │   │
│  │  └─────────────────────────────────────────────────────────┘   │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

7.2 读写分离的查询服务实现

# application/query_service.py
# CQRS 查询端实现
from typing import List, Optional, Dict, Any
from dataclasses import dataclass
from datetime import datetime
import logging

from infrastructure.query_db import QueryDatabase

logger = logging.getLogger(__name__)


@dataclass
class ModelListItem:
    """模型列表项(优化过的读取模型)"""
    model_id: str
    tenant_id: str
    name: str
    description: str
    latest_version: str
    status: str
    base_model: str
    tags: List[str]
    created_at: datetime
    updated_at: datetime
    
    # 预聚合的统计信息
    version_count: int
    total_size_bytes: int
    evaluation_count: int
    avg_score: Optional[float]
    
    # 用于列表展示的简短信息
    short_description: str
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'ModelListItem':
        """从字典创建"""
        return cls(
            model_id=data['model_id'],
            tenant_id=data['tenant_id'],
            name=data['name'],
            description=data['description'],
            latest_version=data['latest_version'],
            status=data['status'],
            base_model=data['base_model'],
            tags=data.get('tags', []),
            created_at=data['created_at'],
            updated_at=data['updated_at'],
            version_count=data.get('version_count', 0),
            total_size_bytes=data.get('total_size_bytes', 0),
            evaluation_count=data.get('evaluation_count', 0),
            avg_score=data.get('avg_score'),
            short_description=data['description'][:100] + '...' if len(data['description']) > 100 else data['description']
        )


@dataclass
class TrainingTaskSummary:
    """训练任务汇总(读取模型)"""
    task_id: str
    tenant_id: str
    name: str
    status: str
    progress: float
    created_at: datetime
    
    # 关联信息
    dataset_name: str
    base_model_name: str
    
    # 时间信息
    duration_seconds: Optional[int]
    started_at: Optional[datetime]
    completed_at: Optional[datetime]
    
    # 关键指标
    current_step: int
    loss: Optional[float]
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'TrainingTaskSummary':
        started = data.get('started_at')
        completed = data.get('completed_at')
        duration = None
        if started and completed:
            duration = int((completed - started).total_seconds())
        
        return cls(
            task_id=data['task_id'],
            tenant_id=data['tenant_id'],
            name=data['name'],
            status=data['status'],
            progress=data.get('progress', 0.0),
            created_at=data['created_at'],
            dataset_name=data.get('dataset_name', ''),
            base_model_name=data.get('base_model_name', ''),
            duration_seconds=duration,
            started_at=started,
            completed_at=completed,
            current_step=data.get('current_step', 0),
            loss=data.get('loss')
        )


class ModelQueryService:
    """
    模型查询服务(Read Side)
    
    负责处理所有模型相关的查询请求
    使用专门的读取数据库优化查询性能
    """
    
    def __init__(self, query_db: QueryDatabase):
        self.query_db = query_db
    
    def search_models(
        self,
        tenant_id: str,
        keyword: Optional[str] = None,
        tags: Optional[List[str]] = None,
        status: Optional[str] = None,
        sort_by: str = 'updated_at',
        sort_order: str = 'desc',
        limit: int = 20,
        offset: int = 0
    ) -> List[ModelListItem]:
        """
        搜索模型列表
        
        支持:
        - 关键词全文搜索
        - 标签过滤
        - 状态过滤
        - 排序
        - 分页
        """
        query = f"""
            SELECT 
                m.model_id,
                m.tenant_id,
                m.name,
                m.description,
                m.latest_version,
                m.status,
                m.base_model,
                m.tags,
                m.created_at,
                m.updated_at,
                COUNT(DISTINCT mv.version_id) as version_count,
                SUM(mv.size_bytes) as total_size_bytes,
                COUNT(DISTINCT e.evaluation_id) as evaluation_count,
                AVG(e.score) as avg_score
            FROM models m
            LEFT JOIN model_versions mv ON m.model_id = mv.model_id
            LEFT JOIN evaluations e ON m.model_id = e.model_id
            WHERE m.tenant_id = %s
        """
        params = [tenant_id]
        
        # 关键词搜索
        if keyword:
            query += " AND (m.name LIKE %s OR m.description LIKE %s)"
            params.extend([f'%{keyword}%', f'%{keyword}%'])
        
        # 标签过滤
        if tags:
            # 使用JSON数组包含检查
            for tag in tags:
                query += " AND m.tags @> %s"
                params.append(f'["{tag}"]')
        
        # 状态过滤
        if status:
            query += " AND m.status = %s"
            params.append(status)
        
        # 分组和排序
        query += f"""
            GROUP BY m.model_id
            ORDER BY m.{sort_by} {sort_order}
            LIMIT %s OFFSET %s
        """
        params.extend([limit, offset])
        
        results = self.query_db.execute(query, params)
        
        return [ModelListItem.from_dict(row) for row in results]
    
    def get_model_detail(self, model_id: str) -> Optional[Dict[str, Any]]:
        """
        获取模型详情
        
        返回包含完整信息的读取模型
        """
        # 预编译的查询,一次性获取所有需要的信息
        query = """
            SELECT 
                m.*,
                -- 版本列表(JSON聚合)
                COALESCE(JSON_AGG(
                    JSON_BUILD_OBJECT(
                        'version_id', mv.version_id,
                        'version', mv.version,
                        'status', mv.status,
                        'parameter_count', mv.parameter_count,
                        'size_bytes', mv.size_bytes,
                        'created_at', mv.created_at
                    )
                ) FILTER (WHERE mv.version_id IS NOT NULL), '[]') as versions,
                
                -- 最新版本的指标
                (
                    SELECT JSON_BUILD_OBJECT(
                        'bleu_score', mv2.bleu_score,
                        'rouge_score', mv2.rouge_score,
                        'accuracy', mv2.accuracy
                    )
                    FROM model_versions mv2
                    WHERE mv2.model_id = m.model_id
                    ORDER BY mv2.created_at DESC
                    LIMIT 1
                ) as latest_metrics,
                
                -- 训练历史(最近10条)
                (
                    SELECT COALESCE(JSON_AGG(t.*), '[]')
                    FROM (
                        SELECT 
                            tt.task_id,
                            tt.name,
                            tt.status,
                            tt.progress,
                            tt.created_at,
                            tt.started_at,
                            tt.completed_at
                        FROM training_tasks tt
                        WHERE tt.base_model_id = m.model_id
                        ORDER BY tt.created_at DESC
                        LIMIT 10
                    ) t
                ) as training_history
                
            FROM models m
            WHERE m.model_id = %s
            GROUP BY m.model_id
        """
        
        results = self.query_db.execute(query, [model_id])
        
        if not results:
            return None
        
        return results[0]
    
    def get_model_versions(self, model_id: str) -> List[Dict[str, Any]]:
        """获取模型版本列表"""
        query = """
            SELECT 
                mv.*,
                e.metrics,
                e.status as eval_status
            FROM model_versions mv
            LEFT JOIN evaluations e ON mv.version_id = e.model_version_id
            WHERE mv.model_id = %s
            ORDER BY mv.created_at DESC
        """
        
        return self.query_db.execute(query, [model_id])


class DashboardQueryService:
    """
    仪表盘查询服务
    
    提供聚合后的统计数据,用于仪表盘展示
    """
    
    def __init__(self, query_db: QueryDatabase):
        self.query_db = query_db
    
    def get_tenant_overview(self, tenant_id: str) -> Dict[str, Any]:
        """
        获取租户概览统计
        
        返回聚合后的统计数据
        """
        query = """
            SELECT 
                -- 模型统计
                COUNT(DISTINCT m.model_id) as total_models,
                COUNT(DISTINCT CASE WHEN m.status = 'deployed' THEN m.model_id END) as deployed_models,
                
                -- 训练统计
                COUNT(DISTINCT tt.task_id) as total_training_tasks,
                COUNT(DISTINCT CASE WHEN tt.status = 'running' THEN tt.task_id END) as running_tasks,
                SUM(CASE WHEN tt.status = 'succeeded' THEN 1 ELSE 0 END) as completed_tasks,
                
                -- 资源使用
                SUM(CASE WHEN tt.status = 'running' THEN tt.requested_gpu_hours ELSE 0 END) as current_gpu_usage,
                
                -- 最近7天活跃度
                COUNT(DISTINCT CASE WHEN tt.created_at >= NOW() - INTERVAL '7 days' 
                    THEN tt.task_id END) as tasks_last_7_days
                
            FROM models m
            CROSS JOIN training_tasks tt
            WHERE m.tenant_id = %s AND tt.tenant_id = %s
        """
        
        results = self.query_db.execute(query, [tenant_id, tenant_id])
        
        return results[0] if results else {}
    
    def get_training_trend(
        self, 
        tenant_id: str, 
        days: int = 30
    ) -> List[Dict[str, Any]]:
        """
        获取训练趋势数据
        
        按天聚合训练任务数量和成功率
        """
        query = """
            SELECT 
                DATE_TRUNC('day', created_at) as date,
                COUNT(*) as total_tasks,
                COUNT(CASE WHEN status = 'succeeded' THEN 1 END) as succeeded,
                COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed,
                AVG(progress) as avg_progress
            FROM training_tasks
            WHERE tenant_id = %s
                AND created_at >= NOW() - INTERVAL '%s days'
            GROUP BY DATE_TRUNC('day', created_at)
            ORDER BY date
        """
        
        return self.query_db.execute(query, [tenant_id, days])

八、项目结构

8.1 DDD项目结构设计

以下是完整的Django DDD项目结构:

ai_platform/
├── domain/                          # 领域层 - 核心业务逻辑
│   ├── __init__.py
│   ├── models/                      # 领域模型
│   │   ├── __init__.py
│   │   ├── model_management.py      # 模型管理领域模型
│   │   ├── training.py             # 训练管理领域模型
│   │   ├── inference.py            # 推理服务领域模型
│   │   └── data.py                 # 数据管理领域模型
│   ├── events.py                    # 领域事件定义
│   ├── repositories.py              # 仓储接口(抽象)
│   └── services.py                  # 领域服务
│
├── application/                     # 应用层 - 用例编排
│   ├── __init__.py
│   ├── commands/                    # 命令(写操作)
│   │   ├── __init__.py
│   │   ├── training_commands.py
│   │   └── model_commands.py
│   ├── queries/                     # 查询(读操作)
│   │   ├── __init__.py
│   │   ├── training_queries.py
│   │   └── model_queries.py
│   └── services/                    # 应用服务
│       ├── __init__.py
│       ├── training_service.py
│       └── model_service.py
│
├── infrastructure/                   # 基础设施层
│   ├── __init__.py
│   ├── repositories/               # 仓储实现
│   │   ├── __init__.py
│   │   ├── mysql_repository.py
│   │   └── elasticsearch_repository.py
│   ├── k8s/                        # Kubernetes集成
│   │   ├── __init__.py
│   │   └── client.py
│   ├── messaging/                  # 消息队列
│   │   ├── __init__.py
│   │   └── event_bus.py
│   └── query_db.py                 # 查询数据库
│
├── api/                             # 接口层
│   ├── __init__.py
│   ├── views/                     # Django视图
│   │   ├── __init__.py
│   │   ├── training_views.py
│   │   └── model_views.py
│   ├── serializers.py              # 序列化器
│   └── urls.py                     # 路由
│
├── core/                            # 核心模块
│   ├── __init__.py
│   ├── aggregates.py              # 聚合工厂
│   ├── value_objects.py            # 值对象定义
│   └── exceptions.py              # 领域异常
│
└── tests/                          # 测试
    ├── __init__.py
    ├── domain/
    ├── application/
    └── integration/

九、总结

9.1 DDD实践要点

通过在AI算法平台后端实践DDD,我们总结了以下关键要点:

限界上下文划分是第一步:清晰的限界上下文划分是DDD成功的关键。我们根据业务能力边界将平台划分为模型管理、训练管理、推理服务、数据管理、用户管理等多个上下文,每个上下文有清晰的职责和统一的语言。

聚合设计要保持精简:聚合是DDD中最核心的战术设计元素。我们坚持"聚合要小"的原则,每个聚合只包含最核心的实体和值对象,通过聚合根控制对外暴露的接口。

领域事件实现解耦:通过领域事件机制,不同限界上下文可以松耦合地通信。当训练完成时,会自动触发模型注册、评测、通知等多个下游操作,整个流程清晰且易于维护。

CQRS优化查询性能:在AI平台中,查询场景远多于写入场景。通过CQRS分离读写模型,我们可以针对不同场景进行专门优化,既保证了写入的一致性,又提升了查询的性能。

9.2 踩坑经验

教训一:不要过早细化。刚开始实施DDD时,我们试图把每个领域的每个概念都建模得非常精细,结果导致领域模型过于复杂,难以维护。后来我们采用了"先跑通核心流程,再逐步演进"的策略,效果好得多。

教训二:防腐层必不可少。在对接外部系统(如Kubernetes、对象存储)时,一定要建立防腐层,将外部系统的概念转换为领域概念,避免外部变化影响核心领域。

教训三:团队DDD能力建设很重要。DDD对团队的设计能力有较高要求。我们通过内部培训、代码审查、结对编程等方式,逐步提升了团队的DDD实践能力。


DDD不是银弹,但它确实是处理复杂业务领域的有力工具。希望本文的实践分享能为正在进行类似架构转型的团队提供一些参考。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区