
从零开始搞 AI 工程我到底经历了什么又踩了哪些坑先说说背景。这个项目叫“ai-engineering-from-scratch”直译过来就是“从零开始的 AI 工程”。名字挺直白——不依赖现成的大模型 API 封装、不靠一键部署平台而是自己动手把数据、模型、服务、评估这条链路整个走一遍。适合谁我看主要有三类人一是刚入门机器学习、想搞清楚“模型之外还有什么”的同学二是已经在业务里调用过接口、但没碰过底层工程的开发者三是想尝试在有限算力下做点真东西的独立开发者。我做完之后的体会最深的不是“AI 有多神”而是“工程化有多碎”——数据清洗、版本管理、服务封装、效果评估每一件事都在消耗时间但每一件事都在决定 AI 能不能落地。这篇就围绕我这次全流程实践把核心思路、关键实现、踩过的坑挨个说透。1. 项目整体设计为什么从零开始而不是直接套壳1.1 从零开始的真正含义市面上已经有大量现成工具Hugging Face 的 pipeline 三行代码就能跑一个模型LangChain 拖几个组件就能搭出问答机器人。那这个项目为什么还要“from scratch”我的观点是套壳能让你快速做出 demo但从零开始才能让你理解系统里每个环节的约束和代价。举个例子用现成的 embedding 接口你永远不用关心向量维度是多少、距离函数怎么选、索引怎么建。可一旦你需要在私有数据上做语义检索或者需要控制单次查询延迟这些问题就全都冒出来了。从零开始本质上是把“隐性问题显性化”逼着你把每一步都搞清楚。这次项目我给自己定的规则是模型可以用开源的、框架可以用现成的但数据处理、训练脚本、服务封装、评估流程必须自己写。1.2 系统的三块核心内容动手之前我先画了一张系统拆解图这是我在纸上画的不是项目文档。整个系统分三块数据层负责采集、清洗、切分和标注同时要维护数据集的版本。模型层负责加载预训练模型、微调训练、结果评估以及把模型导出成可部署格式。服务层负责把模型包装成 HTTP 服务加鉴权、限流、监控和日志。三层的划分几乎和通用后端系统一样唯一的区别是“模型”取代了“业务逻辑”成为系统里最特殊的一环。这也意味着AI 工程本质上还是工程只是多了一堆模型特有的调试手段和部署约束。1.3 MVP 的边界划定这种项目最大的风险是摊子铺太大。一开始我做计划时列了知识库问答、情感分析、文本摘要、图片分类四个方向差点把自己玩死。后来做了一个很关键的决策砍到只剩文本分类。原因很简单——分类任务效果容易量化准确率、F1 一眼就能看明白最适合验证整条工程链路。搜索问答和生成式任务牵扯到的评估复杂得多不适合第一个版本。所以如果你们也要搞类似项目我的建议是先划一个极小但完整的功能切片数据→训练→评估→服务每一步都能跑通再谈扩展。这个建议看起来平平无奇但能卡掉大半烂尾项目。2. 核心细节与实操要点数据、训练、服务三环节的硬骨头2.1 数据清洗比想象中痛苦十倍的环节数据量不大我用了大概 2 万条带标签的文本数据来自公开的新闻分类语料。按理说这是我已经反复处理过很多次的数据但实际一跑还是发现问题不少。第一是标签不均衡。娱乐类的样本量是体育类的三倍直接训练的话模型会偷懒——全预测成娱乐准确率还挺高但根本没意义。我用了两种方法修正一是对少数类做过采样二是计算损失时给少数类加权。大家在做类似处理时最忌讳的是只做其中一种还指望效果有多好我实测下来两者结合效果才稳定。第二是脏数据特别隐蔽。文本里有全角半角混用、多余换行、HTML 实体残留这些都不至于让程序崩溃但会让模型学到一些莫名其妙的规律。比如训练集里所有提到“股市”的文本都来自财经频道模型就可能把“股市”这个词当成强特征遇到其他频道的股市报道就会判断错。我专门写了一个清洗函数把不可见字符、URL、重复标点全部归一化同时打印清洗前后的样本差异来核对。第三是数据切分。正常人都知道要分训练集、验证集、测试集但有个细节容易忽略如果同一条新闻被转载多次原始数据里会出现近乎重复的样本。这种样本一旦同时落在训练集和测试集里测试指标的参考价值就大打折扣。我用 MinHash 做了去重之后再做随机切分保证测试集更可信。2.2 为什么要用开源模型而不是直接调 API这个项目里有朋友问直接用 OpenAI 的接口不是更快吗我承认确实更快但这里有个关键考量——可控性和成本。API 接口每次调用都要花钱做批量训练评估的时候跑几千条样本的费用结算下来并不低。更重要的是API 模型你拿不到中间层的特征表示微调和权重剪枝就更不用想了。所以这个项目我选了一个开源的中文预训练模型参数量在 1 亿级别在单张消费级显卡上就能完成微调。对独立开发者来说这是一个理性选择——既能跑通完整流程又不至于被硬件卡脖子。选型时我看重三件事License 是否宽松、中文效果是否靠谱、模型文件是否容易下载。最后选的是一个基于 Transformer 架构的中文 BERT 变体大约 4 亿参数。说实话这个体量在任务不复杂的情况下已经完全够用了。2.3 微调训练从 loss 到 checkpoint 的完整链路训练阶段我用的是 PyTorch Hugging Face Transformers 库。这里的“从零开始”指的是整个训练脚本自己写而不是命令行直接调用别人封装好的 trainer——别误会我最终也没完全绕开 trainer但核心循环逻辑我保留了。训练参数方面我记录一下这次实测出来的配置参数我的取值说明batch_size32显存占用和梯度稳定性之间的平衡点learning_rate2e-5预训练模型的微调经验值不能再大了epochs3数据集不大太多轮次要过拟合warmup_ratio0.1前 10% 的 step 用较低学习率热启动max_length256分类任务不需要太长的上下文太长浪费算力几个最容易踩的坑说在前面。学习率过大是微调最容易犯的错误我试过 1e-4两轮之后 loss 直接不降了。batch size 太小的话 loss 曲线会像心电图一样震荡看着就难受。另外epoch 数不是越多越好第三轮结束的时候我发现验证集 F1 已经开始原地踏步说明再train也是空转。训练过程中我会记录每个 epoch 的 loss、验证集 F1、学习率变化存成 CSV 文件。有一个很扎心的经验loss 降得好不代表线上效果好所以光看 loss 曲线就宣布胜利等上线再哭就晚了。2.4 评估体系光有准确率远远不够分类任务的评估准确率是最直观的但它会骗人。前面提到数据不均衡如果测试集里娱乐类占 60%无脑预测娱乐类也能有 60% 的准确率看起来像是不错的模型。所以我的评估脚本里同时输出四份报告总体准确率、各类别的精确率/召回率/F1、混淆矩阵、以及少量错误样本的具体预测结果。混淆矩阵特别好用它一眼能看出模型在哪两类之间容易混淆。比如我这个模型在“科技”和“数码”之间经常出错——这其实不奇怪这两个类别标签边界本来就模糊。后来我干脆把两个标签合并了准确率一下涨了几个点。这个操作属于业务判断不能光看数据。另外我还做了一个抽检机制每个类别随机抽 20 条预测结果打印出来人工快速过一遍。这一步花费时间不多但能发现一些指标反映不出来的问题比如模型会把某个类别里的特定品牌名全判别错了这属于刻板偏好指标上不一定难看出来。2.5 服务封装把模型变成能对外提供服务的接口模型训练好之后不能只停留在 notebook 里自嗨必须包装成接口。这里我用了 FastAPI选它的原因很简单性能不错、自带 OpenAPI 文档、写起来简单。我做了两个版本的接口一个用于单条预测的同步接口一个用于批量预测的异步接口。同步接口的逻辑非常直白接收 JSON 请求、解析文本、调用模型推理、返回预测类别和置信度。逻辑虽简单但要“稳”就需要额外处理几件事模型只加载一次不要在每个请求里重新加载。否则内存会被撑爆。输入文本要做与服务端相同的预处理否则模型输入分布不一致效果会退化。输出要做成结构化 JSON同时附上版本号字段方便线上回溯。还有一个细节容易被忽略并发安全。PyTorch 模型在推理时默认不是线程安全的如果多个请求同时进来可能会产生意料之外的错误。我加了一个进程锁让推理在单线程里执行配合 FastAPI 的异步机制实测能在普通 4 核 CPU 上稳定扛住约 20 QPS。对小规模场景足够用了。2.6 模型版本管理Model Registry 的基础实现这也是“从零开始”容易忽视的环节。很多新手训练完模型就忘等想复现效果时找不到是哪个权重文件。我建立了一个极简的模型注册表就是一个目录结构models/ run_001/ config.json vocab.txt model.safetensors metrics.json train_args.jsonrun_001 这个文件夹里除了模型权重还保存了当时的超参数和评估指标。这样每次实验都变成有迹可循的记录回滚也简单。用现在的眼光看Prometheus 或 MLflow 这类工具做这件事更专业但对个人项目来说一个规范的目录结构就已经能解决 80% 的混乱问题。3. 实操过程从环境搭建到服务部署的全场记录3.1 环境准备踩坑的重灾区我是在一台 Ubuntu 22.04 服务器上跑的配置是 8 核 CPU、32G 内存、一张 RTX 4060 显卡16G 显存消费级。系统里 Python 版本 3.10。环境搭建过程说多了都是泪问题主要出在 CUDA 和 PyTorch 的版本匹配上。我把关键坑先写出来。第一不要无脑pip install torch默认装的是 CPU 版或者与你环境不匹配的 CUDA 版一定要按 PyTorch 官网的提示装对应 CUDA 版本的 wheel。第二CUDA 驱动版本和 PyTorch 要求的 CUDA runtime 版本是两回事驱动向下兼容runtime 随包走搞清楚了这俩概念能少走很多弯路。第三用 conda 隔离环境我吃过系统 Python 环境被搞乱的亏现在任何项目第一件事就是建独立虚拟环境。我的安装命令大概长这样conda create -n ai-eng python3.10 conda activate ai-eng pip install torch --index-url https://download.pytorch.org/whl/cu121 pip install transformers datasets fastapi uvicorn scikit-learn装完之后有个快速验证方法跑一段小代码确认 GPU 可用且 PyTorch 能调用 CUDN。这一步别跳过不然训练开始半小时才发现 GPU 没被识别直接心态爆炸。3.2 数据处理的完整脚本逻辑写一个prepare_data.py功能是接收原始语料目录输出干净的 train/val/test 三个 CSV 文件。核心函数包括clean_text()、remove_near_duplicates()、stratify_split()。这里面我花时间最多的是去重逻辑用了 MinHash 计算文本相似度把相似度超过 0.85 的样本过滤掉。效果是数据量从 2.1 万降到 1.8 万测试集 F1 的可信度明显提升了。由于这种去重会砍掉一部分有效数据所以我在脚本里加了日志输出每次运行都能看到“原始样本数、去重后样本数、保留比例”方便判断阈值设得是否合适。阈值如果设得太低可能会误杀一些正常样本所以有条件的话建议抽样看一眼被过滤的文本到底是什么内容。3.3 训练脚本的关键代码片段训练部分我用的是 Hugging Face 的 Trainer但加了自定义的早停机制和数据增强逻辑。核心代码大概是这个样子from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained(bert-base-chinese, num_labels5) training_args TrainingArguments( output_dir./checkpoints, evaluation_strategyepoch, save_strategyepoch, learning_rate2e-5, per_device_train_batch_size32, per_device_eval_batch_size64, num_train_epochs3, warmup_ratio0.1, logging_dir./logs, load_best_model_at_endTrue, metric_for_best_modelf1, save_total_limit2, )这里我给新手提个醒metric_for_best_model要是改成accuracy在不均衡数据上很容易选错最优 checkpoint。选f1更稳。Trainer 最有价值的设置是load_best_model_at_endTrue它会自动保留验证集上最好的权重而不是最后一个 epoch 的权重。省去了手动比较多个 checkpoint 的功夫。另外训练完不要直接部署。先跑一遍测试集脚本输出精确率、召回率、F1 和混淆矩阵把这几个数字留下来。这些数字是模型性能的基线以后任何修改——调数据、换模型、调参数——都要拿它做参照。我还顺手把测试集的预测结果显示成表格人工扫一遍看看有没有模型明显“犯傻”的例子整个过程大概二十分钟但是很值。3.4 服务部署CPU 推理也能用的优化训练在 GPU 上完成之后真正部署到生产环境时往往没有 GPU 可用或者不想为一个小服务独占一张显卡。我测试了纯 CPU 推理的延迟单条短文本256 token 以内平均 80 毫秒左右对小流量内部工具完全可用。这个延迟对用户体验来说是可以接受的。如果觉得延迟太高有两个杀手锏一是改用 ONNX Runtime同样是 CPU速度差不多能提升两三倍二是给模型开量化把 fp32 权重量化成 int8体积缩小四倍速度还能再快一截。这次项目里我只用 TorchScript 做了一次小优化ONNX 转换留到了下一个迭代。但我想强调转换模型的推理代码和原来的 PyTorch 代码多少有一些差异有时候输出数值会略微浮动所以转换完必须重新跑一遍评估脚本确认指标没有大幅下降。3.5 Docker 打包与上线实战为了让服务可以到处跑我写了一个 Dockerfile把模型文件打进镜像。这一步遇到的最大坑是镜像体积——模型文件 400MB加上 Python 环境和依赖镜像直接超过 2GB构建的时候慢得让人怀疑人生。优化办法有几个我实测有效的是尽量用python:3.10-slim作为基础镜像不要用带 GPU 全家桶的大镜像安装依赖的时候使用 pip 的--no-cache-dir模型文件直接 COPY 进镜像不要在构建时联网下载。做完这些镜像体积可以压到 1.2GB 左右对一个带模型的推理服务来说已算不错。部署方式我用了 Docker Compose配置只写了服务和端口映射services: ai-engine: build: . ports: - 8080:8080 environment: - MODEL_VERSIONrun_001 restart: always启动之后先跑一遍健康检查确认接口返回正常。我习惯在服务里加一个/healthz端点返回模型版本和最近一次推理耗时方便判断线上服务的运行状态。3.6 压测笔记一个不严谨但实用的压力测试我在上线前做了一个不太严谨的压测用脚本并发 50 个请求持续打服务。结果发现 CPU 占用冲到 380%四核但请求没有大量失败只是延迟从 80 毫秒涨到 300 毫秒。这说明单机部署的极限就摆在那里并发高了自己也扛不住。于是我在服务端加了一个信号量限制最大并发数为 8超出直接返回 429 状态码。这是一个非常有用的工程技巧——与其让服务过载崩溃不如主动限流让用户体验可控的延迟。4. 常见问题与排查技巧我踩过的坑全在这4.1 问题速查表整理了一张实战问题对照表按数据、训练、服务三类划分。表格里的每一行都是这次项目里真实踩过的坑不是编出来的类别现象原因解决方式数据训练 loss 很低测试 F1 却很差数据切分前没有去重训练集和测试集有重叠用 MinHash 去重后再切分数据某些类别准确率特别低标签不均衡模型偏向多数类过采样 损失函数加权训练loss 曲线震荡不下降学习率过大或 batch size 过小调小学习率增大 batch训练验证集 F1 不升反降过拟合减少 epoch或加早停训练GPU 利用率低数据加载太慢增加 num_workers用 pin_memory服务并发请求时服务崩溃没有并发控制加信号量限制并发数服务CPU 推理延迟高模型没有优化转 ONNX Runtime 或量化服务请求返回结果与测试不一致预处理逻辑不一致统一文本预处理函数我还想特别说说数据预处理不一致这个问题。这是一个特别隐蔽的坑训练时我做了一个clean_text()部署时为了图省事直接调了松一点的清洗正则结果线上推理的效果立刻变差。后来我把预处理函数抽成了公共模块然后写了个测试用例来验证它的确定性这是非常值得的投入。4.2 排查流程分享一个定位问题的思路有一次我遇到一个特别诡异的问题测试集 F1 有 0.87但部署到服务上之后手写几条测试数据效果明显变差。一开始怀疑是模型权重出了问题后来才想到去对比服务收到的请求文本和训练集里的文本——结果发现 API 收到的文本带了 JSON 转义符而我的预处理没处理这个符号导致模型输入的 token 序列完全不同。这就是线上和线下不一致的典型例子。排查这类问题我的思路是三步走第一步固定输入样本分别用测试脚本和服务接口跑对比输出 JSON 是否一致第二步如果输出不一致逐一检查预处理、推理和结果解析三个阶段第三步直接打印模型输入 token看看和测试脚本里的 token 是否相同。这一步可以直接定位问题出在哪一节不用瞎翻日志。4.3 关于算力不足的生存手册很多同学看到项目第一反应是“我显卡不够”。说实话消费级显卡甚至纯 CPU 也能干不少事。我这个项目只有微调那一步必须用 GPU数据处理、评估、服务部署全是 CPU 和内存就能搞定的。训练一个 epoch 在 4060 上大概 7 分钟三个 epoch 也就是 20 多分钟这个算力成本对个人项目来说完全可以接受。如果你显卡显存只有 8G也有办法batch size 降到 16用梯度累积来弥补。如果压根没有 NVIDIA 显卡可以用 Google Colab 的免费 GPU 练一练或者考虑更小的模型架构像 MiniLM。唯一要注意的是如果想在 Colab 上完成微调之后再下载权重本地部署的时候要保证模型结构完全一致否则加载会报错。4.4 我把这个项目往后扩展的三个方向这个 MVP 跑通之后我脑子里已经列出了三个明确的扩展方向。第一个是加入向量检索做一个私有知识库的问答接口这会把服务层结构大大丰富需要引入向量数据库。第二个是把模型服务做成插件式的让训练好的分类、摘要、实体识别模型可以通过配置切换。第三个是在服务层加上更完善的监控把推理耗时、置信度分布、请求量按天汇总起来这些指标能指导模型的下一次迭代。5. 写在最后的个人心得每次做完这种全流程项目最大的收获都不是最后的 F1 数字而是我深知“从零开始跑通”这句话到底意味着什么。它意味着你要在无数个环节里做选择——数据清洗到什么程度、模型选多大、服务放在什么环境、评估做到多细——每一个选择都会影响最终落地效果。没有捷径只有挨个踩过去。最后再分享两个小实践。一是我训练完模型之后顺手保存了 10 条测试集预测错误样本的原始文本没事翻一翻比自己瞎调整模型参数更有启发。二是所有脚本我都放在一个 repo 里每次实验的配置和结果自动写进experiments.md下次想复现某个效果直接按图索骥脑子记不住那么多细节。下一步我打算把第三个扩展方向里的监控模板先做出来毕竟上线之后才真正进入“工程化”的主战场。