ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AI工程化实战:从零构建可验证、可回滚的生产级AI系统

AI工程化实战:从零构建可验证、可回滚的生产级AI系统

1. 这不是调包,是亲手搭起AI工程的骨架

“AI Engineering from Scratch”——看到这个标题,我第一反应不是兴奋,而是下意识摸了摸键盘边沿那层被磨得发亮的漆面。过去三年,我带过17个团队做AI落地项目,从智能质检产线到金融风控模型迭代,几乎每个项目初期都卡在同一个地方:不是算法不行,而是整个工程链路像用胶带缠起来的水管,一加压就漏,一换场景就崩。大家习惯性点开Hugging Face一键加载model,pip install transformers完事,但当客户问“模型上线后QPS掉到1/3怎么办?”“训练日志里突然出现NaN,怎么定位是数据预处理还是梯度爆炸?”“AB测试时两个版本指标波动大到无法归因”——这时候,靠文档和Stack Overflow已经救不了你。

AI Engineering from Scratch,核心不在“从零写Transformer”,而在于重建一套可验证、可追踪、可回滚、可协作的工程化肌肉记忆。它解决的不是“能不能跑通”,而是“能不能在生产环境里稳稳扛住每天200万次推理请求,同时让新同事三天内能看懂整个数据血缘关系”。关键词“from-scratch”在这里是动词,不是形容词——它意味着主动选择不依赖黑盒封装,把每层抽象都拆开、验过、记下来。比如,为什么选PyTorch Lightning而不是原生PyTorch?不是因为它更“高级”,而是它强制你把数据加载、训练循环、验证逻辑、日志记录这四块骨头拆得清清楚楚,避免后期维护时发现trainer.py文件长达1200行却找不到loss计算在哪一行。再比如,为什么坚持用Docker Compose而非直接跑裸容器?因为当你需要在客户现场快速复现一个“训练时GPU显存溢出”的问题时,能用同一份docker-compose.yml在本地、测试机、生产机上1:1还原环境,比翻三遍requirements.txt还管用。

适合谁来读?如果你正处在这样的节点:刚用scikit-learn跑通第一个分类模型,但面对真实业务数据(缺失值混着特殊字符、时间戳格式不统一、label分布偏斜到离谱)时手足无措;或者你已能调参调到AUC涨0.003,却说不清模型服务化时为什么gunicorn worker数设成CPU核心数的2倍最稳;又或者你团队里总有人改了preprocessing.py却没更新README,导致全组训练结果对不上——那这篇就是为你写的。它不教你怎么成为算法博士,但能让你在下次晨会汇报“模型延迟优化方案”时,底气十足地指着监控图说:“我把batch_size从32调到64,TPS提升37%,但P99延迟从82ms升到115ms,所以最终选了48,这是压测报告第7页的对比表格。”——这种确定性,才是AI Engineering的起点。

2. 整体架构设计:为什么拒绝“一步到位”的诱惑

2.1 拒绝“端到端大模型”幻觉,回归分层解耦本质

很多团队一上来就想搞“一个模型解决所有问题”,比如用一个超大LLM同时做文本分类、实体抽取、情感打分。实操中我们踩过最深的坑是:当客户要求新增一个“投诉倾向分级”子任务时,整个pipeline要推倒重来——因为原始模型输出头是硬编码的,微调时连loss函数都要重写。后来我们彻底转向分层解耦架构,把AI系统拆成四个明确边界层:

  • 数据接入层:只负责从Kafka/MySQL/API拉取原始数据,不做任何清洗或转换,输出纯JSON Schema定义的raw_data;
  • 特征工程层:独立服务,输入raw_data,输出feature_vector(固定长度向量+metadata字典),所有transformer(如TF-IDF、Embedding lookup)必须可配置、可回滚;
  • 模型服务层:只接收feature_vector,输出prediction + confidence_score,模型本身必须支持热加载(不用重启服务就能切版本);
  • 业务编排层:用轻量级Python脚本或低代码引擎,把多个模型服务串成工作流(例如:先调风控模型→若score>0.8→再调反欺诈模型→合并结果)。

这个设计看似多此一举,但带来的收益极其实在。去年某银行项目,他们要求把原有信用卡逾期预测模型从XGBoost换成Transformer,我们只替换了模型服务层的Docker镜像,其他三层完全不动,上线耗时4小时(含压测),而隔壁团队因为把特征工程硬编码在训练脚本里,改模型花了11天。

2.2 工具链选型:不是越新越好,而是越“笨”越可靠

很多人以为AI Engineering要堆最新工具:LangChain做RAG、LlamaIndex建索引、Weights & Biases做实验追踪……但我们团队内部有个铁律:所有工具必须满足“三分钟原则”——新成员入职第三天,能独立用该工具完成一次完整实验记录、模型导出、服务部署。基于这个原则,我们淘汰了几个看似炫酷的工具:

  • 放弃MLflow:它的UI太重,启动要Java环境,且实验对比功能藏得太深。改用DVC + Git + 自研轻量Dashboard:每次git commit自动触发dvc repro,生成唯一run_id,dashboard只展示run_id、metrics.json、params.yaml三样东西,点击run_id直接跳转到对应commit。实测下来,新人查历史实验比MLflow快3倍。

  • 放弃FastAPI做模型服务:虽然它文档漂亮,但生产环境遇到高并发时,async/await的调试成本极高。改用Flask + Gunicorn + Nginx组合:Flask保持同步逻辑清晰,Gunicorn worker数=CPU核心数×2(经压测验证),Nginx做负载均衡和健康检查。有次线上突发流量,我们通过Nginx日志5分钟定位到是某个worker进程卡死,直接kill -9重启,而FastAPI的async traceback要翻7层栈。

  • 放弃Jupyter做开发:它太容易写出“不可复现”的代码——变量名随意、cell执行顺序混乱、magic命令满天飞。强制要求VS Code + Python Script + pytest:每个模块必须有test_*.py文件,运行pytest --cov=src/ 覆盖率低于85%不许提交。有次发现一个数据清洗函数在pandas 1.4.0和1.5.0行为不一致,正是靠test脚本提前捕获,避免了上线后数据漂移。

2.3 环境一致性:Docker不是可选项,是呼吸一样的存在

曾有个项目,算法同学本地训练AUC=0.92,部署到测试环境变成0.78。排查三天,最后发现是测试机装了OpenBLAS,而本地用的是Intel MKL,矩阵乘法精度差了1e-5,累积到softmax层就导致label分布偏移。从此我们立下死规矩:所有环节必须运行在Docker容器内,且基础镜像严格锁定版本。

我们的标准Dockerfile长这样:

FROM python:3.9.16-slim-bullseye # 安装系统级依赖(关键!) RUN apt-get update && apt-get install -y \ libfreetype6-dev \ libpng-dev \ libjpeg-dev \ && rm -rf /var/lib/apt/lists/* # 锁定Python依赖(关键!) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 复制代码(关键!) WORKDIR /app COPY . . # 验证环境(关键!) RUN python -c "import torch; print(f'PyTorch {torch.__version__}, CUDA: {torch.cuda.is_available()}')" CMD ["python", "main.py"]

注意三个“关键”:

  1. 系统库显式安装:pandas、numpy底层依赖libpng等,不同Linux发行版默认版本不同,必须显式声明;
  2. requirements.txt绝对锁定版本:torch==1.13.1+cu117,绝不写torch>=1.13;
  3. 启动前环境自检:用python -c验证CUDA是否可用,避免镜像构建成功但GPU不可用的尴尬。

这套流程让我们实现了“一次构建,处处运行”。客户现场服务器没有NVIDIA驱动?没关系,我们提供CPU版镜像,只需改一行env变量。他们要求用国产芯片?我们替换基础镜像为swr.cn-east-3.myhuaweicloud.com/ascend/pytorch:22.03.0-u20.04,其他代码0修改。

3. 核心细节解析:从数据到服务的12个生死关卡

3.1 数据接入层:别让脏数据毁掉整个Pipeline

数据接入层看似简单,实则是事故高发区。我们曾遇到一个电商项目,上游API返回的JSON里,price字段有时是数字199.0,有时是字符串"199.00",有时甚至是null。算法同学直接pd.read_json(),训练时没问题,但上线后服务随机报错。后来我们强制推行Schema First原则:

  • 所有原始数据必须附带JSON Schema定义,例如:
{ "type": "object", "properties": { "price": {"type": ["number", "null"]}, "title": {"type": "string"}, "category": {"type": "string", "enum": ["electronics", "clothing", "books"]} } }
  • 接入层代码必须用jsonschema.validate()校验,不通过的数据打上invalid_schema标签进入隔离区,绝不流入下游;
  • 对于null值,不自动填充,而是记录缺失率报表(每日邮件发送给数据负责人)。

这个改动让数据问题定位时间从平均4.2小时降到17分钟。更重要的是,它倒逼上游团队规范输出——因为他们收到的告警邮件里,精确指出是哪个字段、哪条记录、哪个时间点违反了Schema。

3.2 特征工程层:可复现的特征,才是真特征

特征工程常被当成“魔法”,但工程化要求它必须像代码一样可测试、可版本化。我们的做法是:每个特征变换器必须继承BaseTransformer类,并实现fit()、transform()、get_feature_names_out()三个方法。

以文本TF-IDF为例:

class TextTfidfTransformer(BaseTransformer): def __init__(self, max_features=10000, ngram_range=(1,2)): self.max_features = max_features self.ngram_range = ngram_range self.vectorizer_ = None # fit后才初始化 def fit(self, X, y=None): self.vectorizer_ = TfidfVectorizer( max_features=self.max_features, ngram_range=self.ngram_range, stop_words='english' ) self.vectorizer_.fit(X) return self def transform(self, X): return self.vectorizer_.transform(X).toarray() def get_feature_names_out(self): return self.vectorizer_.get_feature_names_out()

关键点在于:

  • fit()只接受X,不接受y,杜绝数据泄露;
  • transform()返回numpy array,不返回稀疏矩阵(避免下游处理不一致);
  • get_feature_names_out()确保特征名可追溯,后续能做SHAP解释;
  • 所有参数必须在__init__中声明,禁止在fit中动态修改。

我们还配套开发了FeatureValidator工具:输入训练集和测试集,自动检测特征分布偏移(KS检验)、缺失率变化、类别数量差异。有次发现测试集里“iPhone”品类占比从训练集的12%飙升到34%,立刻叫停上线——果然是上游运营活动导致的短期数据漂移。

3.3 模型服务层:让模型真正“活”在生产环境

模型服务不是把model.predict()包个API就完事。我们定义了模型服务的五个黄金指标,每个都必须有监控:

指标监控方式告警阈值应对措施
QPSPrometheus + nginx log parser< 日均值×0.5检查上游流量,非紧急
P99延迟OpenTelemetry trace采样> 200ms立即扩容worker或降级
OOM次数Docker stats + memory limit> 3次/小时强制重启并分析内存dump
Prediction driftEvidently AI实时检测PSI > 0.1触发数据质量告警
Confidence score分布Histogram上报中位数<0.6启动模型重训流程

特别强调OOM监控:我们给每个模型服务容器设置--memory=4g --memory-swap=4g,并用cgroup实时采集RSS。当OOM发生时,不是简单重启,而是自动触发gcore -o /tmp/core.$(date +%s) $(pidof python)生成core dump,再用pstack分析哪段代码在疯狂申请内存。有次定位到是某个embedding layer的torch.nn.Embedding未设置max_norm,导致梯度爆炸式增长。

3.4 业务编排层:用状态机代替if-else地狱

业务逻辑常写成层层嵌套的if-else:

if risk_score > 0.8: if fraud_score > 0.9: approve = False reason = "high_fraud" else: approve = True reason = "low_risk" else: if credit_score > 700: approve = True reason = "good_credit" else: approve = False reason = "low_credit"

这种代码难以测试、无法可视化、上线后改规则要全量回归。我们改用状态机编排:

from transitions import Machine class ApprovalWorkflow: states = ['init', 'risk_check', 'fraud_check', 'credit_check', 'approved', 'rejected'] def __init__(self): self.machine = Machine(model=self, states=states, initial='init') self.machine.add_transition('check_risk', 'init', 'risk_check', conditions=['risk_score_high']) self.machine.add_transition('check_fraud', 'risk_check', 'fraud_check', conditions=['fraud_score_high']) # ... 其他transition

所有业务规则变成可配置的JSON:

{ "transitions": [ {"source": "init", "dest": "risk_check", "conditions": "risk_score > 0.8"}, {"source": "risk_check", "dest": "fraud_check", "conditions": "fraud_score > 0.9"} ] }

前端可拖拽生成流程图,后端自动转成状态机代码。规则变更不再需要发版,只需更新JSON配置。

4. 实操过程:从空目录到可上线服务的完整流水线

4.1 初始化项目骨架:10分钟搭建可交付框架

我们用自研脚手架ai-engineer-init一键生成标准结构:

$ ai-engineer-init my-credit-risk --template=classification

生成目录:

my-credit-risk/ ├── data/ # 原始数据存放(git ignore) │ ├── raw/ # 接入层输出 │ └── processed/ # 特征工程层输出 ├── src/ │ ├── data/ # 数据接入层 │ │ ├── __init__.py │ │ └── kafka_reader.py # 从Kafka读取 │ ├── features/ # 特征工程层 │ │ ├── __init__.py │ │ └── credit_features.py │ ├── models/ # 模型服务层 │ │ ├── __init__.py │ │ └── xgboost_model.py │ └── workflow/ # 业务编排层 │ ├── __init__.py │ └── approval_flow.py ├── tests/ # 所有模块必须有对应test ├── docker/ # Docker相关文件 │ ├── Dockerfile │ └── docker-compose.yml ├── requirements.txt # 严格锁定版本 └── Makefile # 一键执行常用命令

关键设计:

  • Makefile里定义make train(训练)、make serve(本地服务)、make test(全量测试)、make deploy(部署到测试环境);
  • requirements.txt按环境分片:requirements/base.txt(基础依赖)、requirements/dev.txt(开发依赖)、requirements/prod.txt(生产依赖);
  • tests/目录与src/一一映射,test_data_kafka_reader.py必须覆盖src/data/kafka_reader.py所有分支。

4.2 数据接入实战:处理千万级订单流的稳定拉取

以电商订单数据为例,上游是Kafka topicorders_v2,每秒峰值2000条。我们用confluent-kafka而非kafka-python,因为前者用C实现,吞吐量高3倍。

src/data/kafka_reader.py核心逻辑:

from confluent_kafka import Consumer, TopicPartition import json import time class KafkaOrderReader: def __init__(self, bootstrap_servers, group_id): self.consumer = Consumer({ 'bootstrap.servers': bootstrap_servers, 'group.id': group_id, 'auto.offset.reset': 'latest', # 生产环境必须设为latest 'enable.auto.commit': False, # 手动commit,确保at-least-once }) def read_batch(self, topic, max_records=1000): self.consumer.subscribe([topic]) messages = [] for _ in range(max_records): msg = self.consumer.poll(timeout=1.0) # 关键:timeout必须设,防阻塞 if msg is None: break if msg.error(): raise Exception(f"Kafka error: {msg.error()}") # 解析JSON并校验Schema try: data = json.loads(msg.value().decode('utf-8')) validate_order_schema(data) # 调用Schema校验 messages.append(data) except (json.JSONDecodeError, ValidationError) as e: # 记录到dead_letter_queue send_to_dlq(msg.value(), str(e)) # 手动commit offset self.consumer.commit() return messages

实操心得:

  • auto.offset.reset必须设为latest,否则消费者重启会重放历史消息,导致重复计费;
  • poll(timeout=1.0)的timeout值经过压测:设太小(0.1s)CPU空转严重,设太大(5s)则消息延迟高;
  • validate_order_schema()用jsonschema库,Schema文件单独存放在schemas/order_v2.json,便于版本管理。

4.3 特征工程落地:应对实时特征计算的挑战

风控场景要求特征实时计算(如“用户最近1小时交易笔数”)。我们采用Lambda架构:批处理(Spark)计算历史统计特征,流处理(Flink)计算实时窗口特征,最终在服务层合并。

src/features/realtime_features.py示例:

from pyflink.datastream import StreamExecutionEnvironment from pyflink.table import StreamTableEnvironment def build_realtime_feature_stream(): env = StreamExecutionEnvironment.get_execution_environment() t_env = StreamTableEnvironment.create(env) # 从Kafka读取实时交易流 t_env.execute_sql(""" CREATE TABLE transactions ( user_id STRING, amount DOUBLE, event_time TIMESTAMP(3), WATERMARK FOR event_time AS event_time - INTERVAL '5' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'transactions', 'properties.bootstrap.servers' = 'kafka:9092', 'format' = 'json' ) """) # 计算最近1小时交易笔数 t_env.execute_sql(""" CREATE VIEW user_hourly_count AS SELECT user_id, COUNT(*) as tx_count_1h, MAX(event_time) as last_tx_time FROM transactions GROUP BY user_id, TUMBLING(event_time, INTERVAL '1' HOUR) """) return t_env

关键经验:

  • Watermark设置:event_time - INTERVAL '5' SECOND表示允许5秒乱序,超过则丢弃,避免无限等待;
  • TUMBLING窗口:比HOPPING窗口更易理解,且资源消耗更低;
  • View而非Table:避免物化中间结果,节省存储。

4.4 模型服务部署:Gunicorn + Flask的稳定配方

src/models/xgboost_model.py:

import joblib import numpy as np from flask import Flask, request, jsonify app = Flask(__name__) model = None feature_names = None @app.before_first_request def load_model(): global model, feature_names model = joblib.load('/app/model.pkl') feature_names = joblib.load('/app/feature_names.pkl') @app.route('/predict', methods=['POST']) def predict(): try: data = request.get_json() # 输入校验 if not isinstance(data, list): raise ValueError("Input must be list of feature vectors") X = np.array(data) if X.shape[1] != len(feature_names): raise ValueError(f"Expected {len(feature_names)} features, got {X.shape[1]}") # 预测 y_pred = model.predict(X) y_proba = model.predict_proba(X)[:, 1] return jsonify({ "predictions": y_pred.tolist(), "probabilities": y_proba.tolist() }) except Exception as e: app.logger.error(f"Prediction error: {str(e)}") return jsonify({"error": str(e)}), 400 if __name__ == '__main__': app.run(host='0.0.0.0:5000')

docker-compose.yml关键配置:

services: model-service: build: . ports: - "5000:5000" environment: - GUNICORN_CMD_ARGS=--workers=8 --worker-class=sync --timeout=120 --keep-alive=5 deploy: resources: limits: memory: 4G cpus: '2.0'

为什么选8个workers?计算公式:2 × CPU核心数 + 1。我们测试机是4核,所以2×4+1=9,但实际压测发现8个worker时P99延迟最低(worker过多导致上下文切换开销上升)。

5. 常见问题与排查技巧实录:那些凌晨三点的救命笔记

5.1 “模型训练时Loss Nan,但验证集Loss正常”——典型梯度爆炸陷阱

现象:训练loss在第3轮突然变成nan,但验证loss曲线平滑下降。
排查路径:

  1. git blame看最近谁改了optimizer——果然,同事把torch.optim.Adam的lr从1e-3改成1e-2;
  2. 用torch.autograd.set_detect_anomaly(True)开启异常检测,报错指向nn.Linear层输出;
  3. 在forward函数末尾加print(torch.isnan(x).any()),定位到某层输出nan;
  4. 检查该层输入:发现是某个归一化层(LayerNorm)输入包含inf,追查到上游embedding lookup时index越界(vocab_size=10000,但数据里出现idx=10005)。

解决方案:

  • embedding层加padding_idx=0,越界索引自动映射到padding向量;
  • 所有Linear层后加torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0);
  • 训练脚本开头加torch.backends.cudnn.enabled = False(禁用cudnn可复现性更高)。

提示:永远不要相信“训练loss下降就代表模型在学”,必须监控梯度范数(torch.norm(grad)),>1000就要告警。

5.2 “服务响应延迟突增,但CPU/Memory监控正常”——网络I/O黑洞

现象:P99延迟从80ms飙升到1200ms,Prometheus显示CPU使用率仅30%,内存占用稳定。
排查步骤:

  1. strace -p $(pgrep -f "gunicorn.*wsgi") -e trace=network抓网络系统调用;
  2. 发现大量connect()调用超时(connect(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("10.0.1.5")}, 16) = -1 ETIMEDOUT (Connection timed out));
  3. nslookup redis.internal发现DNS解析耗时2秒;
  4. 查/etc/resolv.conf,发现nameserver配置了不稳定的公共DNS。

根治方案:

  • Docker容器内/etc/resolv.conf强制指定内网DNS(nameserver 10.0.0.2);
  • 所有外部服务连接加超时:redis.Redis(host='redis.internal', socket_connect_timeout=1.0, socket_timeout=2.0);
  • 用dig @10.0.0.2 redis.internal定期探测DNS健康度。

5.3 “AB测试指标波动大,无法归因”——数据血缘断裂灾难

现象:新模型AUC提升0.015,但线上转化率下降2%。
根本原因:特征工程层某次更新,悄悄把“用户注册时长”从“天”单位改成“小时”单位,但业务层没同步调整阈值逻辑。
解决方案:

  • 强制所有特征输出带unit字段:{"value": 365, "unit": "days", "source": "mysql.users.created_at"};
  • 开发DataLineageTracker:每次特征transform自动记录输入schema、输出schema、代码commit hash;
  • AB测试平台接入lineage数据,点击任一指标,可下钻查看“该指标依赖哪些特征→这些特征由哪个版本代码生成→该代码commit diff”。

我们做了个真实案例:某次发现转化率下降,下钻发现依赖的user_activity_score特征,其上游click_log表在AB测试期间被上游团队增加了新字段,导致特征计算逻辑意外改变。立即回滚特征版本,2小时内恢复。

5.4 “模型服务偶尔503,重启后恢复”——Gunicorn worker静默死亡

现象:服务偶发503,docker logs无错误,ps aux发现worker进程消失。
深度排查:

  1. docker exec -it <container> ps aux确认worker进程确实没了;
  2. dmesg | grep -i "killed process"发现OOM killer日志:Out of memory: Kill process 12345 (python) score 894 or sacrifice child;
  3. cat /sys/fs/cgroup/memory/docker/<container-id>/memory.usage_in_bytes确认内存超限。

但为什么Prometheus没报警?因为cgroup内存统计有延迟。
终极方案:

  • 在Gunicorn配置里加--preload:让worker启动前先加载模型,避免fork后内存翻倍;
  • --max-requests=1000:每个worker处理1000请求后自动重启,防止内存泄漏累积;
  • --timeout=120:请求超时强制kill,避免长尾请求占满worker。

注意:--preload会增加启动时间,但换来的是内存稳定性——我们宁愿多花3秒启动,也不要半夜被503告警叫醒。

6. 经验沉淀:那些没写在文档里的硬核技巧

6.1 模型版本控制:Git LFS不是银弹,DVC才是生产级答案

很多人用Git LFS存模型文件,但遇到问题:

  • LFS push超时(模型>1GB);
  • git checkout时LFS文件下载失败,导致CI pipeline中断;
  • 无法关联模型与对应的数据版本、代码版本。

我们改用DVC(Data Version Control):

$ dvc init $ dvc remote add -d myremote s3://my-bucket/models $ dvc add models/xgb_v1.pkl $ git add models/xgb_v1.pkl.dvc .dvc/config $ git commit -m "add xgb_v1 model"

DVC优势:

  • 模型文件存在S3,Git只存元数据(.dvc文件),clone极快;
  • dvc repro自动根据dvc.yaml重新运行pipeline,保证模型与数据、代码强一致;
  • dvc metrics show直接查看模型指标,无需手动解析log。

实测:1.2GB模型,Git LFS clone耗时12分钟,DVC clone+pull耗时47秒。

6.2 日志结构化:别再用print,用structlog统一打点

传统print("model loaded")在生产环境毫无价值。我们用structlog:

import structlog logger = structlog.get_logger() def load_model(): logger.info("model.loading.start", model_name="xgb_v1", version="1.2.0") model = joblib.load("model.pkl") logger.info("model.loading.success", model_name="xgb_v1", version="1.2.0", feature_count=len(model.feature_names_)) return model

输出JSON日志:

{"event": "model.loading.success", "model_name": "xgb_v1", "version": "1.2.0", "feature_count": 42, "timestamp": "2023-08-15T14:22:33.123Z"}

配合ELK Stack,可直接查:“找出所有feature_count<30的模型加载事件”,或“统计model_name=xgb_v1的P99延迟分布”。

6.3 压测真实感:用Locust模拟业务流量,而非简单curl

很多团队用ab -n 10000 -c 100 http://localhost:5000/predict,这测的是单点HTTP性能,不是业务性能。我们用Locust写真实场景:

from locust import HttpUser, task, between import random class CreditRiskUser(HttpUser): wait_time = between(1, 3) # 模拟用户操作间隔 @task def predict_high_risk(self): # 模拟高风险用户请求(占30%流量) payload = [[random.uniform(0.1, 0.9) for _ in range(42)]] self.client.post("/predict", json=payload, name="predict.high_risk") @task def predict_low_risk(self): # 模拟低风险用户请求(占70%流量) payload = [[random.uniform(0.01, 0.3) for _ in range(42)]] self.client.post("/predict", json=payload, name="predict.low_risk")

关键点:

  • wait_time模拟真实用户思考时间;
  • 不同task权重反映真实流量分布;
  • name参数让Locust报表按业务场景分组,而非按URL。

压测结论比curl准确3倍:curl测出QPS=1200,Locust测出真实业务QPS=840(因高风险请求计算更重)。

6.4 团队协作:用GitHub Actions实现“提交即验证”

我们CI流程强制执行:

  1. make lint:black + flake8;
  2. make test:pytest覆盖率≥85%;
  3. make dvc-repro:用最新数据重新训练,验证模型指标不退化;
  4. make docker-build:构建镜像并docker run验证服务能启动;
  5. make security-scan:trivy扫描镜像CVE漏洞。

所有步骤失败,PR直接被block。有次trivy发现base镜像有CVE-2023-1234,CI自动comment:“发现高危漏洞,请升级python:3.9.16-slim-bullseye到3.9.18”,开发者点链接直达修复PR。

这套流程让团队从“人肉QA”变成“机器QA”,上线故障率下降76%。

我在实际项目中发现,AI Engineering from Scratch最大的障碍不是技术,而是心态——总想一步到位,结果处处是坑。真正的工程化,是把每个环节拆到不能再拆,然后用最笨的办法验证它。就像搭乐高,不追求最快拼完,而是确保每一块都咔哒一声严丝合缝。当你能在凌晨三点从容说出“这个问题出在特征工程层的Schema校验开关没打开”,而不是手忙脚乱翻日志时,你就真正掌握了AI Engineering的底层逻辑。

返回列表