尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零手搓AI工程流水线:数据、训练、推理与监控全链路实战

从零手搓AI工程流水线:数据、训练、推理与监控全链路实战 1. 为什么我要从零手搓一套AI工程流水线第一次看到ai-engineering-from-scratch这个项目名的时候我正被一堆调包侠式的教程搞得有点烦。满屏都是pip install然后三行代码调用某个封装好的接口跑通了但问一句数据进来之后到底发生了什么就卡壳。这个标题戳中的正是这个痛点——从零开始把AI工程里那些被框架藏起来的环节一层一层自己搭出来。说白了这个项目要解决的不是怎么用AI而是AI系统到底是怎么被工程化地组装起来的。它适合两类人一类是刚入行、只会调API但想搞懂底层链路的工程师另一类是有算法基础、但没做过完整工程落地、不知道模型上线前后要处理多少脏活的老手。我自己属于后者做了几年模型训练真到了要把东西部署给业务用的时候才发现数据管道、特征存储、推理服务、监控告警这些环节每一个都能让人掉一层皮。ai-engineering-from-scratch的核心价值在于它把AI工程拆成了一条可以自己动手复现的流水线从原始数据接入、清洗、特征工程到模型训练、评估、打包再到推理服务、性能压测、线上监控。整条链路不依赖重型平台用最朴素的工具和代码把每个环节跑通。我花了两周时间跟着这个思路自己搭了一遍踩了不少坑也总结了一些文档里不会写的经验。下面就把这套东西完整拆开讲从设计思路到实操细节再到问题排查尽量让不同基础的读者都能照着做出来。2. 整体架构设计与技术选型思路2.1 为什么选择从零手搓而不是直接用平台市面上不缺AI工程平台从云厂商的一站式方案到开源的MLOps工具链功能都很全。但我在实际项目里发现一个规律平台越全出问题时你越不知道从哪查。有一次线上推理延迟突然飙高排查了半天才发现是特征服务里一个缓存key的序列化方式变了导致缓存命中率归零。如果这套东西是我自己一行行搭的我五分钟就能定位到。从零手搓的另一个好处是可控性。AI工程里最要命的不是模型效果而是数据一致性。训练时用的特征和推理时算出来的特征只要有一个字段对不上模型表现就会断崖式下跌。自己搭流水线你能清楚地知道每个字段从哪来、经过哪些变换、最终以什么格式喂给模型。这种掌控感是调包给不了的。当然从零不代表什么都自己写。我的原则是核心链路的逻辑自己实现通用工具用成熟库。比如数据清洗用Pandas模型训练用PyTorch但特征版本管理、推理请求的预处理逻辑、监控指标的上报这些必须自己写因为它们是业务逻辑的载体。2.2 分层架构把AI工程切成四块我把整条流水线分成四层每层职责单一层与层之间通过明确的接口通信。这样设计的好处是任何一层出问题都不会污染其他层替换某一层的实现也不会牵一发动全身。层级职责核心组件常用工具数据层原始数据接入、清洗、版本管理数据管道、特征存储Pandas、Parquet、SQLite训练层特征工程、模型训练、评估训练脚本、实验追踪PyTorch、scikit-learn、MLflow服务层模型打包、推理API、批处理推理服务、请求预处理FastAPI、ONNX Runtime、Redis监控层指标采集、日志、告警指标上报、日志聚合Prometheus、Grafana、logging这个分层不是拍脑袋定的。数据层独立是因为数据变更频率远低于代码而且需要版本追溯训练层独立是因为它通常是离线批处理和服务层的在线推理有完全不同的性能要求服务层独立是因为它要面对高并发和低延迟监控层独立是因为它需要横跨所有层采集信息。2.3 技术选型的几个关键取舍为什么用Parquet而不是CSV存中间数据。CSV可读性好但AI工程里的数据动辄几百万行、上百列CSV的读写速度和存储效率都撑不住。Parquet是列式存储读取时只加载需要的列压缩比也高。我实测过同样一份用户行为数据CSV格式1.2GBParquet只要280MB读取速度快了将近4倍。代价是Parquet不能直接用文本编辑器打开调试时需要额外写几行代码查看但这个代价完全值得。为什么推理服务用FastAPI而不是Flask。Flask够简单但FastAPI原生支持异步和Pydantic数据校验。AI推理服务经常要处理并发请求异步能显著提升吞吐量。更重要的是Pydantic能在请求进入业务逻辑之前就完成字段类型和范围的校验把大量脏请求挡在门外。我试过用Flask手写校验逻辑代码量翻倍不说还容易漏掉边界情况。为什么监控用Prometheus而不是自己写日志分析。自己写日志分析灵活但你要处理指标聚合、时间窗口、告警规则这些通用问题工作量巨大。Prometheus的拉取模型和PromQL查询语言能让你用几行配置就实现过去5分钟推理延迟P99超过200ms就告警这种需求。代价是要额外维护一个Prometheus实例但对于任何认真做的AI服务这个投入都是必要的。提示技术选型没有绝对的对错关键是匹配你的团队规模和业务阶段。如果只是做个原型验证用CSV和Flask完全没问题但如果要上线给真实用户用上面这些取舍就值得认真考虑。3. 数据层从原始数据到可训练特征3.1 数据接入与清洗的实操细节数据层是整个流水线的地基地基没打好后面全是坑。我接过的最脏的一份数据是某业务系统的导出CSV里面混着全角逗号、换行符、还有用NULL字符串表示的缺失值。如果直接喂给模型训练出来的东西根本不能用。我的清洗流程分四步走。第一步是结构校验检查列名、列数、数据类型是否符合预期。这一步用Pydantic定义schema任何不符合的直接拒绝避免脏数据流入下游。第二步是缺失值处理这里有个经验不要无脑填充。先统计每列的缺失率缺失率超过60%的列直接丢弃因为填充出来的值噪声太大缺失率在5%到60%之间的数值列用中位数填充类别列用众数填充缺失率低于5%的可以考虑直接删除对应行。第三步是异常值检测用IQR方法找出超出1.5倍四分位距的值但不要直接删而是标记出来因为有些异常值恰恰是业务上最有价值的样本。第四步是格式统一把日期、金额、枚举值全部转成标准格式。import pandas as pd import numpy as np from pydantic import BaseModel, validator class DataSchema(BaseModel): user_id: int event_time: str amount: float category: str validator(amount) def amount_must_be_positive(cls, v): if v 0: raise ValueError(amount must be non-negative) return v def clean_data(df: pd.DataFrame) - pd.DataFrame: # 结构校验 for _, row in df.iterrows(): DataSchema(**row.to_dict()) # 缺失值处理 missing_rate df.isnull().mean() drop_cols missing_rate[missing_rate 0.6].index.tolist() df df.drop(columnsdrop_cols) for col in df.columns: if df[col].isnull().mean() 0: if df[col].dtype in [float64, int64]: df[col] df[col].fillna(df[col].median()) else: df[col] df[col].fillna(df[col].mode()[0]) # 异常值标记 numeric_cols df.select_dtypes(include[np.number]).columns for col in numeric_cols: Q1 df[col].quantile(0.25) Q3 df[col].quantile(0.75) IQR Q3 - Q1 lower Q1 - 1.5 * IQR upper Q3 1.5 * IQR df[f{col}_is_outlier] ((df[col] lower) | (df[col] upper)).astype(int) return df这段代码里有个细节值得说异常值我没有删除而是新增了一列标记。为什么因为在实际业务里异常值往往对应着特殊事件比如大额交易、系统故障、促销活动。直接删掉会丢失这些信息而模型可能恰恰需要学会识别这些特殊情况。标记出来之后模型可以自己决定怎么利用这个信息。3.2 特征存储的设计与版本管理特征存储是AI工程里最容易被忽视、但出问题最致命的一环。我见过太多团队训练时用一套特征计算逻辑推理时用另一套结果线上线下效果对不上排查几天才发现是某个字段的取整方式不一样。我的做法是特征计算逻辑只写一次训练和推理共用同一份代码。具体来说把每个特征的计算封装成一个函数输入是原始数据输出是特征值。训练时批量调用这些函数推理时单条调用。为了保证一致性所有特征函数必须是纯函数不能依赖外部状态不能有随机性。版本管理方面我用一个简单的约定每次特征逻辑变更版本号加一同时把变更前后的特征分布统计存下来。这样当线上效果波动时可以快速对比是哪个版本的特征出了问题。存储格式用Parquet按日期分区方便回溯。import hashlib import json from datetime import datetime class FeatureStore: def __init__(self, base_path: str): self.base_path base_path self.feature_registry {} def register_feature(self, name: str, func, version: str): 注册特征计算函数 self.feature_registry[name] { func: func, version: version, registered_at: datetime.now().isoformat() } def compute(self, name: str, raw_data: dict) - float: 计算单个特征值 if name not in self.feature_registry: raise KeyError(fFeature {name} not registered) return self.feature_registry[name][func](raw_data) def compute_batch(self, names: list, df: pd.DataFrame) - pd.DataFrame: 批量计算特征 result pd.DataFrame(indexdf.index) for name in names: result[name] df.apply( lambda row: self.compute(name, row.to_dict()), axis1 ) return result def get_version_hash(self) - str: 获取当前特征版本的哈希值 version_str json.dumps( {k: v[version] for k, v in self.feature_registry.items()}, sort_keysTrue ) return hashlib.md5(version_str.encode()).hexdigest()[:8]这个FeatureStore类虽然简单但解决了核心问题特征逻辑集中管理版本可追溯。get_version_hash方法返回的哈希值可以记录到每次训练和推理的日志里一旦发现效果异常直接对比哈希值就能确认是不是特征版本不一致导致的。注意特征计算函数里千万不要做耗时的操作比如查数据库、调外部API。这些应该在数据接入阶段完成特征函数只做纯计算。否则推理时延迟会高得无法接受。3.3 数据管道的调度与容错数据管道要定期跑但不能简单地用cron。我踩过的坑是某次上游数据延迟了半小时cron任务照常启动结果读到的是空文件清洗后得到空数据集训练脚本直接报错退出。更糟的是监控没配好没人发现直到第二天业务方反馈模型效果异常才查出来。我的改进方案是带依赖检查的调度。每次任务启动前先检查上游数据是否就绪检查项包括文件是否存在、文件大小是否在合理范围、数据行数是否与历史均值偏差不超过30%。任何一项不通过就跳过本次执行并告警而不是硬着头皮跑。容错方面每个处理步骤都要能重入。具体做法是每一步的输出都写到临时目录全部成功后再原子性地移动到最终目录。这样即使中途失败也不会留下半成品数据污染下游。重跑时从失败的那一步开始不用从头再来。4. 训练层模型训练与评估的工程化4.1 训练脚本的结构化组织很多人写训练脚本就是一堆代码从头写到尾跑通了就不管了。这种脚本过两周自己都看不懂更别说让别人接手。我的做法是把训练脚本拆成配置、数据加载、模型定义、训练循环、评估、保存六个部分每部分独立成函数或类。配置部分用YAML文件管理所有超参数、路径、开关都写在配置里代码里不出现硬编码。这样做的好处是换一组参数不用改代码直接改配置文件就行实验记录也清晰。# config/train_config.yaml data: train_path: data/processed/train.parquet val_path: data/processed/val.parquet feature_names: [age, income, click_count, purchase_ratio] label_col: is_converted model: input_dim: 4 hidden_dims: [64, 32] output_dim: 1 dropout: 0.3 training: batch_size: 256 learning_rate: 0.001 epochs: 50 early_stop_patience: 5 output: model_dir: models/exp_001 log_dir: logs/exp_001数据加载部分要处理两个关键问题类别特征编码和数据不平衡。类别特征用目标编码还是独热编码取决于基数大小。基数低于10的用独热高于10的用目标编码。数据不平衡用加权采样或者focal loss我一般先用加权采样简单有效。4.2 实验追踪别让实验结果变成一笔糊涂账我刚开始做实验的时候跑了几十组参数结果最好的那个模型忘了存对应的配置只能重跑。这种低级错误在AI工程里太常见了。解决办法就是实验追踪每次训练自动记录配置、指标、模型文件路径。我用MLflow做追踪但即使不用MLflow自己写个简单的记录逻辑也行。核心是三点每次实验有唯一ID、配置和指标绑定存储、模型文件路径可追溯。import mlflow import mlflow.pytorch import yaml def train_with_tracking(config_path: str): with open(config_path) as f: config yaml.safe_load(f) mlflow.set_experiment(ai-engineering-from-scratch) with mlflow.start_run() as run: # 记录配置 mlflow.log_params({ batch_size: config[training][batch_size], learning_rate: config[training][learning_rate], epochs: config[training][epochs], hidden_dims: str(config[model][hidden_dims]) }) # 训练循环 model, metrics train_model(config) # 记录指标 mlflow.log_metrics({ val_auc: metrics[val_auc], val_loss: metrics[val_loss], train_loss: metrics[train_loss] }) # 保存模型 mlflow.pytorch.log_model(model, model) # 记录特征版本 mlflow.set_tag(feature_version, feature_store.get_version_hash()) return run.info.run_id这里有个细节feature_version用tag记录而不是param因为tag支持字符串param在某些版本里对特殊字符有限制。另外模型保存用mlflow.pytorch.log_model而不是直接torch.save因为前者会自动记录PyTorch版本和依赖信息复现时少踩很多坑。4.3 评估指标的选择与陷阱评估指标不能只看准确率这是老生常谈但实际做的时候还是容易掉坑。我总结了几条经验。第一区分业务指标和技术指标。技术指标是AUC、F1这些业务指标是转化率、GMV这些。技术指标涨了不代表业务指标一定涨中间还隔着阈值选择、业务规则等环节。训练阶段盯技术指标上线前一定要用业务指标做最终验证。第二注意评估集的时间分布。如果评估集是随机划分的而实际业务有时间趋势那评估结果会偏乐观。正确做法是按时间划分用过去的数据训练用未来的数据评估。我吃过这个亏随机划分时AUC 0.85按时间划分后掉到0.78差距很大。第三校准比排序更重要。很多场景下模型输出的概率值要直接用于决策比如转化概率大于0.3就发优惠券。这时候概率的绝对准确性校准比排序能力AUC更重要。校准用可靠性图检查必要时用Platt scaling或Isotonic regression做后处理。指标适用场景注意事项AUC排序场景如推荐对类别不平衡不敏感但不反映概率准确性F1类别不平衡的分类需要选阈值阈值不同F1差异大LogLoss需要概率输出的场景对异常预测惩罚大能反映校准程度业务指标最终决策需要AB测试验证离线指标仅供参考5. 服务层推理服务的搭建与优化5.1 推理API的设计要点推理服务不是把模型包一层HTTP接口就完事了。我见过太多推理服务功能上能用但性能一塌糊涂或者稍微有点异常请求就崩溃。设计推理API时有几个点必须考虑。请求校验前置。用Pydantic定义请求体所有字段的类型、范围、必填性都在进入业务逻辑之前校验完。这样业务代码里不用写一堆if-else做防御性编程代码干净很多。批处理支持。单条推理的吞吐量很低如果业务允许尽量支持批量请求。批量推理时把多条请求的特征拼成一个batch一次前向传播搞定吞吐量能提升几倍到几十倍。超时和降级。推理服务必须设超时超过阈值直接返回默认值或错误码不能让请求无限等待。降级策略要提前想好比如模型服务不可用时返回一个基于规则的兜底结果。from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from typing import List import numpy as np import time app FastAPI() class PredictRequest(BaseModel): features: List[float] Field(..., min_items4, max_items4) request_id: str Field(..., min_length1) class PredictResponse(BaseModel): request_id: str score: float model_version: str latency_ms: float class ModelService: def __init__(self, model, feature_store, version: str): self.model model self.feature_store feature_store self.version version def predict(self, features: List[float]) - float: arr np.array([features], dtypenp.float32) with torch.no_grad(): output self.model(torch.from_numpy(arr)) score torch.sigmoid(output).item() return score model_service ModelService(loaded_model, feature_store, v1.2.3) app.post(/predict, response_modelPredictResponse) async def predict(request: PredictRequest): start time.time() try: score model_service.predict(request.features) except Exception as e: raise HTTPException(status_code500, detailstr(e)) latency (time.time() - start) * 1000 return PredictResponse( request_idrequest.request_id, scorescore, model_versionmodel_service.version, latency_mslatency )这段代码里model_version和latency_ms是必须返回的。前者用于问题排查时确认是哪个模型出的结果后者用于监控延迟分布。很多团队忽略这两个字段出问题时只能靠猜。5.2 模型打包与加载的性能优化模型加载慢是推理服务冷启动的常见问题。一个几百MB的PyTorch模型从磁盘加载到内存可能要十几秒这期间服务不可用。优化手段有几个。转ONNX。PyTorch模型转成ONNX格式后加载速度通常能提升2到3倍推理速度也有提升。转换时注意opset版本要匹配ONNX Runtime的版本否则可能加载失败。模型量化。把FP32的权重转成INT8模型体积缩小4倍推理速度提升明显精度损失通常在1%以内。量化分动态和静态两种动态量化简单但加速有限静态量化需要校准数据但加速更明显。预热加载。服务启动时先跑几条假请求把模型的懒加载部分触发完避免第一个真实请求超时。预热数据用全零或随机值都行目的是让计算图完成初始化。import onnxruntime as ort import numpy as np class ONNXModelService: def __init__(self, onnx_path: str): # 设置线程数和优化级别 sess_options ort.SessionOptions() sess_options.intra_op_num_threads 4 sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL self.session ort.InferenceSession( onnx_path, sess_options, providers[CPUExecutionProvider] ) self.input_name self.session.get_inputs()[0].name # 预热 dummy_input np.zeros((1, 4), dtypenp.float32) self.session.run(None, {self.input_name: dummy_input}) def predict(self, features: np.ndarray) - np.ndarray: return self.session.run(None, {self.input_name: features})[0]intra_op_num_threads设成CPU核心数的一半比较合适设太高反而会因为线程切换开销导致性能下降。graph_optimization_level设成ORT_ENABLE_ALL让ONNX Runtime做算子融合等优化。5.3 缓存策略什么时候该缓存什么时候不该推理结果能不能缓存取决于请求的重复率。如果大量请求的特征完全相同缓存能大幅降低计算量。但AI推理的请求往往特征值连续完全相同的概率很低这时候缓存命中率会很低反而增加了一次缓存查询的开销。我的判断标准是统计一天内请求特征的重复率超过20%才值得加缓存。缓存key用特征的哈希值注意浮点数要先量化再哈希否则微小的浮点误差会导致缓存失效。import hashlib import redis import json class CachedPredictor: def __init__(self, model_service, redis_client, ttl: int 3600): self.model_service model_service self.redis redis_client self.ttl ttl def _make_key(self, features: list) - str: # 浮点数保留4位小数后哈希 quantized [round(f, 4) for f in features] key_str json.dumps(quantized, sort_keysTrue) return fpred:{hashlib.md5(key_str.encode()).hexdigest()} def predict(self, features: list) - float: key self._make_key(features) cached self.redis.get(key) if cached is not None: return float(cached) score self.model_service.predict(features) self.redis.setex(key, self.ttl, str(score)) return scoreTTL设1小时是个经验值。设太短缓存没意义设太长模型更新后旧结果还在缓存里会导致线上线下不一致。模型更新时记得主动清空相关缓存。6. 监控层让问题在爆发前被发现6.1 必须监控的四类指标监控不是越多越好指标太多反而会淹没真正重要的信号。我聚焦四类指标每类选一到两个核心项。延迟指标。P50、P95、P99三个分位数都要看。P50反映典型体验P99反映最差体验。如果P99突然飙高但P50正常说明有少量慢请求可能是某些特征值触发了慢路径。流量指标。QPS和请求量趋势。流量突降可能是上游故障或调用方变更流量突增可能是被攻击或业务活动。错误指标。错误率和错误类型分布。错误率超过1%就要告警错误类型分布能帮你快速定位是参数问题还是模型问题。模型指标。预测分数的分布。如果分数分布突然偏移比如均值从0.3变成0.6说明输入数据分布变了模型可能已经失效。from prometheus_client import Counter, Histogram, Gauge import time # 定义指标 REQUEST_COUNT Counter( predict_requests_total, Total predict requests, [status, model_version] ) REQUEST_LATENCY Histogram( predict_latency_seconds, Predict latency, buckets[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0] ) PREDICTION_SCORE Histogram( prediction_score, Distribution of prediction scores, buckets[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] ) def monitored_predict(features, model_version): start time.time() try: score model_service.predict(features) REQUEST_COUNT.labels(statussuccess, model_versionmodel_version).inc() PREDICTION_SCORE.observe(score) return score except Exception as e: REQUEST_COUNT.labels(statuserror, model_versionmodel_version).inc() raise finally: REQUEST_LATENCY.observe(time.time() - start)PREDICTION_SCORE这个直方图特别有用。把它的分位数画成时间序列图一旦发现P50或P90突然跳变基本可以确定是数据分布漂移。这时候要立刻检查上游数据管道而不是等业务方反馈效果变差。6.2 告警规则的设计与误报控制告警最怕两件事该报的没报和不该报的乱报。前者导致故障扩大后者导致告警疲劳最后真出事了没人看。我的告警规则设计原则是基于趋势而非瞬时值基于多指标组合而非单指标。比如延迟告警不是P99超过200ms就报而是过去5分钟P99持续超过200ms且QPS大于10才报。加QPS条件是为了过滤掉低流量时段的偶发慢请求。告警分级也很重要。P0告警电话通知只留给服务不可用、错误率超过10%这种级别P1告警群消息用于延迟超标、分数分布漂移P2告警邮件用于磁盘空间不足、证书即将过期这类可以慢慢处理的问题。告警项条件级别处理时限服务不可用健康检查连续3次失败P0立即错误率超标5分钟错误率10%P0立即延迟超标5分钟P99200ms且QPS10P130分钟分数漂移P50分数偏移0.15P11小时磁盘空间使用率85%P224小时6.3 日志规范出问题时能快速定位日志不是越多越好关键是结构化和可检索。我用JSON格式打日志每条日志包含时间戳、请求ID、模型版本、特征哈希、预测分数、延迟。这样出问题时用请求ID一搜整条链路的日志都能串起来。import logging import json from datetime import datetime class StructuredLogger: def __init__(self, name: str): self.logger logging.getLogger(name) handler logging.StreamHandler() handler.setFormatter(logging.Formatter(%(message)s)) self.logger.addHandler(handler) self.logger.setLevel(logging.INFO) def log_prediction(self, request_id: str, features: list, score: float, latency_ms: float, model_version: str): record { timestamp: datetime.utcnow().isoformat(), request_id: request_id, model_version: model_version, feature_hash: hashlib.md5( json.dumps([round(f, 4) for f in features]).encode() ).hexdigest()[:8], score: round(score, 4), latency_ms: round(latency_ms, 2) } self.logger.info(json.dumps(record))feature_hash这个字段是排查数据问题的利器。如果怀疑某类请求有问题用feature_hash去日志里搜能快速找到所有相同特征的请求对比它们的预测结果和延迟。7. 常见问题与排查技巧实录7.1 线上线下效果不一致的排查路径这是AI工程里最经典的问题排查起来像破案。我总结了一条排查路径按顺序检查基本能覆盖90%的情况。第一步对比特征版本哈希。训练时记录的特征版本哈希和线上推理时的是否一致。不一致的话直接定位到是哪个特征变了。第二步对比特征分布。从线上日志里采样一批请求的特征值和训练集的特征分布做对比。用KS检验或者简单的分位数对比找出偏移最大的特征。第三步检查预处理逻辑。有时候特征版本一致但预处理逻辑有细微差别。比如训练时对缺失值填了中位数推理时填了0。这种问题最隐蔽需要逐行对比代码。第四步检查模型加载。确认线上加载的模型文件和训练产出的模型文件是同一个。用文件哈希对比不要只看文件名。提示排查这类问题时一定要用数据说话不要靠猜。每一步都要有明确的对比结果否则容易在错误的方向上浪费时间。7.2 推理延迟突然升高的常见原因延迟问题排查相对直接因为指标明确。我遇到过的情况按频率排序如下。特征计算变慢。某个特征的计算依赖了外部服务外部服务响应变慢导致整体延迟升高。排查方法是给每个特征的计算单独打点看哪个特征耗时增加了。模型输入维度变化。上游传过来的特征数量或顺序变了模型做了额外的对齐操作。这种情况通常伴随错误率上升比较容易发现。资源竞争。同一台机器上跑了其他任务CPU或内存被抢占。看系统监控的CPU使用率和内存使用率如果推理服务进程的资源使用没变但延迟升高基本就是资源竞争。GC停顿。Python的垃圾回收在对象多的时候会停顿。如果延迟呈现周期性尖刺很可能是GC导致的。可以用gc.set_threshold调整GC频率或者用gc.freeze冻结长期存活的对象。7.3 模型效果衰减的应对策略模型上线后效果会随时间衰减这是必然的因为业务在变、用户在变。关键是要早发现、快响应。早发现靠监控前面说的分数分布漂移监控就是干这个的。快响应靠预案我一般准备三套方案快速重训、规则兜底、回滚旧版本。快速重训是用最近一周的数据重新训练一个模型通常几小时内能完成。规则兜底是当模型完全不可用时切换到基于业务规则的简单策略保证服务不中断。回滚旧版本是当新模型出问题时切回上一个稳定版本。问题严重程度响应策略预计恢复时间分数轻微漂移观察准备重训1-2天分数明显漂移快速重训AB测试4-8小时模型完全失效规则兜底30分钟新模型引入故障回滚旧版本10分钟7.4 实操心得与避坑清单最后分享几条我踩坑换来的经验都是文档里不会写的。不要在推理服务里做特征工程。特征计算应该在请求进入推理服务之前完成推理服务只负责模型前向传播。把特征工程放进推理服务会导致服务臃肿、延迟高、难以维护。模型文件要带版本号。model.pt这种命名方式迟早会出事。用model_v1.2.3_20240115.pt这种格式版本、日期一目了然。配置文件不要硬编码路径。用环境变量或配置中心管理路径本地开发和线上部署用同一份代码只改配置。日志里不要打完整特征值。特征值可能包含敏感信息而且日志量会爆炸。打特征哈希就够了需要具体值时用请求ID去数据仓库查。压测要在生产同配置的机器上做。开发机上压测通过不代表线上没问题CPU型号、内存大小、网络延迟都会影响结果。监控告警要定期演练。告警配了不代表能用定期手动触发一次确认通知能收到、处理流程能走通。我见过告警配置写错邮箱故障时没人收到通知的情况。这套ai-engineering-from-scratch的流水线我前前后后搭了三遍每遍都有新的体会。第一遍只求跑通第二遍开始关注性能和稳定性第三遍才真正理解每个设计决策背后的权衡。如果你也在做类似的事情我的建议是不要一开始就追求完美先把主链路跑通然后针对实际遇到的问题逐个优化。工程能力是在解决具体问题的过程中长出来的不是看文档看出来的。
返回列表