news 2026/10/2 8:20:24

从零搭建AI工程体系:数据、特征、模型、服务全链路实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零搭建AI工程体系:数据、特征、模型、服务全链路实战

1. 从零搭建AI工程体系,为什么我劝你别一上来就调包

"ai-engineering-from-scratch"这个标题,第一次看到的时候我愣了一下。不是因为陌生,恰恰相反,是因为它戳中了我这几年带团队、做项目最痛的一个点:太多人把AI工程等同于"会调API"或者"会跑个demo",但真正落到生产环境里,从数据管道到模型服务、从特征管理到监控告警,中间那一大段"工程化"的活儿,几乎没人系统讲过。

我自己是从传统后端转过来的,最早做推荐系统那会儿,踩过的坑现在想起来还肉疼。模型离线AUC 0.85,上线之后线上效果直接腰斩;特征在训练脚本里算一遍、在服务端又算一遍,两边逻辑对不上,排查了整整三天;模型文件从实验室的pkl换成线上要用的格式,光是序列化兼容问题就折腾了一周。这些事儿,没有一件是"算法"问题,全是工程问题。

所以这篇内容,我想聊的不是某个具体模型怎么训,而是从零开始把一套AI工程体系搭起来这件事本身。它适合谁看?如果你是刚入行的算法工程师,只会写notebook不会写服务,这篇能帮你补上工程这一课;如果你是后端或者数据工程师,想转AI方向但不知道从哪下手,这篇能给你一条清晰的路径;如果你是技术负责人,正发愁团队里"模型能跑但上不了线",这篇里的很多坑你应该都似曾相识。

核心关键词就一个:ai-engineering-from-scratch,从零构建AI工程能力。我会按"整体设计思路 → 核心模块拆解 → 实操落地 → 问题排查"这条线来讲,每一块都尽量给到能直接抄作业的细节,而不是泛泛而谈。

2. 整体架构怎么设计:先想清楚数据流,再谈技术选型

2.1 为什么"从零"不等于"从轮子造起"

很多人对"from scratch"有个误解,以为是要自己手写一个TensorFlow。这就跑偏了。AI工程里的"从零",指的是从零搭建一套完整的工程链路,而不是从零实现底层框架。这个区别特别重要,因为它直接决定了你的技术选型策略。

我的原则是:底层框架和成熟组件直接用,业务链路的胶水层自己写。什么意思?PyTorch、TensorFlow、Spark、Kafka这些,人家几万人维护,你没必要重造;但数据怎么从Kafka流到特征库、特征怎么喂给模型、模型输出怎么回写业务库,这一整条链路的编排逻辑,必须你自己掌控,因为这是你业务的命脉,用现成的"一站式平台"往往会被绑死。

我见过太多团队一上来就买了个"AI中台",结果发现业务需求稍微一变,平台改不动,最后又退回自己搭。所以从零开始,反而是最灵活的路子。

2.2 一条完整的AI工程链路长什么样

抛开具体技术,任何一套AI工程体系,本质上都是这么一条数据流:

原始数据 → 数据清洗与校验 → 特征工程 → 特征存储 → 模型训练 → 模型评估 → 模型注册 → 在线服务 → 监控与回流

这条链路里,离线部分(清洗、特征、训练、评估)和在线部分(服务、监控)是两套完全不同的技术栈,但它们必须共享同一份"特征定义"和"模型契约",否则就会出现我前面说的"线上线下不一致"。

我一般会把整个体系拆成四个层次来设计:

层次职责典型组件关键产出
数据层采集、清洗、校验Kafka、Spark、Great Expectations干净的结构化数据
特征层特征计算、存储、复用Spark、Feast、Redis特征视图与特征服务
模型层训练、评估、注册PyTorch、MLflow、Airflow可追溯的模型版本
服务层在线推理、监控、回流FastAPI、Triton、Prometheus稳定的推理接口

这四层里,特征层是最容易被忽视、但最要命的一层。很多团队数据层和模型层都做得不错,唯独特征层是"训练脚本里现算",结果就是线上线下两套逻辑,早晚出事。

2.3 技术选型背后的取舍逻辑

选型这事儿,我不喜欢给"标准答案",因为每个团队的规模、人力、业务节奏都不一样。但我可以给你几条我踩过坑之后总结的判断标准。

第一,优先选"能被替换"的组件。比如特征存储,Feast和自研Redis方案,我倾向先用Feast,因为它有标准接口,将来想换底层存储不用改业务代码。反过来,如果你一上来就深度绑定某个商业平台,迁移成本会高到你想哭。

第二,离线在线尽量用同一套计算引擎。如果你的离线特征用Spark算,在线特征用Python算,那两套逻辑必然会有细微差异(浮点精度、空值处理、时间窗口边界)。我的做法是:核心特征用同一份SQL或同一份Python函数定义,离线批量跑、在线单条跑,从源头保证一致性。

第三,别过早引入"重"组件。团队就三五个人,业务量也不大,非要上Kubernetes + Triton + 特征平台全家桶,运维成本能把你拖垮。我一般建议:日请求量百万级以下,FastAPI + Redis + 单机模型服务就够了,等真扛不住了再升级。

3. 核心模块拆解:数据、特征、模型、服务四件套

3.1 数据层:校验比清洗更重要

数据层最容易犯的错,是把精力全花在"清洗"上,却忽略了"校验"。清洗是修数据,校验是发现数据什么时候坏了。生产环境里,数据源突然改字段、上游任务延迟、编码格式变化,这些都会让你的模型悄悄失效,而你还蒙在鼓里。

我的做法是在数据入口加一道数据契约校验。用Great Expectations或者自己写一套简单的规则引擎,对每一批进来的数据做检查:

# 一个极简的数据校验示例,实际项目里我会用GE或pandera import pandas as pd def validate_batch(df: pd.DataFrame) -> list: errors = [] # 1. 关键字段不能为空 for col in ["user_id", "item_id", "event_time"]: if df[col].isnull().any(): errors.append(f"{col} 存在空值") # 2. 数值范围检查 if (df["price"] < 0).any(): errors.append("price 出现负值") # 3. 时间不能是未来 if (pd.to_datetime(df["event_time"]) > pd.Timestamp.now()).any(): errors.append("event_time 出现未来时间") return errors

这段代码看着简单,但它救过我很多次。有一次上游把价格单位从"元"改成了"分",数值直接放大100倍,就是靠范围校验第一时间发现的。校验规则要跟着业务走,每加一个字段就加一条规则,别嫌麻烦。

注意:校验失败不要直接丢弃数据,而是告警 + 落盘到隔离区。丢弃会让你丢失现场,事后根本查不出问题出在哪。

3.2 特征层:一致性是命根子

特征层的核心矛盾就一个:离线算的特征和在线算的特征,必须一模一样。这个"一模一样"包括:同样的输入、同样的计算逻辑、同样的空值处理、同样的时间窗口。

我推荐的做法是特征定义即代码。把每个特征写成一个纯函数,离线批量调用、在线单条调用,共用同一份实现:

# features/user_features.py def avg_order_amount_7d(orders: list) -> float: """近7天平均订单金额,离线和在线共用""" if not orders: return 0.0 # 空值统一返回0,别用None,否则线上线下行为不一致 valid = [o["amount"] for o in orders if o["amount"] is not None] if not valid: return 0.0 return sum(valid) / len(valid)

离线跑的时候,你把一个用户近7天的订单列表传进去;在线跑的时候,你从Redis里取出这个列表再传进去。逻辑只有一份,就不会有偏差。

特征存储这块,小团队我建议直接用Redis + 一张MySQL元数据表。Redis存特征值(key是feature:user_id:feature_name),MySQL存特征的元信息(谁定义的、什么类型、更新频率)。等特征数量上千、团队多人协作了,再考虑上Feast这类专业平台。

3.3 模型层:可追溯比高精度更重要

模型层我见过最混乱的场景是:线上跑着一个模型,但没人知道它是哪天训的、用的哪份数据、参数是什么。出了问题想回滚,发现旧模型文件找不到了。这就是典型的缺乏模型管理。

我的铁律是:每一个上线的模型,都必须能回答三个问题——用哪份数据训的、用什么代码训的、评估指标是多少。这三个问题答不上来,就不许上线。

实现上,MLflow是性价比最高的选择。它帮你记录每次实验的参数、指标、产物,还能做模型注册。一个典型的训练脚本长这样:

import mlflow import mlflow.pytorch mlflow.set_experiment("recommendation_model") with mlflow.start_run(): # 记录超参数 mlflow.log_params({"lr": 0.001, "batch_size": 256, "epochs": 10}) # 记录数据版本 mlflow.log_param("data_version", "2024-01-15") # 训练... # 记录指标 mlflow.log_metrics({"auc": 0.85, "logloss": 0.32}) # 保存模型 mlflow.pytorch.log_model(model, "model")

跑完之后,MLflow的UI里能清楚看到每次实验的对比。模型注册环节,我会给通过评估的模型打上staging标签,人工验证后再转production,绝不自动上线。

3.4 服务层:接口设计决定后期维护成本

服务层是模型和业务之间的桥梁,它的设计好坏,直接决定你后期改需求时是"改一行"还是"改一周"。

我的接口设计原则有三条:

第一,请求和响应都用明确的schema。别用裸dict,用Pydantic定义清楚:

from pydantic import BaseModel from typing import List class PredictRequest(BaseModel): user_id: str item_ids: List[str] context: dict = {} class PredictResponse(BaseModel): scores: List[float] model_version: str

第二,响应里必须带模型版本号。这样出问题时你能立刻定位是哪个版本,也方便做A/B测试。

第三,推理逻辑和业务逻辑分离。服务层只负责"取特征 → 调模型 → 返回分数",至于这个分数怎么用、要不要过滤、要不要排序,交给业务层。这样模型迭代时,业务代码不用动。

4. 实操落地:从零跑通一条最小可用链路

4.1 环境准备与目录结构

理论讲再多,不如跑一遍。我给你一条最小可用链路,一台4核8G的机器就能跑起来。先看目录结构,这个结构我用了好几年,清晰且好扩展:

ai-engineering/ ├── data/ # 数据相关 │ ├── ingest.py # 数据接入 │ └── validate.py # 数据校验 ├── features/ # 特征定义(离线在线共用) │ └── user_features.py ├── training/ # 训练 │ └── train.py ├── serving/ # 服务 │ ├── app.py │ └── feature_client.py ├── configs/ # 配置 │ └── config.yaml └── requirements.txt

依赖就几个核心的:pandas、scikit-learn、fastapi、uvicorn、redis、mlflow、pyyaml。别一上来就装一堆,够用就行。

4.2 数据接入与校验的实操

假设我们的场景是"用户下单预测",数据从CSV来(真实场景换成Kafka消费即可)。接入脚本的核心是幂等——同一批数据重复跑,结果不能变。

# data/ingest.py import pandas as pd from data.validate import validate_batch def ingest(file_path: str) -> pd.DataFrame: df = pd.read_csv(file_path) # 去重,保证幂等 df = df.drop_duplicates(subset=["order_id"]) # 校验 errors = validate_batch(df) if errors: # 落盘隔离区,别丢 df.to_parquet(f"data/quarantine/{pd.Timestamp.now().date()}.parquet") raise ValueError(f"数据校验失败: {errors}") return df

这里有个细节:去重放在校验之前。因为重复数据本身可能触发校验规则(比如同一订单出现两次),先去重能减少误报。这个顺序我调过好几次才定下来。

4.3 特征计算与存储的实操

特征计算我坚持"一份逻辑两处调用"。先定义特征函数,然后写两个入口:一个批量算(离线),一个单条算(在线)。

# features/user_features.py def compute_user_features(orders: list) -> dict: """输入用户订单列表,输出特征字典""" amounts = [o["amount"] for o in orders if o.get("amount") is not None] return { "order_count_7d": len(orders), "avg_amount_7d": sum(amounts) / len(amounts) if amounts else 0.0, "max_amount_7d": max(amounts) if amounts else 0.0, }

离线批量算完,写进Redis:

import redis, json r = redis.Redis(host="localhost", port=6379) def save_features(user_id: str, features: dict): r.set(f"feature:{user_id}", json.dumps(features), ex=86400) # 24小时过期

在线取的时候,直接从Redis读,读不到就返回默认值(千万别现场去数据库捞,会拖垮服务):

def get_features(user_id: str) -> dict: raw = r.get(f"feature:{user_id}") if raw is None: return {"order_count_7d": 0, "avg_amount_7d": 0.0, "max_amount_7d": 0.0} return json.loads(raw)

4.4 训练与模型注册的实操

训练脚本的关键是可复现。固定随机种子、记录数据版本、记录代码commit,这三样缺一不可。

# training/train.py import numpy as np, mlflow from sklearn.ensemble import GradientBoostingClassifier from sklearn.metrics import roc_auc_score np.random.seed(42) # 固定种子 def train(X_train, y_train, X_val, y_val): with mlflow.start_run(): mlflow.log_param("data_version", "2024-01-15") model = GradientBoostingClassifier(n_estimators=100, max_depth=5) model.fit(X_train, y_train) auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1]) mlflow.log_metric("auc", auc) mlflow.sklearn.log_model(model, "model") return model, auc

跑完之后,去MLflow UI里看结果。AUC达标(比如>0.75)才进入注册环节,注册时打上版本号,人工确认后转生产。

4.5 在线服务的实操

服务层用FastAPI,核心是启动时加载模型,请求时只做推理,别每次请求都重新加载模型。

# serving/app.py from fastapi import FastAPI from pydantic import BaseModel from typing import List import mlflow.pyfunc app = FastAPI() model = None @app.on_event("startup") def load_model(): global model # 从模型注册中心加载生产版本 model = mlflow.pyfunc.load_model("models:/recommendation_model/production") class PredictRequest(BaseModel): user_id: str item_ids: List[str] @app.post("/predict") def predict(req: PredictRequest): features = get_features(req.user_id) # 构造模型输入... scores = model.predict(...) return {"scores": scores.tolist(), "model_version": "v1.2.0"}

启动命令就一句:uvicorn serving.app:app --host 0.0.0.0 --port 8000。压测一下,单机QPS跑到几百没问题,小业务完全够用。

5. 常见问题与排查技巧实录

5.1 线上线下效果不一致,怎么查

这是最高频的问题,没有之一。排查思路我总结成一张表:

排查方向具体检查点常见原因
特征一致性同一用户离线在线特征值是否相同空值处理不同、时间窗口边界不同
数据分布线上请求的特征分布 vs 训练集分布训练数据过时、线上新用户多
模型加载线上加载的是不是最新版本版本号没更新、缓存未刷新
预处理归一化/编码逻辑是否一致离线用了fit的参数,在线重新fit了

我的排查顺序是:先比对特征,再比对分布,最后查模型版本。90%的问题出在特征上。具体做法是:挑10个线上请求,把它们的特征值dump出来,和离线算的对比,一眼就能看出差异。

5.2 模型服务内存泄漏怎么办

服务跑几天内存就涨满,重启才好,这是典型的内存泄漏。常见原因有三个:

  • 模型对象被反复加载:检查是不是每次请求都load_model了,必须放startup里。
  • 缓存无上限:本地缓存(比如LRU)没设maxsize,请求多了就爆。
  • 特征对象没释放:大对象用完及时del,别指望GC。

我一般会在服务里加一个/health接口,返回当前内存占用,配合Prometheus做监控,涨到阈值就告警。

5.3 特征更新延迟导致预测偏差

特征不是实时更新的,比如你每小时批量刷一次Redis,那这一小时内的新行为就反映不到特征里。这个延迟对某些业务(比如实时推荐)是致命的。

解决办法有两个:一是缩短批量刷新周期(比如改成5分钟一次),二是对关键特征做实时更新(用户下单后立刻更新该用户的特征)。我一般对"最近一次行为"这类特征做实时更新,对"7天统计"这类做批量更新,两者结合。

提示:实时更新特征时,注意并发写问题。多个请求同时更新同一用户特征,要用Redis的原子操作或者加锁,否则会丢更新。

5.4 模型回滚的正确姿势

模型上线后发现效果不好,要回滚。这时候如果没做版本管理,就只能干瞪眼。我的做法是:每次上线都保留上一个生产版本,回滚就是改一个指针。

在MLflow里,把production标签从新版本移回旧版本,服务端监听标签变化(或者定时拉取),自动加载旧模型。整个过程不超过1分钟。千万别用"重新训练一个旧模型"来回滚,那既慢又不可靠。

5.5 几个我踩过的独家坑

坑一:时间窗口用"自然日"还是"滚动24小时"。离线训练时我用的是自然日(今天0点到昨天0点),在线却用了滚动24小时,结果特征对不上。后来统一成滚动窗口,问题消失。时间窗口的定义必须写进特征文档,全团队统一。

坑二:浮点数精度。离线用float64算,在线用float32存Redis,取出来再算,结果有微小差异。对大多数模型无所谓,但对某些敏感模型(比如排序分数卡阈值)就会出问题。统一用float32,或者干脆存字符串。

坑三:空值语义。离线把"没有订单"处理成0,在线却处理成None,模型输入直接报错。空值的处理方式必须和特征定义绑死,写在一个函数里。

6. 后续可以怎么扩展这套体系

跑通最小链路之后,这套体系还有很多可以往上加的东西。我按优先级给你排个序,你可以根据自己的业务节奏来。

第一优先级:加监控。模型上线不是终点,是起点。至少要监控三个指标:请求量、延迟、预测分布。预测分布尤其重要,如果某天分布突然偏移,说明上游数据或业务变了,模型可能已经失效。用Prometheus + Grafana,半天就能搭起来。

第二优先级:加A/B测试。新模型别直接全量,先切5%流量,对比核心业务指标。A/B测试的框架可以很简单:请求进来时按user_id哈希分流,不同流量走不同模型版本,结果分别打点。

第三优先级:加自动化训练。用Airflow或者简单的cron,定期(比如每天)拉新数据、重训模型、自动评估,达标就推到staging。这一步能把你从"手动炼丹"里解放出来。

第四优先级:特征平台化。当特征数量超过几百个、多个团队共用时,就该上Feast这类专业平台了。但记住,平台是为人服务的,别为了平台而平台,小团队用Redis + 元数据表完全够用很久。

我个人在实际操作中的体会是:AI工程最难的不是某个技术点,而是"一致性"和"可追溯"这两件事。把这两件事做好,你的系统就能稳定运行;做不好,再牛的模型也白搭。从零搭建的过程,其实就是不断和"不一致"作斗争的过程。每次你觉得"这里应该没问题吧",往往就是问题所在。多写校验、多留日志、多做对比,这三件事看起来笨,但最管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 8:19:56

AI编程助手Skills完全指南:从安装到自写技能

最近好几个朋友都在问同一件事&#xff1a;AI 编程助手里的 skills 到底怎么装、怎么用、怎么自己写。老实说&#xff0c;我最早也被这个概念搞得有点晕——本来以为又是一堆提示词模板&#xff0c;真正用起来才发现&#xff0c;它比提示词系统得多&#xff0c;也实用得多。这篇…

作者头像 李华
网站建设 2026/10/2 8:18:35

GoWVP 开发者鉴权方案指南

GoWVP 开发者鉴权方案指南 本文档面向系统集成开发者与前端开发者&#xff0c;详细介绍 GoWVP&#xff08;OWL&#xff09;提供的三种接口鉴权机制、设计原理、配置方式及调用示例。1. 鉴权方案概览 GoWVP 支持以下三种鉴权模式&#xff0c;三种模式在网关中间件中无缝协同、层…

作者头像 李华
网站建设 2026/10/2 8:18:17

012-0401-Mysql-基础DDL、DML

MySQL概述 基本概念 数据库是什么? 数据库(DB:Database):存储数据的地方。 为什么要用数据库? 应用程序产生的数据是在内存中的,如果程序退出或者是断电了,则数据就会消失。使用数据库是为了能够永久保存数据。当然这里指的是非内存数据库。 用普通文件存储行不行…

作者头像 李华