1. 为什么我劝你别急着调包,先看一遍AI工程全链路
很多人一上来就问我:AI工程到底该怎么学?是不是跑通几个开源项目、看几篇教程就能上手?我做了这么多年AI应用开发和架构设计,最深的感受是:**ai-engineering-from-scratch(从零开始构建AI工程)**这条路,才是真正让人建立起完整技术认知的路线。
先把这个概念说清楚。AI工程不是单纯的算法调参,也不是写几个Python脚本调用现成模型。它覆盖的是从数据收集清洗、特征工程、模型训练与评估、到推理服务部署、监控反馈这一个完整的生命周期。而"from-scratch"的核心含义,是指抛开那些封装完善的框架,自己动手实现关键组件,理解AI系统底层到底发生了什么事。
这条路线解决的最大痛点,是"纸上谈兵"和"面试背题"两种典型问题。我见过太多人简历上写着熟悉TensorFlow、PyTorch,但真到了线上服务出故障、模型效果突然退化的时候,完全不知道从哪个环节排查。而如果你亲手写过反向传播、自己搭过训练循环、自己设计过特征管道,那么面对同样的故障,你心里是有一张完整地图的。
这篇文章适合三类读者:第一是刚入行、被各种高大上名词搞得一头雾水的初学者;第二是已经能跑通现成模型、但想深入理解系统内部机理的进阶者;第三是需要从零搭建AI服务、却不知道如何下手选择技术栈的工程师。我会按照一条从零开始的完整路径,把AI工程的骨架、血肉和雷区都摊开讲。
2. AI工程的整体设计思路:为什么"从零开始"这条路不可跳过
2.1 调包侠到真工程师之间,差的就是"从零实现"这一步
先摆一个残酷的事实:现在开源生态太丰富了,任何一个功能几乎都有现成的库可以调用。人脸识别?有。文本生成?有。推荐系统?也有。正因为这样,大量所谓的AI工程师其实只是"模型调用工程师"——他们做的事情就是把数据整理好,喂给现成的模型,然后调几个超参数看loss曲线。
这不是贬低这种工作方式。在实际生产中,复用成熟模型是效率最高的选择。但问题在于:**如果一个人只停留在调用层,他永远无法应对超出预期的情况。**比如你用的模型库内部改了接口,你完全不知道哪里报错;线上推理延迟突然变高,你分不清是预处理瓶颈还是模型计算瓶颈;模型在某个子群体上性能崩溃,你甚至不知道怎么定位数据分布的问题。
从零实现的意义就在于,它逼着你把每一层皮都剥开。当你自己写过张量相乘、写过分组卷积、写过BatchNorm的前向和反向,你会发现那些高大上的框架不过是一层方便调用的壳。从零搭一次,你的排错能力、架构理解深度、对性能瓶颈的嗅觉,会有一个数量级的提升。
2.2 从零开始的三种路线,对应三种技术目标
并不是所有人从零开始的方式都一样。我根据目标的不同,把AI工程从零开始分为三条路线,你可以对照自己的情况选:
**路线一:从数学原理到代码实现。**这条路线以理解算法本质为核心,不依赖任何深度学习框架,用NumPy甚至纯Python实现线性回归、逻辑回归、多层感知机、CNN的基础组件。目标是把反向传播、梯度下降、损失函数这些概念彻底搞懂。
**路线二:从单机训练到分布式系统。**这条路线以规模化为核心,在一台机器上能跑通模型之后,逐步走向多机多卡训练、数据并行、模型并行。目标是理解在大数据量、大模型场景下,AI工程为什么需要像分布式系统一样设计。
**路线三:从模型训练到全链路交付。**这条路线以工程化为核心,从模型训练扩展到数据处理、特征平台、模型版本管理、在线推理服务、A/B测试、监控告警。目标是构建一个生产环境可用的AI系统。
三条路线不是互斥的,一个合格的AI工程师最终要全部覆盖。但如果你自己动手从零走过一遍,你会发现它们其实高度耦合——分布式训练考虑的数据分片,和线上推理考虑的请求负载均衡,本质上是同一类工程思维。
2.3 我踩过的坑:抄捷径带来的连锁反应
我职业生涯早期犯过一个典型的"抄捷径"错误。当时要做一个文本分类系统,我直接用了某个开源NLP框架,一天就跑通了demo,感觉非常顺利。结果到了上线前的性能压测阶段,问题接连爆炸:训练时数据预处理用了一种方式,推理时sckit-learn的文本向量化器序列化版本不一致,导致线上推理结果完全错乱;框架内部对长短不一的句子自动做了padding,但我根本不知道,结果模型服务的内存暴涨。
那一次我花了一周时间排查问题,最后翻了框架源码才发现问题出在我没看懂的地方。就是从那时候起,我开始强迫自己"不黑盒"地看技术组件。后来我花了一个月时间,纯手写了一个带完整训练循环和推理接口的小型NLP模型系统,虽然效果不如成熟框架,但从数据流到梯度计算,中间每一环我都了如指掌。以后再遇到问题,我能一眼定位是哪一层出了事。这个习惯帮我在后续好几年里省了无数排查时间。
3. 核心细节解析:从零搭建AI工程的三个关键层
3.1 数据层:不要让脏数据成为你模型的"地雷"
AI工程的第一层不是算法,是数据。很多人从零开始时喜欢直接上模型,结果模型效果差得离谱,然后疯狂调参,却始终不知道症结在数据上。我总结了数据层必须刻意练习的几个细节:
**特征表示的一致性。**训练数据和推理数据必须走完全相同的处理管道。举个最常见的例子:训练时你删除了缺失值,用的是"按列均值填充",但如果推理服务代码里写的是"删除该行",结果就是两条完全不同的管道,线上表现必然失真。从零构建时,要把特征处理写成一个独立的模块,训练和推理共用一份代码,而不是各写一套。
**数据泄漏的隐蔽性。**这是新手最容易踩的雷。比如做时间序列预测,你做了特征的全局标准化,但"全局"指的是整个历史数据的均值方差,这就把未来的信息泄漏进了训练集。正确的做法是只在训练集上计算均值方差,然后用同样的参数去变换验证集和测试集。从零开始时,你要对每一处特征计算的时机保持警惕。
**样本权重的现实意义。**生产环境的数据天然是不平衡的——广告点击率数据里正样本可能只有千分之一。这时候单纯用准确率评估就是自欺欺人。从零搭建时,你最好自己实现过加权损失函数,才能理解class weight到底是怎样影响模型优化的。
3.2 模型层:理解反向传播,比记住十种激活函数更重要
模型层是AI工程的心脏。如果你问我要从零开始先死磕哪个知识点,我一定会说:反向传播。这不是因为反向传播本身很难,而是因为它是连接"损失"和"参数"的唯一桥梁。你只有真正亲手推导并实现过反向传播,才会明白学习率为什么不能盲目设置得太大、梯度消失为什么会导致深层网络训练不动、BatchNorm为什么能加速收敛。
我用一个生活化的类比来解释反向传播。想象你在一个伸手不见五指的山谷里,想知道下山的路。你看不清全局,只能用脚探探周围的坡度——哪个方向更陡,就往哪个方向迈一步。反向传播干的就是这件事:它在每一层算出"损失相对于参数的坡度",然后告诉你参数应该往哪个方向调整。模型训练就是一次一次地重复这个"探坡度→调整步伐"的过程。
从零实现反向传播时,我建议按这个顺序练手:
- 先写前向传播——把输入数据流经每一层的计算过程显式写出来;
- 求每个中间变量的梯度表达式——这一步需要你对链式法则足够熟悉;
- 实现参数更新——用最简单的SGD,不要一上来就Adam。
这里必须提醒一个实操要点:梯度检查(gradient check)。手写了反向传播之后,你要用数值微分来验证梯度算得对不对——对每个参数加一个极小量e,用(f(x+e) - f(x-e)) / 2e 近似梯度,和你解析算出来的梯度对比。一旦发现差异超过千分之一,赶紧纠正,否则模型训练会以"缓慢地不收敛"这种最让人头疼的方式失败。
3.3 工程层:模型训练完只是开始,部署监控才是真考验
AI工程走到工程化环节,才真正露出它"工程"的一面。训练环境是"实验室":你有完整的数据、充足的GPU、随时可以重启的训练脚本。但线上推理环境是"战地":请求流量忽高忽低、网络抖动、第三方依赖版本漂移、资源有限。
从零搭建部署环节,有几个核心组件缺一不可:
**模型序列化与版本注册。**你必须把模型、预处理管道、后处理逻辑打包成同一个版本。我见过因为模型文件更新了、但预处理代码没同步更新导致的线上效果暴跌事故。解决办法是在模型包内部记录一个schema版本号,加载时强制校验,不一致就拒绝启动。
**推理服务的接口设计。**推荐使用同步HTTP接口和异步消息队列两种模式。同步接口适合毫秒级响应的场景,比如在线推荐;异步模式适合离线批量打分,比如每日一次的用户分群。接口设计上要注意超时控制、背压机制、批量推理的并发窗口设置,不然流量高峰期会把服务打挂。
**监控可观测性。**不只是CPU、内存、GPU利用率这些基础设施指标,更重要的是模型行为指标:输入特征分布有没有漂移、预测结果的分布有没有异常、平均置信度有没有下降。这些指标才能在模型性能下降时帮你第一时间抓住信号,而不是等用户差评拿刀架脖子了才发现。
4. 实操过程:从零手写一个AI训练与推理全流程
4.1 环境与工具准备:Minimal yet Complete
从零开始并不意味着拒绝一切辅助工具。我们要做的是"不把核心逻辑黑盒化",但底层加速、数据操作这些成熟工具还是值得用。推荐环境如下:
# 建议使用Python 3.10+,NumPy为数值计算底座 python -m venv ai-engineering-venv source ai-engineering-venv/bin/activate pip install numpy pandas scikit-learn pip install fastapi uvicorn # 推理服务用,轻量且Python友好 pip install pytest # 测试必不可少不用PyTorch和TensorFlow是有意为之——在练手从零实现时,框架的自动求导会掩盖掉你本该理解的梯度细节。但在真实生产项目中,我还是会建议用PyTorch这类成熟框架,毕竟项目交付效率优先。这个实操环境的定位是"教学验证"。
4.2 手写一个带反向传播的两层网络
我们选一个最简单的任务:对sklearn自带的乳腺癌数据集做二分类,用NumPy手写一个两层全连接网络。
import numpy as np from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler data = load_breast_cancer() X, y = data.data, data.target.reshape(-1, 1) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 初始化参数 np.random.seed(42) n_features = X_train.shape[1] n_hidden = 32 n_output = 1 W1 = np.random.randn(n_features, n_hidden) * np.sqrt(2.0 / n_features) b1 = np.zeros((1, n_hidden)) W2 = np.random.randn(n_hidden, n_output) * np.sqrt(2.0 / n_hidden) b2 = np.zeros((1, n_output))先解释一下初始化为什么用0.01级别的小随机数:如果权重初始值太大,经过Sigmoid激活函数后,深层输出会直接落在饱和区,梯度趋近于零,训练就动弹不得。用np.sqrt(2.0 / n)这种比例缩放,是让每一层的输入方差和输出方差尽量一致,信息能流动得更顺畅。
前向传播和损失计算:
def sigmoid(x): return 1.0 / (1.0 + np.exp(-x)) def forward(X): z1 = X.dot(W1) + b1 a1 = np.tanh(z1) # 隐藏层激活 z2 = a1.dot(W2) + b2 a2 = sigmoid(z2) # 输出层 return a1, a2 # BCE损失 def compute_loss(y_true, y_pred): eps = 1e-8 return -np.mean(y_true * np.log(y_pred + eps) + (1 - y_true) * np.log(1 - y_pred + eps))反向传播。这里的关键是用链式法则从输出层往输入层逐层计算梯度:
def backward(X, y_true, a1, a2): m = X.shape[0] # 输出层梯度 dz2 = a2 - y_true dW2 = (1.0 / m) * a1.T.dot(dz2) db2 = (1.0 / m) * np.sum(dz2, axis=0, keepdims=True) # 隐藏层梯度 dz1 = dz2.dot(W2.T) * (1 - np.tanh(a1) ** 2) dW1 = (1.0 / m) * X.T.dot(dz1) db1 = (1.0 / m) * np.sum(dz1, axis=0, keepdims=True) return dW1, db1, dW2, db2训练循环:
learning_rate = 0.1 epochs = 500 for epoch in range(epochs): a1, a2 = forward(X_train) loss = compute_loss(y_train, a2) dW1, db1, dW2, db2 = backward(X_train, y_train, a1, a2) W1 -= learning_rate * dW1 b1 -= learning_rate * db1 W2 -= learning_rate * dW2 b2 -= learning_rate * db2 if epoch % 100 == 0: print(f"Epoch {epoch}, Loss: {loss:.4f}")跑完这个500轮训练,训练集的loss应该可以降到0.1左右。你可以试试把学习率改到1.0,大概率loss会震荡甚至发散。这就是"梯度太大,参数一步迈过头"的直观感受。
评估模型,看准确率和AUC,你会得到一个接近0.95以上的结果——二分类这个难度并不高,但重点是整个过程都在你的掌控中,每一行代码你都知道它为什么要存在。
4.3 打造一个可调用的推理服务
模型训练完之后,我把它暴露成一个HTTP API来做线上推理。这里重点不是FastAPI的用法,而是为什么线上推理服务和训练代码必须解耦,又必须严格一致。
import json import numpy as np from fastapi import FastAPI, Request app = FastAPI() class ModelService: def __init__(self, weights_path, scaler_params_path): params = np.load(weights_path) self.W1, self.b1, self.W2, self.b2 = params["W1"], params["b1"], params["W2"], params["b2"] scaler_params = json.load(open(scaler_params_path)) self.mean = np.array(scaler_params["mean"]) self.std = np.array(scaler_params["std"]) def preprocess(self, raw_features): # 必须与训练时的预处理逻辑完全一致:标准化 features = (np.array(raw_features) - self.mean) / (self.std + 1e-8) return features.reshape(1, -1) def predict(self, raw_features): x = self.preprocess(raw_features) z1 = x.dot(self.W1) + self.b1 a1 = np.tanh(z1) z2 = a1.dot(self.W2) + self.b2 prob = 1.0 / (1.0 + np.exp(-z2)) return float(prob[0][0]) service = ModelService("model_params.npz", "scaler_params.json") @app.post("/api/predict") async def predict(request: Request): payload = await request.json() prob = service.predict(payload["features"]) return {"probability": prob, "label": int(prob >= 0.5)}这个服务我故意做了一个"简版",但实际生产里你必须补上这几个点:
第一,超时与熔断。每个请求不能无限等待,建议在网关层设置300ms超时;如果模型服务出现大量超时错误,要有熔断机制自动降级,比如临时返回一个默认策略,而不是让请求堆积到雪崩。
第二,请求日志与追踪。每条请求要有trace_id,日志里记录特征hash值、返回概率、响应耗时。这能帮助你在模型翻车时回溯是哪些样本出了问题。
第三,版本灰度。新模型上线不要直接全量替换,先切5%的流量,观察A/B指标。这块我在后面的常见问题里再展开讲。
启动服务方式:
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2然后用curl测一下:
curl -X POST http://localhost:8000/api/predict \ -H "Content-Type: application/json" \ -d '{"features": [17.99, 10.38, 122.8, 1001.0, 0.1184, ...]}'如果返回的probability在0.9以上,说明整个链路通了。
4.4 一个完整的AI工程闭环:加上监控与反馈
训练好模型并部署上线,AI工程的工作只完成了不到一半。真正的闭环必须有监控和反馈,否则模型会随着数据分布的变化而腐烂。
我在这个简易系统上加了两个监控维度。第一个是输入数据分布监控:统计每个特征的均值和标准差,一天跑一次,与训练集的统计值做对比,如果某个特征的均值偏移超过两个标准差,就触发告警。第二个是预测分布监控:线上预测结果的均值如果从0.5慢慢漂移到0.4,可能说明模型正在对某些新出现的模式产生误判。
从零实现这些监控听起来不难,但真正麻烦的是"阈值怎么定"。阈值定太宽松,问题已经爆发了才报警;定太严格,运维同学会被毫秒级的抖动告警折磨到集体离职。我的经验是:先不设阈值,纯记录历史数据,跑两周后看统计分布,再基于"正常时期"的分布设定告警线。这比自己拍脑袋定阈值靠谱得多。
5. 常见问题与排查技巧实录
5.1 模型训练不收敛,从哪个环节查起
训练不收敛大概是AI工程师每天都可能遇到的难题。按我的排查顺序,从最廉价的检查开始逐层深入——
**第一步,检查损失函数和标签是否匹配。**我接手过一个项目,模型loss在0.69附近纹丝不动。0.69这个数字意味着什么?它是log(2),恰好是二分类中"完全随机猜测"的熵。这说明模型完全没有学到东西。排查后才发现,标签在数据管道里被意外反转了,正负样本编码反了。模型挖掘到的"规律"和真实标签完全是负相关,梯度更新方向根本是反的。这一步的教训是:不要急着调参,先用小批量数据做过拟合测试——用一个样本或十几个样本训练,如果loss都不能降到接近0,说明管道哪里肯定坏了。
**第二步,检查数值稳定性。**如果loss输出中出现NaN,多半是计算过程中出现了数值上溢或下溢。比如计算Softmax时直接做exp大数,结果爆炸。解决办法是减去最大值后再做指数运算。另外,log(0)也会产生-inf,务必在log里加一个小epsilon。我自己吃过这个亏,所以代码里一直保留加epsilon的习惯。
**第三步,检查梯度与学习率匹配度。**如果loss在下降但速度极慢,很可能是学习率过小;如果loss震荡剧烈或直接发散,学习率大概率过大了。我习惯先打印梯度的范数和参数范数,如果梯度范数比参数范数大好几个数量级,那就需要梯度裁剪或者干脆调低学习率。这些"检查手感"来自于一次次从零搭训练循环的积累。
5.2 训练集表现好但测试集崩了,是过拟合还是数据泄漏
这个问题出现的概率极高。我的排查方法论是:**先找泄漏,再谈正则化。**因为数据泄漏导致的"测试集崩掉",是比单纯过拟合更隐蔽也更需要警惕的问题。
数据泄漏怎么查?我一般从"测试集是否接触过训练集的信息"这个角度逆向审查。比如:数据清洗时你用了全量数据来计算缺失值的填充均值,这意味着测试集的信息已经渗透进了训练流程;做特征工程时,你用了未来数据来计算滚动窗口统计量;做样本划分时,你没有按时间切分,而是随机切分,导致同一用户的不同行为被分到了训练集和测试集两侧。这些都是泄漏的经典场景。
如果排除了泄漏,确实是过拟合,那更通用的解决方案依次是:增加数据量(或者数据增强)、降低模型复杂度、加大正则化强度、早停。我经常跟团队说一句话:能去赚数据的人不要逼自己调参,数据量上去了,很多问题自动消失。
5.3 线上推理延迟太高,怎么定位瓶颈
线上性能问题同样有一套固定的排查顺序。先从最顶层看整体链路耗时分布:请求进来后,预处理耗时多少、模型推理耗时多少、后处理耗时多少。最简单的方法是加日志打点,记录每个阶段的时间戳。然后再用profile工具做细粒度分析。
在我自己的项目里,预处理阶段最常见的瓶颈是特征拼接和向量化。有些工程师习惯用Python的for循环去遍历每条样本做特征变换,数据量一大就卡。正确做法是尽量使用NumPy的向量化运算,一次处理一个batch。模型推理阶段最常见的瓶颈则是模型参数量过大、输入序列过长。框架层面的优化手段包括量化、剪枝、批处理合并(把多个请求合并成一个batch推理,大幅提升GPU利用率)。
这里分享一个很实用的技巧:**用延迟占比图快速缩小排查范围。**比如总延迟100ms,预处理占80ms,那你就算把模型推理优化到极致也只能改善20ms;反之如果模型推理占80ms,你就该集中火力优化模型。很多人一遇到延迟高就想着换GPU、加机器,却往往忽视了真正的瓶颈其实在数据加载那一步。
5.4 模型上线后效果不如离线评测,差在哪里
这是从AI工程"从零到生产"最经典的一道坎。离线AUC 0.92,线上却表现平庸,甚至不如规则系统。导致这种差异的核心原因通常有三类:
第一,**训练与推理的特征环境不一致。**离线训练时你拥有的是完整的历史特征,比如"用户点击量";在线推理时,这个特征在当前时刻还没产生,你只能拿到"截至目前为止的点击量"。如果直接用"点击量"而没做时间戳裁剪,线上特征的值就和训练时完全不同。
第二,**延迟的反馈信号。**很多标签有滞后性——比如某个转化事件要七天之后才产生。离线训练时你用的标签是全量完成的,而线上评估时,样本在短期内根本看不出转化结果。所以取数时要把"观察窗口"设置得足够长,否则离线指标会虚高。
第三,**数据分布漂移。**训练数据来自上一个季度,而线上用户行为已经被推荐系统本身改变。这里的解决方案只能是持续监控数据漂移,并用周期性重训练来对抗漂移。不要指望一个模型能一直打天下,这在AI工程里从来都不成立。
6. 我是如何组织一个从零开始项目的学习顺序的
前面讲了很多具体的实现细节和技术点,最后我想系统性地聊一下:一个完全从零开始的AI工程项目,应该按怎样的顺序推进,才不至于中途放弃或迷失方向。
我个人的做法是把项目拆成四个里程碑,每个里程碑都有独立可交付的成果物。
**里程碑一:可复现的模型实验环境。**先把环境搭建好,包括依赖管理、数据下载与预处理脚本、模型训练的骨架代码。这个里程碑的交付标准是:别人拿到你的项目仓库,跑一条命令就能把模型训练出来,损失曲线一致。这一阶段最难的是数据预处理流程的规范化,很多新手都在这一步被真实数据的脏乱吓退了。
**里程碑二:可解释的模型核心实现。**在这个阶段,你不能依赖任何自动求导框架,亲手实现前向、反向、梯度更新和训练循环。交付标准是:你能够向任何一个人讲清楚你的模型每一层的输入输出维度、每一步梯度怎么算出来的、loss为什么这个数值是合理的。我从这个里程碑里收获最大的不是实现了神经网络,而是建立了"训练失败时怎么思考"的直觉。
**里程碑三:可部署的推理服务。**把训练好的模型包装成API服务。这个阶段你才会认真思考模型文件管理、并发处理、超时控制、版本兼容这些问题。交付标准是:写一个压测脚本,在50并发下服务稳定运行且P99延迟低于200ms。很多模型只能活在Jupyter Notebook里,就是从这步开始真正变成产品的一部分。
**里程碑四:可观测的模型监控。**为线上服务加上指标监控、日志追踪、版本回滚机制。交付标准是:模拟一次数据漂移事件,你能在30分钟之内通过监控指标定位到是哪个特征发生了漂移,并做出策略调整。这个能力在面试和实际工作中都非常加分。
这四个里程碑一路走下来,你完成的已经不是"一个模型项目",而是一个完整的AI工程闭环。
7. 个人经验与心得:从零开始教会了我什么
文章写到这里,技术上的东西基本讲透了。最后分享一些我个人的体感和心得。
从零开始构建AI工程这条路,最大的价值不在于你最终产出的那个模型有多精准,而在于它给了你一张完整的认知地图。之后你再去用PyTorch也好、用TensorFlow也好、用各种AutoML平台也好,你都知道这些工具替你省掉了哪一步、又把哪一步隐藏起来了。这种"框架背后的透明感",让我在实际工作中很少被"黑盒问题"卡住。
另外一个体会是,从零开始不等于拒绝一切现成工具。很多人把"from-scratch"理解成所有代码都自己写,连JSON解析都要手搓,这是没有必要的。真正的工程智慧是区分"核心依赖"和"辅助工具"——凡是你必须深入理解的环节,你亲自动手;凡是与业务逻辑无关的通用能力,站在巨人的肩膀上。
最后再分享一个小技巧:不管做任何AI项目,在开始动手前先花十五分钟画一条数据流图——从原始数据、特征处理、模型输入、预测输出、业务决策,到最终的反馈回流。这条数据流图就是你的项目地图。绝大多数工程问题的根源,都可以在这张图上找到——是不是某条边断掉了,是不是某个节点数据格式不匹配,是不是反馈环路没有闭合。养成这个习惯,比记住任何框架API都有用。
从零开始的路确实要花更多时间,但那些亲手踩过的坑、亲手修复过的bug、亲手调通的训练循环,会变成你技术判断力的真正底子。希望这篇分享能给你一些参考,去做那个"知其然也知其所以然"的AI工程师。