
1. 先从项目背景说起AI工程到底在做什么1.1 AI工程不等于调模型它是一条完整链路去年底我给自己定了一个小目标不借助任何一键部署AI的平台模板从Python环境搭建开始亲手把一个AI工程从零跑通。项目名就叫ai-engineering-from-scratch。当时想得很直接既然行业里到处都在喊AI工程化“AI落地”那我不如顺着整条链路亲自动手走一遍数据、训练、评估、服务化、部署、监控一个环节都不跳。先说一个大多数人的误解。很多朋友觉得AI工程就是做模型把准确率从95%刷到96%就算入门。真上手之后你会发现模型训练在整个项目周期里占比可能不到三分之一。一个能稳定运行的AI应用完整链条是业务问题定义、数据采集、清洗与标注、特征工程、模型训练与评估、推理服务化、容器部署、线上监控再回到数据和特征做下一轮迭代。这中间的每一步都可能让你卡住一天甚至一周。我习惯用一个做饭的类比来解释这件事。数据是食材模型是菜谱但AI工程不等于有一本好菜谱。你得会买菜、洗菜、切菜得知道火候怎么控制还得保证客人随时来都能上菜菜坏了要能发现是哪一步出了问题。训练模型只是按菜谱炒一道菜工程化是把整个后厨支起来。这个项目的本质就是把后厨从零搭一遍。1.2 项目目标设定先跑通最小闭环再谈花活做这个项目之前我给自己定了三条边界防止跑偏。第一不追SOTA。我不打算在某个榜单上刷到第一名也不打算用最前沿的模型目标是把链路打通让每个环节都可解释、可复现。第二场景选简单但完整的。我选的是情感分类作为第一个支点数据公开好找、任务直观、评估指标清楚适合用来验证整条工程链路。第三交付物可运行。不是notebook里能出个准确率就完事而是要有API接口、有容器镜像、有基本监控别人拉下来能直接跑。这里有个很关键的思考为什么先用小场景因为AI工程的难点不在模型有多复杂而在于每个环节的可靠衔接。用一个小而完整的场景把链路磨顺了后面换任何模型、换任何任务套的骨架都是同一套。我见过太多人一上来就做大模型应用结果卡在数据格式、接口并发、部署环境这些基础问题上反而把最重要的工程能力耽误了。这个项目适合谁参考两类人。一是刚入门机器学习和深度学习想转向工程实践的开发者你可以照着这条路线一步步走。二是本来做后端或全栈、想往AI应用方向延伸的同学你会在这篇文章里看到AI应用落地时最常遇到的那些非模型问题。如果已经有多年AI工程经验这里面的踩坑记录也许能帮你查漏补缺。2. 从零开始的路线图设计先想清楚不做什么2.1 三阶段递进基础、算法、服务化整个项目我拆成了三个阶段每个阶段有明确的产出物。第一阶段是Python数据处理基础。不是简单学语法而是直接用pandas、numpy处理真实数据集做缺失值统计、字段类型转换、分组聚合、可视化分布。练手项目是分析IMDB影评数据集的长度分布、标签比例、常见词频。这个阶段的产出是一套干净的数据处理脚本为后面所有环节打底。第二阶段是传统机器学习。我用逻辑回归、决策树、随机森林三个模型做情感分类重点不是调参而是强制自己理解三个概念过拟合、交叉验证、评估指标。每个模型都要输出混淆矩阵和precision/recall/F1并解释为什么在这个场景下accuracy不够用。这个阶段结束后工具栈里多了scikit-learn的pipeline机制。第三阶段才是深度学习和大模型应用。用PyTorch搭一个基础的文本分类模型搞懂embedding和softmax的来龙去脉然后过渡到LLM应用做一个最小可用的RAG问答系统最后用FastAPI加Docker完成服务化部署。到这一步传统模型和LLM应用在工程上的共性就非常清楚了。为什么这么设计因为AI工程里的很多东西是层层依赖的。你不理解交叉验证就不知道怎么评估RAG的检索效果不理解TF-IDF的词汇表对齐就理解不了embedding为什么要在同一语义空间里计算。自底向上学每一步的为什么都能落地。2.2 为什么从传统机器学习切入而不是直接上大模型这是我在选题时纠结最久的一点。现在大模型工具链已经很成熟LangChain、LlamaIndex、各种开源模型都能直接跑为什么不直接学这些我的答案是一个反问如果工厂里的质检机器出了问题你会修吗很多人会但只是会按手册重启。真正的问题排查能力来自对内部原理的把握。大模型不是黑盒但你直接跳到API层会错过太多底层机制。传统机器学习阶段有三个不可替代的价值。第一成本低笔记本CPU就能跑迭代速度极快你可以在十分钟内改一个参数、重训一次、对比一次结果这种反馈速度对建立直觉特别重要。第二指标准确理解数据量不大你可以手动验算一个precision彻底明白它到底在算什么东西。第三概念迁移性强大模型里的tokenization、embedding、注意力机制思想和传统NLP里的词表、词向量一脉相承有了底层理解上层工具怎么封装都不会迷路。当然这不代表每个人都必须从线性回归手推公式开始。如果你已经有扎实的ML基础可以直接从第三阶段进入。但对from scratch项目来说宁可慢一点也要把链路走扎实。3. 核心实操从环境到第一个可部署的AI应用3.1 环境准备与仓库结构别小看这一步很多人死在第一步环境没配好后面全都是问题。我用的方案是conda创建一个独立环境Python版本锁定3.10。这里有一个建议依赖锁定一定要做requirements.txt里每个包都写死版本号不要用这种写法。我踩过的坑是某次升级numpy之后旧模型的joblib文件直接加载报错排查了整整一个下午最后发现是版本不兼容。项目目录我建议按功能拆分不要在根目录堆一堆文件ai-engineering-from-scratch/ ├── data/ # 原始数据与处理后数据 ├── notebooks/ # 探索性分析 ├── src/ # 训练/推理/服务化代码 ├── models/ # 模型产物joblib/onnx等 ├── scripts/ # 一键训练/评估脚本 ├── tests/ # 接口与函数测试 └── requirements.txt目录结构的意义不是好看而是让每个环节的产物有明确归处也让后来的迭代不互相污染。数据我用的是IMDB影评数据集大约5万条网上直接可以下到csv格式。下载之后先做一件事统计label分布。IMDB是相对均衡的但真实项目里这一步不能省标签分布会直接影响评估策略。3.2 最小可复现的训练流程pipeline是救命稻草先说我走的完整流程。数据加载之后划分训练集和测试集这里注意用stratify按标签比例分层抽样避免切分后测试集标签分布和整体偏差太大。然后做文本预处理全部转小写、去掉HTML标签、用正则清洗特殊符号、按空格或中文分词。英文我用spacy中文场景用jieba这个项目里选哪个看数据语言。接着是特征工程。我用scikit-learn的TfidfVectorizer把文本转成向量ngram_range设为(1,2)max_features限制到5万避免维度爆炸。值得说的是这一步必须和模型一起放进Pipeline里不能分开写。因为TF-IDF的词汇表是从训练集拟合出来的推理时也必须用同一套词汇表分开写在测试集上重新fit特征维度都对不上这是新手最容易犯的错误。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline model Pipeline([ (tfidf, TfidfVectorizer(ngram_range(1, 2), max_features50000)), (clf, LogisticRegression(C1.0, max_iter1000)) ]) model.fit(X_train, y_train)基线模型我用逻辑回归不为什么花哨理由就是快、稳、可解释。训练完成后用测试集做评估输出四样东西accuracy、precision、recall、F1以及混淆矩阵。你会发现准确率可能到85%以上但对负面评论的precision可能偏低这就是后续要优化的方向。这里有个关键体验流程跑通之后的模型评估不止是在notebook里看一眼数字而是要写一个eval.py把每次实验的指标记录到一个csv文件里方便横向对比。我后来做模型迭代全靠这份记录不然调了几版参数之后根本记不清哪个组合是什么效果。3.3 服务化与部署FastAPI加Docker打底模型训练好只是第一步真正交给业务用的是服务化。我用FastAPI写了两个接口一个是健康检查/health另一个是预测接口/predict。第一个坑出现在接口函数定义上。FastAPI里如果用def定义接口函数它会在线程池里执行用async def则会在事件循环里执行。如果你内部调用的是同步的模型推理用async def反而会阻塞整个事件循环并发一高全部卡住。所以我的做法是模型推理这类CPU密集任务用普通的def接口让FastAPI自动丢到线程池处理。from fastapi import FastAPI from pydantic import BaseModel import joblib app FastAPI() model joblib.load(models/lr_sentiment.joblib) class Review(BaseModel): text: str app.get(/health) def health(): return {status: ok} app.post(/predict) def predict(item: Review): pred model.predict([item.text])[0] return {label: int(pred)}模型加载我放在了模块顶层进程启动时只load一次而不是每次请求都重新load。这个优化看起来不起眼但直接把单次请求延迟从上百毫秒降到了几毫秒因为joblib加载逻辑回归模型虽然不慢但总经不起每次请求来一遍。真实项目里如果加载的是几百MB的深度学习模型这个差异就是秒级和毫秒级的区别。Docker部署我用了最简单的方式。基础镜像选python:3.10-slim先把requirements.txt复制进去装依赖再把代码复制进去这样能充分用上Docker的层缓存改代码之后重新构建不需要重装一遍依赖。镜像构建好之后本地起容器用curl实测那两个接口确认返回正常再算部署完成。为什么不一步到位上Kubernetes我的看法是单机容器部署都没跑明白上编排系统只会让你同时面对两套问题。先把一个镜像在容器里稳定对外服务这件事吃透后面再谈弹性伸缩。3.4 从传统模型过渡到LLM应用最小RAG实现传统模型做完分类之后我明显感觉到边界它只能做封闭式判断给不了开放式回答。要往前走就得进入大模型应用的范畴。这里我选了RAG作为过渡因为它是目前LLM应用里最务实的落地模式。RAG全称是Retrieval-Augmented Generation检索增强生成核心思路是先把你自己的文档切成小块用embedding模型转成向量存进向量数据库用户提问时把问题向量化去库里检索最相似的几段内容和问题一起拼成prompt送给大模型让模型基于检索到的材料回答而不是凭空发挥。最小实现不用上LangChain直接手写就能跑通。我用text-embedding系列或者开源bge-small模型做向量化向量库先用Faiss或Chroma跑通逻辑之后你自然知道哪些地方需要替换。我把它类比成整理书架切块是给每本书贴索引标签embedding是给每个标签编码检索是按相关度找出最相关的几本LLM则是读完这几本书之后替你组织语言回答。RAG的难点不在写代码而在细节文档切多大合适是一段一段切还是按固定token数切检索返回TopK取多少K值太小答案缺失K值太大上下文太长让模型抓不住重点拼prompt时要不要给模型加一个如果资料里没有答案就直说的约束。这些细节只有自己动手调一遍才能有手感。4. 项目推进中踩过的坑与排查实录4.1 数据环节的三个坑每个都让人崩溃先说第一个坑切分数据时没设置随机种子。有段时间每次跑出来的评估结果都不一样一开始还以为是模型收敛问题后来才发现是train/test split的random_state没固定。这个问题很隐蔽因为它不报错就是结果飘。从此我只用固定随机种子并且在代码注释里写明种子值。第二个坑是数据泄漏。我做预处理时先在完整数据集上统一做了停用词过滤和标准化然后再切分训练集和测试集。看似没毛病实际上测试集的信息已经通过词表统计泄漏进了训练过程导致评估结果虚高。这个问题的解决方案就是上一节说的把预处理和模型放进Pipeline里让scikit-learn保证每一折都在训练集上独立拟合。第三个坑是标签不均衡。我用了一个极端的模拟数据实验99个正例1个负例模型全部预测为正accuracy高达99%但precision/recall完全没法看。这个实验让我真正理解了为什么行业里普遍强调用F1而不是accuracy。真实业务里信用卡欺诈、异常检测全是这类分布上来先看准确率注定被表象骗。4.2 模型与推理的坑从joblib到显存溢出推理阶段最容易出的问题第一个是保存和加载不一致。我用joblib保存模型时没注意版本换了环境之后加载报错或者特征维度不匹配。后来规范了操作保存时打印模型版本和特征维度加载时自动化校验。这个习惯救了我好多次。第二个问题是每次请求都重新加载模型。我见过一个同学的Flask服务里把模型加载写进了函数内部一个测试请求要等好几秒并发一上来直接卡死。我的建议是模型加载放模块级或者用懒加载加缓存保证同一个进程里只有一份模型在内存中。第三个是深度模型部署时的显存问题。我用PyTorch做文本分类模型时测试阶段一个不留神把batch_size设成了训练时的64本地测试没问题但要部署的机器显存小直接OOM。推理时的batch_size要单独调而且加一个显存监控。不要假设所有机器都和你本地一样。4.3 工程化部署的坑健康检查、日志、镜像大小部署阶段第一个坑是缺少健康检查。容器起来了但依赖的向量数据库还没就绪服务一直报错。后来在/health接口里不仅检查进程存活还检查关键依赖是否可连接这样编排系统才能准确判断服务是否真正可用。第二个坑是日志太随意。训练时print几个指标没问题线上服务可不能依赖print。我养成了用logging模块输出结构化日志的习惯每条请求都有trace_id方便后端和推理端对齐排查。没有日志线上出了问题就像在暗房里找一根针。第三个坑是Docker镜像臃肿。最开始我用python:3.10作为基础镜像装完依赖之后镜像快2GB构建要几分钟。换成slim版本并清理pip缓存之后直接砍到几百MB。对大模型应用这类优化虽然不能改变模型本身的体积但至少让基础环境不再拖后腿。4.4 常见问题速查表建议直接收藏症状可能原因快速排查/解决方案每次训练结果不一样没有固定random_state在所有随机操作中显式设置种子评估指标高但线上效果差数据泄漏检查预处理是否在切分前统一执行改用Pipeline接口首次请求特别慢模型在请求时加载模型加载移到模块顶层并加缓存并发一高接口全部卡住async def里跑了同步推理改用普通def利用线程池Docker重新构建特别慢代码变更后依赖重装先复制requirements再复制代码利用层缓存RAG检索结果明显不相关切块粒度不合理调小切块长度检查embedding模型是否匹配容器起来了但一直报错依赖服务未就绪健康检查里加入依赖连接检测显存直接溢出推理batch_size沿用了训练值推理单独调小batch_size加显存监控这张表不是我编出来的全是这个项目里真实撞过的墙。每一条后面都有一段至少半小时的排查经历如果你遇到类似症状按表格里的方案走基本能省下这些时间。4.5 关于RAG和LLM应用你必须知道的两个坑最后补两个LLM阶段的独家体会。第一个坑是embedding模型的语义空间不一致。我一开始用BGE模型做向量化后面想换成新出的一个英文模型结果没有重新建索引直接在旧向量库里检索召回结果惨不忍睹。根源是不同模型的向量分布根本不在一个空间里。换embedding模型就必须全量重新切分文档、重新生成向量、重新建索引没有捷径。第二个坑是prompt约束的措辞影响极大。我在RAG的prompt里加了如果资料中没有答案请说明无法回答结果模型还是经常强行编造。后来改成更强的措辞明确告诉模型只基于以下检索到的内容回答不要使用内部知识并加上few-shot示例编造率才明显下降。这让我认识到大模型应用工程中prompt也是一种需要测试和维护的代码不要小看它的影响。5. 最后分享一点我自己的体会这个项目从立项到跑通第一版前后投入了大概三个月。收获最大的不是某个技术点而是一种对AI工程全貌的掌控感。以前看别人讲RAG、讲部署每个词都认识但总感觉隔着一层纱。亲手把链路走完之后再听这些概念每个词背后都有画面数据长什么样、模型是怎么存下来的、接口返回慢是因为什么。如果你也想尝试类似的from scratch路线我给三个建议。一是项目小一点先把数据-模型-接口-容器这个最小闭环跑通别一上来就铺大摊子。二是代码可以抄但每一步必须自己跑一遍抄代码不动手三个月后你依然不会排错。三是保留每一版实验记录和日志用表格记录下来它们是你后续迭代最宝贵的参考。这个项目后续我打算补两件事一是给RAG链路加可观测性把检索到的文档片段和最终答案一起输出到日志方便判断是检索的问题还是生成的问题二是做简单的A/B测试框架让模型版本上线时能对比线上指标再决定是否全量。如果你也在做类似的AI工程实践欢迎一起交流踩坑心得。