☰
AI工程从零实战:从手写神经网络到RAG服务部署
2026/10/1 11:42:24 网站建设 项目流程

AI engineering from scratch,这不是一个现成的课程链接,而是一种把自己扔进“炼钢炉”里的方式:不借助任何封装好的低代码平台,从Python语法、矩阵运算、损失函数开始,一路亲手搭出能跑的训练脚本、能回答问题的RAG应用、能对接业务系统的Agent服务。我给自己定了这个项目,目标只有一个——不靠“调包侠”式的熟练度,而是把AI工程里每一层的关键环节都亲手过一遍。这篇文章不是学习笔记的汇总,而是把这个项目的完整思路、技术选型、实操过程和踩坑记录摊开给你看,适合准备转型AI工程方向的开发者,也适合团队里想从“会用”走向“能造”的工程师参考。

1. 先搞清楚:“AI工程from scratch”到底在做什么

很多人在听到“AI工程”这四个字时,第一反应是“这不就是训练模型嘛”。但真正上手之后会发现,训练模型只是整条流水线里的一小段,好比盖房子时浇混凝土,之前要画图纸、打地基、绑钢筋,之后还要装修、通水电、做验收。AI工程的核心,是把模型当作一个需要持续维护的软件系统来对待,而不是把它当作一次性的实验脚本。

1.1 AI工程不是算法岗,是系统岗

如果你去看各大公司对AI工程师的岗位要求,会发现一个很有意思的现象:除了要求你懂机器学习、深度学习,还有一大堆工程化关键词——数据管道、模型服务化、性能调优、监控告警、CI/CD、容器化部署。也就是说,AI工程更接近“系统工程师 + 算法基础”的复合角色。

我见过不少算法功底不错的人,模型在Notebook里跑得飞起,一上生产环境就翻车:接口延迟高到不可接受、显存泄漏没人发现、数据分布一变效果立刻崩。反过来,我也见过纯后端出身的同事,工程能力很强,但面对loss曲线不下降、梯度爆炸这类问题时,缺乏从数学原理倒推问题的能力,只能靠瞎试参数。这个项目从头开始做的意义,恰恰是同时补齐这两块短板。

1.2 为什么非要从零开始

有人会问:现在工具这么成熟,HuggingFace拿个预训练模型下来、LangChain拼两个链、再套个Streamlit,一个AI应用半天就能上线,为什么还要从梯度下降手写起?我的回答是:为了建立排查问题的能力。

工具能帮你省时间,但不能帮你建立直觉。你调包的时候遇到一个报错,可能通过搜索引擎找到答案,但如果你不知道模型内部发生了什么,你连搜索的关键词都想不出来。举个例子,当你用BERT做文本分类,发现验证集的F1值一直在0.5左右死活上不去,如果你不理解softmax输出和交叉熵损失之间的关系,连“该检查标签是否从0开始编号”这个思路都不会有。from scratch的价值,不是说不用工具,而是让你在出问题的时候,能够从底层逻辑出发定位问题,而不是在配置文件和模型文件之间来回折腾。

1.3 项目的四个阶段与交付物

我给自己规划了这个项目的四个阶段,每个阶段都有明确的交付物,而不是漫无目的地看书:

  • 第一阶段:地基。完成数学基础(线性代数、概率论、微积分、优化)和Python工程基础,交付物是能独立实现一个线性回归的numpy代码。
  • 第二阶段:核心。理解机器学习和深度学习核心原理,交付物是手写反向传播的两层神经网络,在MNIST数据集上跑出95%以上的准确率。
  • 第三阶段:进阶。掌握Transformer原理和LLM应用开发,交付物是微调一个文本分类模型,并基于RAG架构做一个能回答私有文档问题的问答系统。
  • 第四阶段:工程化。学习模型部署、性能优化、监控评估,交付物是把这个RAG问答系统容器化部署,跑通完整的CI/CD流程。

这四个阶段层层递进,前一个阶段是后一个阶段的地基,每个阶段的交付物都是一个能跑的真实项目,而不是虚无缥缈的“学完某门课”。

2. 知识体系与技术选型,少走弯路的关键

这个项目走了不少弯路之后,我最大的体会是:技术选型决定了你80%的学习体验。选对了,工具会自然地引导你走向最佳实践;选错了,光配置环境就能消磨掉你一半的耐心。所以启动项目之前,花一周时间把整体技术栈和工具链理清楚,比闷头学一个月更划算。

2.1 技能栈全景:从数学到部署的完整链路

AI工程的技术栈远不止Python和PyTorch。我把完整的技能栈分成六层,每层之间关系紧密:

层级核心内容工程工具
数学基础线性代数、概率论、微积分、优化numpy、sympy
编程基础Python语法、数据结构、调试、GitVS Code、Jupyter
机器学习回归、分类、聚类、评估、特征工程scikit-learn
深度学习神经网络、反向传播、CNN/RNN/TransformerPyTorch
LLM应用Prompt工程、RAG、Agent、微调transformers、LangChain
工程化API服务、容器化、监控、性能调优FastAPI、Docker

很多人会在第一层和第二层花太长时间,总想把数学学得无比透彻再往下走,结果学了三个月微积分,连一个模型都没跑过。我的建议是:数学学到“够用”就行——能看懂损失函数、能理解梯度下降的迭代公式、能推导链式法则,就赶紧上手写代码。写代码过程中遇到数学盲区,再回头补,效率比纯看书高得多。

2.2 工具链选型:为什么我选了PyTorch+FastAPI这套组合

先说深度学习框架。PyTorch和TensorFlow之争已经持续了好几年,现在社区生态的天平明显偏向PyTorch,尤其是学术界发布的新模型,绝大多数都优先出PyTorch版本。PyTorch的调试体验也更好,动态计算图让你可以像写普通Python代码一样打断点,对初学者极其友好。TensorFlow虽然在工业部署上仍有自己的优势,但学习曲线更陡峭,调试体验也更糟。

LLM应用框架方面,我做了个有点“逆潮流”的决定:早期阶段不用LangChain,而是自己手写RAG。LangChain帮你把文档加载、切分、向量化、检索、拼接提示词、调模型封装成一条链,确实省事,但问题是你对中间每一个环节的细节完全没有感知。比如向量化时的chunk_size设为多少、检索时用余弦相似度还是点积、Prompt模板里指令和上下文怎么排布,这些参数直接决定效果好坏,而框架把决策权隐藏了。自己手写一遍,你才能真正理解这些参数到底在调什么。

向量数据库选了Chroma,原因是轻量。它不需要单独起服务,可以直接嵌入在Python进程里,文件持久化也简单,做原型和本地实验足够用了。等数据量上来、并发请求变多了,再迁移到Milvus或者Qdrant不迟。

服务端框架用FastAPI,没有太多悬念。它有原生的异步支持、自动生成API文档、基于Pydantic做请求校验,写起来简洁,性能在纯Python框架里也属于第一梯队。部署用Docker,确保任何机器上都能复现同样的运行环境。

2.3 最小可运行的工程环境:版本、依赖和第一行代码

环境配置是整个项目里第一个劝退点。我花了两个晚上才搞定一个干净的环境,在这里把最终验证可行的版本组合整理给你:

  • Python 3.10:兼容性最好,不要用3.12,部分库还不完全支持。
  • CUDA 11.8:PyTorch 2.x的默认支持版本。建议先装PyTorch再装CUDA驱动,因为PyTorch会自己打包CUDA runtime。
  • PyTorch 2.1.2:用pip安装就好。
  • transformers 4.36.0:加载预训练模型用。
  • chromadb 0.4.22:本地向量数据库。

安装指令很简单:

python -m venv ai-eng source ai-eng/bin/activate pip install torch==2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers[torch]==4.36.0 chromadb==0.4.22 fastapi uvicorn docker

装完之后,第一时间跑一个最小的PyTorch验证脚本,确认GPU能被调用:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0)) x = torch.randn(3, 3).cuda() y = torch.randn(3, 3).cuda() print((x + y).sum().item())

如果输出里cuda.is_available()是False,大概率是PyTorch版本和CUDA驱动不匹配,后面我会在问题排查部分详细讲。

3. 从零到落地:四阶段实操全记录

这一章是这个项目的核心正文。我按阶段记录实际操作过程,包括每段代码背后的设计逻辑、参数的来龙去脉和现场踩过的坑。

3.1 第一阶段:数学与Python地基,别被劝退的三条经验

第一阶段最难熬,因为知识和现实之间还没有建立起联结,你不知道学矩阵分解以后用在哪。这时候我总结了三条经验,帮你把枯燥期缩短。

第一条,用做题为辅、直觉为主的学习方式。线性代数不需要你会手算四阶行列式,但需要你理解矩阵乘法是“批量计算一组线性组合”、特征向量是“经过变换后只缩放不旋转的方向”。概率论不需要你证明中心极限定理,但要知道极大似然估计的思想是“找一组参数,让当前数据出现的概率最大”。每学一个概念,就问自己一句“这个东西在机器学习里是干什么用的”。

第二条,用numpy做实验代替纸上做题。比如学梯度下降的时候,画一条y=3x+2的直线,生成一些带噪声的数据点,然后用numpy实现梯度下降去拟合这条直线。学习率分别设0.001、0.1、1.0,可视化损失函数的下降曲线,你马上就能直观感受到学习率太大导致发散、太小导致收敛缓慢是什么意思。这比做十道微积分题都有用。

第三条,Python不用全学完,但要会看traceback。我一开始总想系统学Python语法,看了两章就放弃了。做项目才发现,掌握列表推导、字典操作、异常处理、类和装饰器就够用了。真正值钱的技能是读懂报错信息——从traceback里定位是哪一行、哪个函数调用出了问题。因为后面写模型代码的时候,你99%的时间都在跟各种报错搏斗。

第一阶段我的交付物是把一个线性回归从零用numpy实现了一遍,这个练习虽然简单,但让我第一次把数学和代码对应上,为后面手写神经网络打下了基础。

3.2 第二阶段:不靠框架手写梯度下降,建立反向传播直觉

如果说这个项目只做一件事,我建议每个人都手写一次反向传播。这是理解深度学习的核心门槛——只有亲自动手实现过forward和backward,你才能真正明白什么是“梯度沿计算图回流”。

我用一个两层神经网络在MNIST上手写训练。模型的forward计算图是:输入x经过线性变换W1得到隐藏层,加偏置b1,经过ReLU激活,再经过线性变换W2得到输出,最后过softmax得到概率分布。这个结构很简单,但包含了深度学习里所有关键组件。

反向传播代码的核心是链式法则的逐层实现:

def forward(x, params): w1, b1, w2, b2 = params # 隐藏层 z1 = x @ w1 + b1 a1 = np.maximum(0, z1) # ReLU # 输出层 z2 = a1 @ w2 + b2 probs = np.exp(z2 - np.max(z2, axis=-1, keepdims=True)) probs = probs / np.sum(probs, axis=-1, keepdims=True) return probs, (z1, a1, z2) def backward(x, y_true, probs, cache, params): w1, b1, w2, b2 = params z1, a1, z2 = cache # 输出层梯度:softmax + 交叉熵的梯度推导化简为 probs - y_true dz2 = probs - y_true dw2 = a1.T @ dz2 db2 = np.sum(dz2, axis=0) # 隐藏层梯度:链式法则回传 da1 = dz2 @ w2.T dz1 = da1 * (z1 > 0) # ReLU的梯度:输入大于0为1,否则为0 dw1 = x.T @ dz1 db1 = np.sum(dz1, axis=0) return [dw1, db1, dw2, db2]

这里最值得琢磨的是dz2 = probs - y_true这一行。为什么softmax和交叉熵组合之后,反向传播的梯度会变得这么简洁?因为交叉熵损失对softmax输入端的导数就是概率与真实标签的差。如果你只是调包,永远看不到这个漂亮的性质。

第一个版本跑出来之后,准确率只有87%,和PyTorch实现的95%差了一大截。我对比了很久才发现问题:初始化参数时用0初始化导致对称性问题,换成小随机数初始化之后,准确率立刻跳到94%。有个细节想说的是,MNIST的像素值是0到255,不做归一化直接喂进网络,数值范围太大会让梯度更新不稳定。除以255之后再训练,收敛速度明显变快。这些经验,不手写一遍、不踩一次坑,你是记不到肌肉记忆里的。

3.3 第三阶段:用PyTorch训练第一个文本分类模型

有了手写神经网络的基础,切换到PyTorch就非常顺畅了,因为你知道每一步在做什么,只是让框架帮你算梯度而已。

我选的场景是情感分类:IMDB影评数据集,判断每条评论是正面还是负面。完整训练代码如下:

import torch import torch.nn as nn from transformers import AutoTokenizer, AutoModelForSequenceClassification from torch.utils.data import Dataset, DataLoader tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased") model = AutoModelForSequenceClassification.from_pretrained( "bert-base-uncased", num_labels=2 ).cuda() class IMDbDataset(Dataset): def __init__(self, texts, labels, max_len=256): self.texts = texts self.labels = labels self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): text = self.texts[idx] label = self.labels[idx] encodings = tokenizer( text, truncation=True, padding="max_length", max_length=self.max_len, return_tensors="pt", ) return { "input_ids": encodings["input_ids"].squeeze(), "attention_mask": encodings["attention_mask"].squeeze(), "labels": torch.tensor(label, dtype=torch.long), } train_loader = DataLoader(train_ds, batch_size=32, shuffle=True) optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5) criterion = nn.CrossEntropyLoss() for epoch in range(3): model.train() for step, batch in enumerate(train_loader): input_ids = batch["input_ids"].cuda() attention_mask = batch["attention_mask"].cuda() labels = batch["labels"].cuda() outputs = model( input_ids=input_ids, attention_mask=attention_mask, labels=labels, ) loss = outputs.loss optimizer.zero_grad() loss.backward() optimizer.step() if step % 50 == 0: print(f"epoch {epoch+1}, step {step}, loss {loss.item():.4f}")

这里有两个参数值得你留意。一个是learning rate设为2e-5,这个数字不是拍脑袋拍出来的——BERT这类预训练模型在下游任务上微调时,学习率如果设太大,会破坏预训练阶段学到的特征(有人在论文里专门研究过),一般范

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询