ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建AI工程体系:数据、训练、评估、部署全链路实践

从零搭建AI工程体系:数据、训练、评估、部署全链路实践 ai-engineering-from-scratch——很多人看到这个标题的第一反应是这又是一个什么巨型系统还是某家公司的内部平台实际上我最初只是把它当作一个个人项目来做的核心诉求特别朴素把自己从零搭建一套AI应用工程体系的方法论、代码骨架和踩坑记录沉淀下来让后续无论是做算法实验、模型微调还是落地部署都有现成的路径可走。这套体系做完之后我发现它解决的问题确实是很多团队普遍卡住的点算法同学有一堆训练脚本但交付不了工程同学会写服务但不懂模型生命周期整个链路数据、训练、评估、部署各管一段出了问题互相甩锅。所以这份工程的定位很清晰它不是某个具体算法的实现而是一套贯穿AI应用全生命周期的可复用骨架从环境准备、数据版本控制、实验追踪到模型训练、评估、部署、监控一条线拉通。适合三拨人参考刚入门想建立全局认知的算法工程师、需要把模型落地的后端开发者、以及带算法团队的Tech Lead。整个拆解过程我会按照实际搭建顺序来讲每部分都会给出选型逻辑、实操配置和真正的坑尽量让读完的人能直接照着重现一遍。1. 内容整体设计与思路拆解1.1 从Classic ML到LLM时代工程框架为什么需要重构早期做机器学习工程大家的习惯是训练脚本归训练脚本线上服务归线上服务中间靠一个模型文件传递本质上是“离线实验为主在线系统为辅”。这套模式延续了很多年直到大语言模型出现后全乱了套。Prompt、上下文窗口、向量检索、Agent编排、流式输出这些概念一股脑涌入传统的监督学习pipeline根本装不下这些新东西工程边界一下子变得模糊了。在这个项目里我并没有刻意去追求“最新最热”而是反推了一下一个从零开始的AI工程体系到底哪些组件是无论如何都绕不开的。最后收敛成四个模块基础环境与工具链包括依赖管理、实验追踪、版本控制这是地基。数据工程数据的获取、清洗、版本管理、质量验证这是决定模型上限的部分。训练与微调闭环从baseline到微调再到实验对比要形成一个可迭代的回路。评估、部署与监控模型只有上线且持续可观测才能真正产生价值。这个划分逻辑和传统软件开发有相似之处但差异也很明显传统工程写代码追求确定性AI工程处理的是概率系统同样的输入可能输出不同结果。所以整个框架设计的底层原则从“保证正确”变成了“保证可控”所有模块都是围绕可复现、可比较、可回滚来设计的。1.2 为什么我坚持“自建优先平台辅助”市面上已经有非常成熟的MLOps平台比如各种云厂商的AI平台、开源的Kubeflow、MLflow等直接拿来用不是更快吗我在项目中确实评估过最终的结论很务实平台可以用但不能把自己的工作流绑架在某个平台的交互方式上。云平台的问题是黑盒化程度太高数据怎么流转、模型怎么调度、评估逻辑是什么都被封装在平台内部。出了问题平台本身的错误信息往往起不到什么排查作用。自建骨架的本质是把每一个环节的关键决策明确地握在自己手里用什么版本的数据、跑了什么实验、为什么这个超参组合胜出这些信息必须留在自己的项目体系里。我采用的方式是“轻量自建工具借力”工作流编排和核心逻辑自己写而实验追踪、数据版本这类成熟能力直接用开源工具的后端。这样既避免了从零造轮子的重复劳动又保证了整体流程的可控性和清晰度。1.3 目录结构背后的分层思想项目目录一开始就很折磨人我前前后后重构了三四版最后沉淀下来的结构是这样ai-engineering-from-scratch/ ├── configs/ # 所有实验、服务、评估的配置文件 ├── data/ # 原始数据、中间数据、最终数据含版本信息 ├── src/ │ ├── data/ # 数据采集、清洗、转换脚本 │ ├── train/ # 训练入口、模型定义、优化器配置 │ ├── evaluate/ # 评估指标、评估数据集、报告生成 │ ├── deploy/ # 推理服务、接口封装、性能优化 │ └── monitor/ # 线上监控、日志采集、告警规则 ├── experiments/ # 每次实验的完整记录配置指标产物 ├── scripts/ # 一键启动、数据校验、环境初始化等脚本 ├── tests/ # 单元测试、数据验证测试、接口测试 └── docs/ # 决策记录、架构说明、复盘文档细心的话会发现我没有单独建一个models/目录存放模型文件。这是刻意的模型文件是产物应该跟着实验记录走而不是散落在某个共享目录里。每次实验跑完模型的权重、配置文件、评估指标、关键参数都归档在experiments/下对应的实验目录中。这样做的直接好处是一个多月后你回来看某次实验能完整还原当时的全部上下文而不是面对一个孤零零的model.pt文件发愁。2. 环境与工具链先把地基夯扎实2.1 Python环境与依赖管理的真正解法AI工程对Python环境的要求比普通后端项目苛刻得多CUDA版本、PyTorch版本、各类进厂库之间往往存在微妙的兼容关系。早期我图省事直接pip install一把梭结果项目跑到一半发现某个依赖悄悄升级导致结果不可复现那种痛感相信很多人都有过。在这个项目里我最终锁定了Poetry 2.x pyproject.toml的方案并且做了依赖分组的精细管理[tool.poetry.group.train.dependencies] torch 2.1,2.4 transformers 4.36,4.44 datasets 2.14,2.20 [tool.poetry.group.serve.dependencies] fastapi 0.100,0.116 uvicorn 0.23,0.30这里有个设计细节值得说明训练和推理的依赖组彻底分开。训练环境需要transformers、peft这类重型库而线上推理追求轻量和稳定根本不需要引入完整的训练栈。如果混在一起每次上线光依赖扫描就要耗去大量时间而且攻击面会大幅增加。环境的可复现还涉及另一个层面基础镜像锁定。我把CUDA 12.1 PyTorch 2.1.x的镜像地址直接固化到Dockerfile里版本号全部写死不加latest标签。可能有人觉得这样不够“灵活”但工程体系追求的就是这种不灵活——确定性永远优于便利性。2.2 实验追踪的选型与配置实录实验追踪工具我对比过MLflow、Weights Biases、TensorBoard三家。TensorBoard在可视化方面依然能打但把超参、指标、模型产物整合在一个地方的能力偏弱更像一个训练过程的查看器WB体验最好但团队协作时的费用和私有化部署对个人项目或中小团队来说是个门槛。最后选了MLflow原因是它在满足核心需求的前提下保持了不错的开放性和本地部署能力。初始化逻辑我封装在了一个函数里方便每次实验复用import mlflow def init_experiment(experiment_name, tagsNone): mlflow.set_tracking_uri(file:///path/to/mlruns) mlflow.set_experiment(experiment_name) if tags: for k, v in tags.items(): mlflow.set_tag(k, v) with mlflow.start_run() as run: return run.info.run_id def log_params_and_metrics(params, metrics): mlflow.log_params(params) mlflow.log_metrics(metrics)需要特别提醒的一个坑MLflow的tracking URI如果设置在项目目录内部模型产物会随着项目仓库越滚越大。所以我单独指定了一个项目外的路径做存储目录本身加入.gitignore防止仓库被撑爆。这属于用了工具之后才会发现的实操教训文档里一般不会主动提。2.3 数据版本管理数据也是代码数据版本控制是这个项目里最容易被低估的部分。传统开发场景中代码有Git管着出问题随时回滚。但AI工程的数据集尤其是经过多轮清洗、标注、增强的版本如果不做版本管理有一天你发现线上效果变差了可能根本说不清是哪一版数据、哪一次处理逻辑造成的。我这边直接引入了DVCData Version Control。它的核心理念和Git一脉相承但针对的是大文件。我用它管理data/目录下所有原始数据和中间产物dvc init dvc add data/raw dvc add data/processed git add data/raw.dvc data/processed.dvc .dvc/config git commit -m chore: track raw and processed dataDVC的设计有一个很聪明的点仓库里只存.meta信息文件哈希、依赖关系等实际数据存在本地或远端存储我配置了S3兼容的MinIO所以Git仓库本身保持轻量但完整的数据链路却随时可以恢复。我实际跑过的恢复流程是从空白环境开始dvc pull几分钟内就能拿到全部数据这个体验能让人对“数据即代码”有很真切的感受。3. 数据工程决定模型上限的隐形杠杆3.1 数据从哪里来获取渠道与合规底线这个项目的定位既然是工程体系搭建我没有纠结于某个特定领域的数据而是做了两条数据线一条是公开数据集的标准接入流程另一条是针对业务场景的自采数据流程。前者主要用来验证pipeline本身跑得通后者是真正解决问题的部分。自采数据这边我会先写一个采集脚本把用户反馈、工单记录、历史问答等结构化信息聚合起来。采集第一版时很容易犯一个错误看到什么抓什么导致后续清洗工作量爆炸。我后来设定了三个前置条件再做采集每个字段都明确业务含义和存储格式每条数据都记录来源标识和时间戳敏感信息在采集端做脱敏而不是等入库后再处理脱敏这块尤其要留意不是简单的正则替换就完事涉及姓名、电话、地址这类信息建议用PII检测库先做识别再做规则化替换否则漏网的概率很高。3.2 清洗与去重的实操链路不管什么来源的数据进到pipeline之后都要过一套固定的清洗流程。我抽象成了几个串联的Filter组件每个组件只做一件事class DataCleanPipeline: def __init__(self, stages): self.stages stages def run(self, dataset): for stage_name, stage_fn in self.stages: dataset stage_fn(dataset) log_stage_stats(stage_name, dataset) return dataset实际应用中我常驻的清洗阶段有五个格式标准化统一编码、日期、数字格式、缺失值处理区分删除和填充的策略、去重MinHash 精确匹配组合、内容质量过滤长度、语言、重复度等硬指标、敏感信息扫描。其中内容质量过滤我额外做了一个困惑度perplexity的参考指数专门用来剔除那些语法正常但语义混乱的文本。这个方法对中文数据同样适用效果算得上立竿见影。去重环节有一个容易被忽略的坑全局去重之后训练集和验证集之间还可能有高相似度的样本。如果不处理验证集的指标会虚高模型的真实泛化能力被高估。我用的方案是MinHash LSH做模糊去重把阈值设在0.8以上才算真正重复。跑完这个步骤再划分数据集模型效果评估才有参考价值。3.3 数据质量验证与其信感觉不如断断言数据清洗完之后直接送进训练往往会在中途爆炸。我在框架里加了一个独立的validate_dataset()阶段用一套断言清单在训练前完成校验。它包含的内容很直接数据集大小是否达到最低要求、目标列分布是否严重失衡、文本字段的缺失率是否低于阈值、格式是否符合模型输入要求、是否还有潜在的重复样本。这些断言全部失败时pipeline会自动终止并给出报告。这个设计在项目初期帮了大忙——有好几次是上一轮清洗引入的逻辑漏洞导致数据异常如果没有前置验证训练跑到一半才发现问题浪费的时间都是按小时算的。另外一个从实践里得到的经验所有清洗和转换步骤必须是确定性的也就是说同样的输入必然得到同样的输出。任何引入随机性的操作除了显式的数据增强都会破坏整条链路的可复现性。这也是为什么我在代码注释里明确标注了哪些函数必须保持纯函数语义。4. 模型训练与微调从baseline到可控迭代4.1 基座模型与微调方式的选择逻辑模型选型是整个项目中最容易被情绪左右的部分。今天这个新模型出来了明天那个开源了如果每次都要迁移一次工程体系就会一直处于重建状态。我定了一个原则基座模型的选择以“生态成熟度”和“硬件友好度”为准而不是看评测榜单的数字波动。在这个思路下微调框架选了Hugging Face的Transformers PEFT这套组合基座模型以7B量级为主——这个量级的好处是单张消费级显卡24GB显存就能跑LoRA微调同时效果和推理成本处在相对理想的平衡点。选择LoRALow-Rank Adaptation而非全参微调是综合权衡的结果全参微调需要的内存以及对基座模型知识的灾难性遗忘风险对一个工程化项目来说都不可控。LoRA的思路很好理解冻结原始模型参数只训练注入的低秩分解矩阵效果上在多数任务中能达到接近全量微调的水平但训练参数量只有整体参数的百分之一级别。这背后的逻辑就像一个公司原有部门架构不动只是空降几个高权限顾问去协同处理新任务原有业务完全不受影响。4.2 LoRA训练的超参配置与训练脚本骨架我直接给出一份经过实际验证的配置样例这个配置在7B模型 单卡A100或4090场景下表现比较稳妥model: base_model: your-base-model-id max_length: 2048 load_in_8bit: false lora: r: 16 alpha: 32 dropout: 0.05 target_modules: [q_proj, k_proj, v_proj, o_proj] training: batch_size: 4 grad_accumulation_steps: 8 learning_rate: 2e-4 lr_scheduler: cosine num_epochs: 3 warmup_ratio: 0.03 optimizer: paged_adamw_8bit logging_steps: 10 save_steps: 200这里r16、alpha32的组合是LoRA论文里的常见推荐值。alpha/r的比值等于缩放系数alpha设成r的2倍在多数任务中能达到既保持适应能力又不至于过拟合的平衡。特别注意学习率全参微调通常用1e-5到5e-5的量级而LoRA因为只更新少量参数学习率可以放宽到1e-4到3e-4再高就容易震荡。整个训练过程我封装成了一个入口关键是保证一个epoch结束后除了保存模型还要跑一次完整的验证集评估把checkpoint的指标记录到MLflow。这样每个checkpoint的好坏一目了然而不是全部跑完再回头找最优的。4.3 训练过程中的关键监控与断点恢复在长时间训练中我最怕两类问题Loss突然变成NaN以及显存溢出导致进程被杀。这两类问题的共性是如果等到训练结束时才发现代价就是几天的计算资源白费。所以整套训练框架里必须有自动监控和断点恢复机制。我做了一个轻量的训练监控器每10个step记录一次梯度范数gradient norm和Loss值。梯度范数如果骤然飙升通常意味着学习率过高或数据里有异常样本。NaN检测一旦触发立刻保存当前状态并终止训练。另外checkpoint每200步存一次这个频率看起来频繁但磁盘开销完全可以接受换来的是一旦崩溃最多只丢失最近200步的进度。断点恢复我使用的是Transformers的Trainer自带逻辑但如果手写训练循环需要自己维护optimizer_state和scheduler_state。这里的实操建议是如果你刚开始搭框架尽量直接用Trainer来做不要手写训练循环。手写的灵活度高但稳定性和恢复机制都靠不住。5. 评估体系与可观测性把“感觉还行”变成数字5.1 离线评估集怎么搭才不“自欺欺人”评估是整个工程体系里最容易被做成一纸空文的环节。常见做法是挑几个指标、跑一下测试集、出一份报告就完事但这样的评估对决策几乎没有帮助。我在项目中搭了三层评估体系第一层是任务指标。比如分类任务看F1、生成任务看ROUGE/BLEU。这些指标胜在标准化可以横向对比。第二层是业务规则指标。模型输出在业务语境中是否满足硬性约束比如是否包含指定格式、是否拒绝了不该拒绝的请求、是否出现敏感词等。这一层用规则代码直接判定生成一个pass/fail比率。第三层是人工评测集。我挑了一批高难度的代表性样本不超过200条定期由人工按规范化评分标准打分。这个环节的意义在于自动指标经常会与真实用户感受脱节而人工评价能够给整体质量提供一个方向性的校正。评估脚本统一通过命令行触发每次评估的结果都会写入MLflow跟训练超参、数据集版本关联起来。这样回归对比就有了数据基础不会出现“我印象里上次表现挺好的”这种无法追责的判断。5.2 可观测性设计推理服务不是黑盒模型上线后最怕的一件事是线上效果变差了但没有任何线索能定位到原因。所以在设计推理服务时我坚持把可观测性直接编码到每一个请求的生命周期里。我基于FastAPI搭了推理服务中间加了四个核心观测点请求日志记录prompt长度、响应耗时、模型返回的token数、推理框架的排队时间性能指标QPS、P50/P95/P99延迟专门放置一个中间件做采集输入分布漂移检测对线上输入的文本做embedding然后与训练分布做距离比对错误审计对超时、空响应、异常输出建立独立日志通道尤其是输入分布漂移这一点很多团队会忽略。模型效果下降往往不是因为模型本身坏了而是线上请求的分布跟训练时不一致。比如你训练时处理的是短问题线上突然涌进来大量长文本表现肯定会掉。这个信号前置发现得越早介入调整就越从容。5.3 回归守护让每一次迭代都有据可依为了守住模型更新的质量底线我建立了一个基于历史版本对比的回归测试机制。每次新的模型版本准备上线前都会强制跑两套对比一套是黄金数据集回归固定200条高置信度样本新旧模型都跑一遍对比任务指标和业务规则的通过率。任何一项指标低于旧模型就要给出合理解释。另一套是随机样本抽样人工盲评在测试集上随机抽50条不标注模型版本让人或让LLM作为裁判做质量对比。这个做法能避免“更新了但感觉不出来好在哪”的问题。回归测试报告会自动生成一个简洁的markdown文档包含新旧版本的指标对比、典型差异案例以及推荐结论上线、继续调优或放弃。这个过程已经从纯粹的人工判断演变成有数据支撑的决策流程对团队协作尤其重要。6. 部署与服务化让模型真正跑起来6.1 推理框架选型吞吐优先还是延迟优先部署环节首先需要决策的是推理框架。同一个模型在不同框架下的延迟表现能差出好几倍我先后对比了原生Transformers pipeline、vLLM和TGIText Generation Inference最终按场景做了拆分。如果只是内部调试和低频调用直接用Transformers的pipeline或简单的FastAPI封装胜在简单不用额外维护推理服务。如果是面对真实用户的高并发请求我会优先推荐vLLM。它的PagedAttention机制能把显存利用率大幅提升吞吐量相比原生方案有数量级差异。一个直观的例子同样一张卡Transformers pipeline同时只能处理少量请求而vLLM可以高效并发数十个请求平均每个请求的排队时间大幅下降。这里给一个实际选型建议不要盲目追求吞吐指标。如果你的业务对延迟极其敏感、请求量集中在少数时段那么自建的小规模推理服务加上合理的队列控制就够用了如果用户规模大且波动明显vLLM这类专门优化过的引擎更有优势。6.2 性能优化三板斧批处理、缓存与量化模型服务上线后用户可感知的响应速度会直接影响留存所以性能优化是部署环节的重头戏。我把常用的优化手段归纳为三板斧。第一板斧是动态批处理Continuous Batching。单条请求逐个推理GPU利用率一定很难看。vLLM等框架天然支持动态批处理它会在不严格等齐一个batch的前提下把多个请求塞进同一轮前向计算大幅提升吞吐。我自己实测的项目里开启动态批处理后同等显存下QPS提升接近3倍。第二板斧是语义缓存。对于文本生成这类任务相似度极高的重复请求占相当比例。我在服务层加入了基于向量相似度的缓存机制只有当新请求与缓存中的历史请求相似度低于阈值时才真正调用模型。注意这个缓存必须有失效策略并加一个置信度门控否则可能给用户返回过时结果。第三板斧是模型量化。把模型从FP16压到INT8或INT4显存占用直接减半甚至更多。如果硬件支持实测INT8量化在7B模型上损失通常能控制在可接受范围换来的是部署成本明显下降。6.3 从Notebook到Docker一份可上线的部署骨架部署环节我踩过最多的坑是环境不一致“我在Notebook里跑得好好的怎么到容器里就报错了”为了解决这个问题我最终固定了一份Dockerfile模板核心是锁定三维环境基础镜像、Python依赖、非Python系统库。容器启动之后我额外绕开了默认的8000端口直接让服务监听内网端口由上层网关统一路由。这个设计虽然看起来多余但在多模型同时部署时会非常有用因为各模型服务的端口可以随意扩展而不互相争抢。上线前的最后一道工序是压测。我用locust写了一个简单的压测脚本模拟不同并发量的请求重点关注P95延迟和错误率。压测数据会存档方便后续每次模型更新后做横向对比。7. 常见问题与排查技巧实录7.1 踩坑速查表我把这个项目推进过程中碰到的高频问题整理成了表格直接说现象、原因和对策现象可能原因排查与对策GPU显存OOMbatch_size过大或序列长度过长调低batch_size、开启梯度累积用torch.cuda.max_memory_allocated()定位占用量Loss不下降学习率过高/过低或数据标签错误先跑较小数据子集确认模型能拟合再检查数据标签分布训练几轮后Loss突变为NaN学习率过高导致梯度爆炸减小学习率增加梯度裁剪检查是否混入缺失值模型微调后通用能力下降LoRA rank过大或学习率过高降低rank值减少微调轮数加入通用语料做混合训练线上效果与离线评估差距大输入分布漂移或评估集太简单重新采样线上数据到评估集用K-S检验或向量距离监测分布漂移推理延迟突然升高batch排队或CPU/GPU切换异常查请求堆积数开启动态批处理检查是否有长文本请求阻塞容器启动即崩溃CUDA版本与PyTorch不匹配锁死nvidia/cuda镜像Tag本地用nvidia-smi核对驱动版本这个表不用全背下来但值得贴在项目文档首页。很多问题之所以耗时长不是因为难而是因为一开始方向就错了。有了这个速查表至少能快速定位到正确的排查路径上。7.2 一个真实的排查案例模型“变笨”之后分享一个我在项目中真实遇到的案例。有一版模型上线后用户反馈质量下降但离线测试集的指标几乎没有变化。按常规思维容易首先怀疑模型是不是被微调坏了但我的观测系统给出了另一个线索线上请求的输入长度分布发生了明显右移大量请求接近模型上下文上限。顺着这个线索我很快定位到真实原因业务方上线了一个新功能会把更多历史对话塞进prompt导致平均输入长度激增。模型在长上下文下的注意力稀疏效应和处理能力确实不如短文本效果下降是系统性的。最终解决方案是滑动窗口截断策略只保留最近几轮对话同时为长文本场景单独做了一个摘要模型前置处理。这个案例给我的启发很直接模型效果下降时先看输入侧变化再查模型本身。很多时候“模型变笨”只是表象真正的变量在数据的分布侧也就是数据工程和观测设计就位的意义所在。7.3 框架性建议哪些东西值得提前投资从零搭建这套体系耗时最多的其实不是写代码而是反复的架构权衡。如果让我重新来一次我会把前期投入集中在三方面第一数据版本控制和实验追踪必须最早搭建。这决定了项目几周后是否还能看懂自己的历史记录。第二评估体系要尽早做。很多人先训练出模型再想怎么评估这时可选的改进空间已经很小了。更好的做法是数据准备好之后先把评估脚手架搭出来即使是baseline模型也有对比参照。第三用一份详细的“决策记录”ADR记录关键选型背后的理由。这是我个人比较受用的习惯比如当时为什么选DVC不选其他工具、为什么这个任务用LoRA而不是全参微调每一条都记录下来。三个月后回看这些记录比代码本身更能帮你理解整个体系的演进逻辑。我个人在实际整理这份框架时最大的感触是所谓“从零开始的AI工程”真正复杂的地方不在于某一个算法或工具用得多熟而在于整个生命周期的每一个环节都有清晰的决策依据和可复现的执行路径。很多团队不缺牛人缺的是把牛人的能力沉淀为体系的机制。希望这份实战拆解能让你在搭建自己的AI工程框架时少走几步真正的弯路。
返回列表