ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI工程从零实战:从手写神经网络到RAG服务部署

AI工程从零实战:从手写神经网络到RAG服务部署 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/TransformerPyTorchLLM应用Prompt工程、RAG、Agent、微调transformers、LangChain工程化API服务、容器化、监控、性能调优FastAPI、Docker很多人会在第一层和第二层花太长时间总想把数学学得无比透彻再往下走结果学了三个月微积分连一个模型都没跑过。我的建议是数学学到“够用”就行——能看懂损失函数、能理解梯度下降的迭代公式、能推导链式法则就赶紧上手写代码。写代码过程中遇到数学盲区再回头补效率比纯看书高得多。2.2 工具链选型为什么我选了PyTorchFastAPI这套组合先说深度学习框架。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.8PyTorch 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 torch2.1.2 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install transformers[torch]4.36.0 chromadb0.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做实验代替纸上做题。比如学梯度下降的时候画一条y3x2的直线生成一些带噪声的数据点然后用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, keepdimsTrue)) probs probs / np.sum(probs, axis-1, keepdimsTrue) 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, axis0) # 隐藏层梯度链式法则回传 da1 dz2 w2.T dz1 da1 * (z1 0) # ReLU的梯度输入大于0为1否则为0 dw1 x.T dz1 db1 np.sum(dz1, axis0) 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_labels2 ).cuda() class IMDbDataset(Dataset): def __init__(self, texts, labels, max_len256): 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, truncationTrue, paddingmax_length, max_lengthself.max_len, return_tensorspt, ) return { input_ids: encodings[input_ids].squeeze(), attention_mask: encodings[attention_mask].squeeze(), labels: torch.tensor(label, dtypetorch.long), } train_loader DataLoader(train_ds, batch_size32, shuffleTrue) optimizer torch.optim.AdamW(model.parameters(), lr2e-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_idsinput_ids, attention_maskattention_mask, labelslabels, ) loss outputs.loss optimizer.zero_grad() loss.backward() optimizer.step() if step % 50 0: print(fepoch {epoch1}, step {step}, loss {loss.item():.4f})这里有两个参数值得你留意。一个是learning rate设为2e-5这个数字不是拍脑袋拍出来的——BERT这类预训练模型在下游任务上微调时学习率如果设太大会破坏预训练阶段学到的特征有人在论文里专门研究过一般范
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进