LLMOps平台架构:从模型训练到在线推理的全流程基建设计
大模型落地过程中,团队往往把 80% 的精力花在模型本身,却忽略了将模型推向生产环境所需的工程基础设施。本文结合 MLflow 与 Kubeflow 的实践经验,梳理一套从模型注册、实验追踪、版本管理到在线推理的 LLMOps 全流程基座方案。
一、LLMOps 的核心挑战与平台定位
传统 MLOps 解决的是小模型的训练与部署问题,而 LLMOps 面临的是另一套约束条件:模型体积动辄数十 GB、推理延迟要求严格、Prompt 版本需要与模型版本联动管理、成本核算从 GPU 小时扩展到 Token 维度。这些差异决定了不能简单地把旧有 ML 平台"升级"一下了事。
从工程视角看,一套合格的 LLMOps 平台至少需要覆盖以下六个环节:
- 模型注册中心:统一管理基座模型、微调模型、量化模型等多态模型资产
- 实验追踪:记录每一次微调实验的超参、数据集快照、评估指标
- 模型版本管理:建立 Prompt 版本与模型版本的绑定关系,支持一键回滚
- 推理部署流水线:从模型 artifacts 到可对外服务的自动化 CI/CD
- A/B 实验框架:在生产流量中对不同模型版本做受控对比
- 效果监控闭环:在线指标采集 → 异常告警 → 触发优化迭代
graph TB subgraph 数据准备层 A[训练数据集] --> B[数据版本管理] B --> C[数据质量校验] end subgraph 实验与训练层 D[实验配置] --> E[分布式训练] E --> F[实验追踪 MLflow] F --> G[模型评估] end subgraph 模型管理层 G --> H[模型注册中心] H --> I[模型版本管理] I --> J[Prompt版本绑定] end subgraph 部署与服务层 J --> K[推理部署流水线] K --> L[A/B实验路由] L --> M[在线推理服务] end subgraph 监控与闭环 M --> N[指标采集与告警] N --> O[效果分析报告] O -->|触发优化| D end style 数据准备层 fill:#e1f5fe style 实验与训练层 fill:#fff3e0 style 模型管理层 fill:#e8f5e9 style 部署与服务层 fill:#f3e5f5 style 监控与闭环 fill:#fce4ec上图展示了 LLMOps 平台的核心数据流。其中模型管理层是整个体系的中枢——它向上承接训练产出的模型 artifacts,向下驱动部署流水线的触发。
二、模型全生命周期管理:注册、实验与版本控制
2.1 模型注册中心设计
模型注册中心不只是一个"模型文件的 FTP 服务器"。它需要存储模型的元信息:基座模型来源(Llama3、Qwen2 等)、微调方式(LoRA、全参微调)、量化精度(FP16、INT8、GPTQ)、适用的任务类型、输入输出 Schema、以及关联的 Prompt 模板。
以下是基于 MLflow Model Registry 的模型注册接口封装:
/** * 模型注册服务 - 封装 MLflow Model Registry 操作 * 提供模型注册、版本查询、阶段转换等核心能力 */ @Service @Slf4j public class ModelRegistryService { private final MlflowClient mlflowClient; private final ModelMetadataRepository metadataRepository; public ModelRegistryService(MlflowClient mlflowClient, ModelMetadataRepository metadataRepository) { this.mlflowClient = mlflowClient; this.metadataRepository = metadataRepository; } /** * 注册新模型版本 * * @param request 包含模型名称、artifact路径、元信息 * @return 注册后的模型版本信息 */ public ModelVersionDTO registerModel(RegisterModelRequest request) { // 参数校验 if (request.getModelName() == null || request.getModelName().isBlank()) { throw new IllegalArgumentException("模型名称不能为空"); } if (request.getArtifactUri() == null) { throw new IllegalArgumentException("模型artifact路径不能为空"); } try { // 1. 通过 MLflow 注册模型版本 RegisteredModel registeredModel = mlflowClient.getRegisteredModel( request.getModelName()); if (registeredModel == null) { mlflowClient.createRegisteredModel(request.getModelName()); log.info("创建新的注册模型: {}", request.getModelName()); } ModelVersion modelVersion = mlflowClient.createModelVersion( request.getModelName(), request.getArtifactUri(), request.getRunId() ); // 2. 持久化扩展元信息(MLflow 原生不支持的字段) ModelMetadataEntity metadata = ModelMetadataEntity.builder() .modelVersionId(modelVersion.getVersion()) .baseModel(request.getBaseModel()) // 如 "meta-llama/Llama-3-70b" .fineTuneMethod(request.getFineTuneMethod()) // LoRA/Full/QLoRA .quantization(request.getQuantization()) // FP16/INT8/GPTQ .taskType(request.getTaskType()) // chat/completion/embedding .inputSchema(request.getInputSchema()) // JSON Schema .outputSchema(request.getOutputSchema()) .promptTemplate(request.getPromptTemplate()) .build(); metadataRepository.save(metadata); log.info("模型注册成功: name={}, version={}", request.getModelName(), modelVersion.getVersion()); return ModelVersionDTO.from(modelVersion, metadata); } catch (MlflowException e) { log.error("模型注册失败: name={}, error={}", request.getModelName(), e.getMessage(), e); throw new ModelRegistryException("模型注册失败: " + e.getMessage(), e); } } /** * 将模型版本推进到指定阶段 * 阶段流转: None → Staging → Production → Archived */ @Transactional public void transitionStage(String modelName, String version, ModelStage targetStage) { ModelVersion currentVersion = mlflowClient.getModelVersion(modelName, version); ModelStage currentStage = ModelStage.fromString(currentVersion.getCurrentStage()); // 阶段流转合法性校验 if (!isValidTransition(currentStage, targetStage)) { throw new IllegalStateException( String.format("不允许的阶段流转: %s → %s", currentStage, targetStage)); } // 如果目标阶段是 Production,先将当前 Production 版本归档 if (targetStage == ModelStage.PRODUCTION) { archiveCurrentProduction(modelName); } mlflowClient.transitionModelVersionStage(modelName, version, targetStage.name()); log.info("模型阶段流转: {} v{} {} → {}", modelName, version, currentStage, targetStage); } private boolean isValidTransition(ModelStage from, ModelStage to) { // None → Staging → Production / Archived // Staging → Production / Archived / None // Production → Archived // Archived → None(重新激活) return switch (from) { case NONE -> to == ModelStage.STAGING; case STAGING -> to == ModelStage.PRODUCTION || to == ModelStage.ARCHIVED || to == ModelStage.NONE; case PRODUCTION -> to == ModelStage.ARCHIVED; case ARCHIVED -> to == ModelStage.NONE; }; } private void archiveCurrentProduction(String modelName) { mlflowClient.getLatestVersions(modelName, List.of("Production")) .forEach(v -> mlflowClient.transitionModelVersionStage( modelName, v.getVersion(), "Archived")); } }2.2 实验追踪与指标对比
每次微调实验需要记录的信息远不止 loss 曲线。关键要素包括:
| 追踪维度 | 具体内容 | 用途 |
|---|---|---|
| 超参数 | learning_rate, batch_size, lora_rank, epochs | 复现与对比 |
| 数据集快照 | 数据版本 hash、样本数量、分布统计 | 归因分析 |
| 评估指标 | BLEU, ROUGE, 人工评分, 安全评分 | 模型选优 |
| 资源消耗 | GPU 型号、显存峰值、训练时长 | 成本核算 |
| 环境信息 | 镜像 tag、依赖库版本、CUDA 版本 | 可复现性 |
实操中,建议将 MLflow 的 Tracking Server 对接 PostgreSQL 做元数据存储,Artifact 对接 S3/MinIO 做模型文件存储。每次训练任务启动时注入统一的MLFLOW_TRACKING_URI和MLFLOW_EXPERIMENT_NAME,确保不会出现"散落"的实验记录。
三、推理部署流水线与 A/B 实验框架
3.1 部署流水线设计
推理部署流水线的核心挑战在于:模型文件体积大(数十 GB),镜像构建耗时长,且不同模型的推理引擎各异(vLLM、TGI、Triton Inference Server)。
推荐的流水线架构如下:
graph LR A[模型注册中心<br/>版本变更事件] --> B{触发类型判断} B -->|新版本Staging| C[自动化测试流水线] B -->|推进Production| D[生产部署流水线] C --> C1[功能测试<br/>输入输出校验] C1 --> C2[性能测试<br/>延迟/吞吐基准] C2 --> C3[安全测试<br/>越狱/有害内容] D --> D1[镜像构建<br/>Kaniko/BuildKit] D1 --> D2[K8s滚动更新<br/>vLLM Deployment] D2 --> D3[健康检查<br/>就绪探针验证] D3 --> D4[流量切换<br/>Istio权重调整] style C fill:#fff3e0 style D fill:#e8f5e9关键工程细节:
- 镜像分层构建:将推理引擎(vLLM)作为基础镜像层,模型权重文件作为独立层挂载(通过 PVC 或模型存储 CSI 驱动),避免每次模型更新都重新构建数 GB 的镜像
- 预热机制:新 Pod 启动后先执行"预热请求"(发送一组代表性 prompt),待模型 KV Cache 预热完毕后再接入流量
- 优雅下线:旧版本 Pod 收到 SIGTERM 后,等待正在处理的请求完成(最长 graceful_shutdown_timeout),同时 Istio 摘除其 endpoint
3.2 A/B 实验框架
模型的 A/B 实验与传统应用的 A/B 不同:同一个用户在不同模型下可能产生截然不同的对话体验,因此需要更精细的分流策略。
/** * 模型A/B实验路由器 * 基于用户ID哈希实现确定性分流,保证同一用户始终路由到同一模型版本 */ @Component public class ModelExperimentRouter { private final ExperimentConfigRepository configRepository; private final LoadingCache<String, ExperimentConfig> configCache; // 使用一致性哈希环,实验配置变更时减少用户漂移 private volatile NavigableMap<Integer, ModelVariant> hashRing = new TreeMap<>(); public ModelExperimentRouter(ExperimentConfigRepository configRepository) { this.configRepository = configRepository; this.configCache = Caffeine.newBuilder() .refreshAfterWrite(30, TimeUnit.SECONDS) .expireAfterWrite(5, TimeUnit.MINUTES) .build(this::loadExperimentConfig); // 启动定时刷新哈希环 ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(this::rebuildHashRing, 0, 30, TimeUnit.SECONDS); } /** * 根据用户ID路由到目标模型变体 * * @param userId 用户唯一标识 * @param modelName 模型名称(如 "chat-assistant") * @return 目标模型变体(包含模型版本和推理端点) */ public ModelVariant route(String userId, String modelName) { ExperimentConfig config = configCache.get(modelName); if (config == null || !config.isEnabled()) { return getDefaultVariant(modelName); } // 检查白名单用户(用于内部验证) if (config.getWhitelistUsers().contains(userId)) { return config.getWhitelistVariant(); } // 一致性哈希路由 int hash = Math.abs(MurmurHash3.hash32(userId.getBytes(StandardCharsets.UTF_8))); Map.Entry<Integer, ModelVariant> entry = hashRing.ceilingEntry(hash); if (entry == null) { entry = hashRing.firstEntry(); } ModelVariant variant = entry.getValue(); log.debug("A/B路由: userId={}, model={}, hash={}, variant={}", userId, modelName, hash, variant.getVersion()); return variant; } private void rebuildHashRing() { NavigableMap<Integer, ModelVariant> newRing = new TreeMap<>(); List<ExperimentConfig> configs = configRepository.findAllEnabled(); for (ExperimentConfig config : configs) { int virtualNodes = 150; // 虚拟节点数,减少哈希倾斜 for (ModelVariant variant : config.getVariants()) { for (int i = 0; i < virtualNodes; i++) { int hash = Math.abs(MurmurHash3.hash32( (variant.getVersion() + "#" + i).getBytes(StandardCharsets.UTF_8))); newRing.put(hash, variant); } } } this.hashRing = newRing; log.info("A/B哈希环重建完成, 虚拟节点数: {}", newRing.size()); } private ModelVariant getDefaultVariant(String modelName) { // 从注册中心获取 Production 阶段的最新版本 return modelRegistryService.getProductionVariant(modelName); } }四、效果监控闭环与告警体系
监控是 LLMOps 闭环的"最后一公里",也是启动下一轮优化的触发器。
4.1 监控指标体系
LLM 推理服务的监控需要覆盖四层:
| 层级 | 核心指标 | 告警阈值示例 |
|---|---|---|
| 基础设施层 | GPU 利用率、显存占用、节点温度 | GPU 利用率 > 95% 持续 5min |
| 推理服务层 | 请求 QPS、首 Token 延迟(TTFT)、Token 生成速率、排队长度 | P99 TTFT > 2000ms |
| 模型质量层 | 输出长度分布、拒答率、空响应率 | 空响应率 > 1% |
| 业务指标层 | 用户点赞率、对话留存率、任务完成率 | 点赞率日环比下降 > 20% |
4.2 指标采集实现
/** * 推理服务指标采集拦截器 * 在 gRPC 拦截器层面对所有推理请求做指标采集 */ @Component @Slf4j public class InferenceMetricsInterceptor implements ServerInterceptor { private final MeterRegistry meterRegistry; private final Clock clock; // 核心指标定义 private final Timer ttftTimer; // Time To First Token private final Counter requestCounter; // 请求总数 private final Counter tokenCounter; // Token 计数 private final DistributionSummary outputLengthSummary; // 输出长度分布 public InferenceMetricsInterceptor(MeterRegistry meterRegistry, Clock clock) { this.meterRegistry = meterRegistry; this.clock = clock; this.ttftTimer = Timer.builder("llm.inference.ttft") .description("首Token延迟(毫秒)") .publishPercentiles(0.5, 0.95, 0.99) .register(meterRegistry); this.requestCounter = Counter.builder("llm.inference.requests") .description("推理请求总数") .register(meterRegistry); this.tokenCounter = Counter.builder("llm.inference.tokens") .description("Token总数(输入+输出)") .register(meterRegistry); this.outputLengthSummary = DistributionSummary.builder("llm.inference.output_length") .description("输出Token数分布") .publishPercentiles(0.5, 0.95, 0.99) .register(meterRegistry); } @Override public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall( ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) { long startTime = clock.millis(); String modelVersion = headers.get( Metadata.Key.of("x-model-version", Metadata.ASCII_STRING_MARSHALLER)); // 增加请求计数 Tags tags = Tags.of( Tag.of("model_version", modelVersion != null ? modelVersion : "unknown"), Tag.of("endpoint", call.getMethodDescriptor().getFullMethodName()) ); requestCounter.increment(); ServerCall.Listener<ReqT> listener = next.startCall( new ForwardingServerCall.SimpleForwardingServerCall<>(call) { private long firstTokenTime = -1; private int outputTokenCount = 0; @Override public void sendMessage(RespT message) { // 记录首Token时间 if (firstTokenTime == -1) { firstTokenTime = clock.millis(); long ttft = firstTokenTime - startTime; ttftTimer.record(ttft, TimeUnit.MILLISECONDS); log.debug("TTFT recorded: {}ms for model={}", ttft, modelVersion); } outputTokenCount++; super.sendMessage(message); } @Override public void close(Status status, Metadata trailers) { // 采集输出长度分布 if (outputTokenCount > 0) { outputLengthSummary.record(outputTokenCount); tokenCounter.increment(outputTokenCount); } // 记录异常响应 if (!status.isOk()) { Counter.builder("llm.inference.errors") .tag("error_code", status.getCode().name()) .register(meterRegistry) .increment(); } long totalTime = clock.millis() - startTime; log.debug("推理完成: model={}, tokens={}, time={}ms, status={}", modelVersion, outputTokenCount, totalTime, status.getCode()); super.close(status, trailers); } }, headers); return listener; } }4.3 质量监控与自动回滚
当模型质量指标(空响应率、拒答率)连续 3 个采样周期超过阈值时,触发自动回滚到上一稳定版本。这需要在部署流水线中维护一个"已知稳定版本"的指针,回滚操作实质上是将流量权重从当前版本切回稳定版本。
# Prometheus 告警规则:模型质量自动回滚触发条件 groups: - name: llm_model_quality rules: - alert: ModelEmptyResponseRateHigh expr: | rate(llm_inference_requests{status="empty_response"}[5m]) / rate(llm_inference_requests[5m]) > 0.01 for: 15m labels: severity: critical action: auto_rollback annotations: summary: "模型 {{ $labels.model_version }} 空响应率超过1%" description: "当前空响应率 {{ $value | humanizePercentage }},触发自动回滚"五、总结
构建 LLMOps 平台,本质上是在解决"模型迭代速度"与"服务稳定性"之间的矛盾。从工程实践看,有三个关键认知:
第一,模型注册中心是 LLMOps 的中枢。模型的元信息(基座来源、微调方式、量化精度、关联 Prompt)必须结构化存储,才能支撑后续的自动化流水线。第二,部署流水线要与模型文件的生命周期解耦。通过分层镜像 + PVC 挂载的方式,避免每次模型更新都重新构建巨型镜像。第三,监控体系必须覆盖到模型质量层。基础设施和推理服务的"绿灯"不代表模型输出质量合格,空响应率、输出长度异常分布这些信号往往比 GPU 告警更早暴露问题。
LLMOps 不是一次性工程,而是一个持续迭代的运营体系。当你的平台能够做到"模型训练完成 → 自动评估 → 灰度发布 → 效果监控 → 自动回滚/扩量"的完整闭环,才算真正具备了将 AI 能力规模化交付的工程底座。