ARTICLE DETAIL

资讯详情

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

AI工程从零到一:数据管线、模型部署与监控迭代实战

AI工程从零到一:数据管线、模型部署与监控迭代实战 刚做这个项目的时候我给自己起了个特别长的标题ai-engineering-from-scratch。说白了就是一句话——把从零开始做AI工程这件事亲手从头到尾走一遍并记录下来。过去几年我见过太多人一头扎进模型调参结果代码能跑系统却上不了线我也见过不少团队花大价钱买了全套现成方案出了问题连日志都不知道去哪看。这个项目就是冲着这些问题去的不要全家桶平台不要一键部署的魔法从一个干净的命令行开始把数据、训练、部署、监控、迭代这条链路全部打通每一步都尽量解释清楚为什么这么做。这篇文章写给三类人算法工程师想补工程化短板、后端或全栈想切入AI工程、以及所有被一行代码跑通模型骗过、真上手却无从下手的人。我会从项目为什么起这个名字开始讲数据管线怎么搭、训练实验怎么管、模型怎么变成稳定服务、上线后怎么监控和迭代最后再聊到当前更热门的LLM应用工程化看看这套从零打下的地基还在不在用。1. 从会用模型到能交付系统这个项目到底在解决什么问题1.1 我见过最多的AI项目死于哪里先说我观察到的现象。很多AI项目死掉不是死在模型效果不好而是死在系统工程缺位。最容易见到的一幕是算法同学在Jupyter Notebook里把准确率调到了95%然后把这个notebook丢给后端同学说模型做好了帮我部署一下。后端打开一看里面全是魔改的预处理逻辑、硬编码的路径、没有固定的随机种子依赖装了三屏还缺版本锁定。结果光是把环境复现出来就花了一周好不容易跑通了线上效果又和离线完全不一样。还有一种死法更隐蔽模型上线第一天表现不错一个月后用户投诉量悄悄上涨但没人知道是数据分布变了、规则改了、还是上游输入多了某种新格式。没有监控、没有日志、没有版本回滚机制整个系统像一个黑盒谁也不敢动它。这些项目缺的从来不是某个模型结构或某个新算法而是工程这两个字。而工程恰恰是学校里不教、教程里很少系统讲、只能靠大量实操踩坑攒出来的东西。我的这个项目目标就是把AI工程这条链路里每个环节用最朴素的方式走一遍搞清楚它们为什么存在、怎么设计才可靠。1.2 AI工程能力拆开看数据、模型、部署、监控、迭代做这个项目之前我先把AI工程的能力拆成了五个模块每个模块对应一类典型问题。这张表是我后来整理给别人看的也是我自己搭建路线图时的底稿模块要解决的典型问题最容易出现的失败姿势数据工程数据从哪儿来、干不干净、怎么持续迭代拿一份Excel硬喂给模型不看出处和分布模型开发训练过程可复现、效果可评估在notebook里调出好效果但实验记录全在脑子里模型部署模型变成稳定可靠的线上服务交付一个脚本让后端自己琢磨监控迭代线上质量劣化能感知、badcase能回流上线后不闻不问直到用户骂上门项目治理数据/模型/代码/文档如何版本化协同全凭记忆管理三个月后自己也看不懂你会发现不管模型算法怎么换这五块骨架基本不变。哪怕今天大家都在做大模型应用底层的数据管理、可观测性、评估闭环、灰度发布依然是同一套思路只是换了一批更上层的工具。1.3 项目给自己定的三条红线为了避免这个项目变成又一个收藏夹里的教程我给它立了三条硬规矩第一最小依赖。不引入机器学习平台全家桶能自己用脚本解决的就不上重工具。比如实验管理一开始就用一个Markdown表格跑通了再考虑要不要升级到专业工具。这样做的目的不是排斥工具而是逼自己在用拐杖之前先学会走路。第二可复现。仓库里任何一个脚本在另一台干净机器上clone下来按README执行应该能跑出一样的结果。数据版本、依赖版本、随机种子、配置参数全部锁死。这一条看着简单做起来比想象中难十倍。第三能解释。每个关键决策都要写清为什么。为什么选这个模型、为什么这样划分数据、为什么阈值定在0.6而不是0.5。不写这些项目三个月后回头看就是一堆没有灵魂的代码。这三条红线贯穿了项目全程后面所有篇章基本都是围绕它们展开的。2. 数据管线先行一个可用数据集是怎么从零搭出来的2.1 为什么数据的工作量占到一半以上真正动手之后我对数据决定模型上限这句话有了更具体的感受。模型结构再花哨喂进去的文本是脏的、重复的、带有泄漏的后面所有工作都是给烂地基盖楼。我给自己选了一个非常适合起步的任务中文短文本分类。具体是判断一条用户反馈属于咨询、报障、建议、表扬中的哪一类。选这个任务有几个原因业务语义简单、不需要额外领域知识、数据容易获取和构造、而且类别之间有真实的不平衡性方便后面练习处理这类问题。让我特别意外的是数据这块的工作量。整个项目时间分配下来数据获取、清洗、标注、版本管理差不多占了一半剩下的才是模型训练和部署。这不是因为我效率低而是真实业务里的AI工程本来就是这副样子。那些看起来只需要跑一下脚本的数据集背后都有一堆看不见的脏活累活。2.2 从0到1搭建短文本分类数据管线的具体步骤我按一套尽量朴素、但足够规范的流程来建设数据。第一步是数据获取。我先拿了一份公开的短文本语料池大概几万条然后结合业务规则生成了一批模拟用户反馈的伪标注数据后续用人工标注来校验质量。第二步是清洗。这一步极其枯燥但收益非常直接。我按顺序处理了空文本、表情符号和URL统一化、超长文本截断、以及去重。去重是重点不能只做完全相同的文本去重还要处理只改了一两个字的近似重复。我一开始用了一个简单有效的办法把文本做字符级别hash后比较后来为了效率升级成simhash思路。贴一个最基本版本的逻辑import hashlib def dedup_by_text(texts): seen set() for text in texts: normalized .join(text.split()) digest hashlib.md5(normalized.encode(utf-8)).hexdigest() if digest not in seen: seen.add(digest) yield text第三步是标注。人工标注了约一万两千条还专门写了一份标注规范文档里面定义了边界情况——比如既是咨询又是报障的反馈怎么归类。这类模糊case如果不提前约定两个人会标出两套结论模型学到的就是噪声。第四步是数据划分。这步非常关键我先按时间顺序划出最近一段时间的数据作为测试集模拟线上真实分布再在剩余数据上随机切出训练集和验证集。很多教程直接给你随机切分在真实场景里随机切分很容易高估效果因为线上永远是未来的数据不会和训练数据同分布。第五步是数据版本管理。每个数据集版本以一个JSON清单文件为准类似这样{ dataset_id: feedback_cls_v1.2, source: public_pool_and_rules_v1, total_records: 29354, label_distribution: { 咨询: 0.40, 报障: 0.30, 建议: 0.20, 表扬: 0.10 }, changelog: [ { date: 2025-03-02, change: 建立初始版本完成去重与基础清洗 }, { date: 2025-03-11, change: 修订标注规范补标300条模糊case }, { date: 2025-03-18, change: 按时间重划验证集修复泄漏 } ] }每个版本都给一个全局唯一的dataset_id代码和实验记录里引用数据时都用这个id而不是写死路径。这样模型版本和数据版本能对应起来排查问题的时候才知道当时用的是哪份数据。2.3 数据版本管理与三个真实踩坑记录说几个我实际踩过的坑每个都值得单独拿出来讲。第一个坑是标签泄漏也最隐蔽。我在做特征工程时加了一个文本是否带有问号的规则特征训练时发现它对咨询类判断特别有效。后来仔细一想线上推理时这个特征虽然也能算出来但用户反馈里问价格和问售后政策本质上都是咨询靠问号判断会让模型学到一个虚假规律。我还在训练数据里把业务方人工打好的分类标签当特征塞进去过那个更离谱属于拿到了标准答案当输入。这类泄漏会极大高估离线效果上线后直接打回原形。第二个坑是重复样本导致的评估虚高。去重前验证集准确率97%我心里还挺美。后来发现数据里大量相似文本同时出现在训练集和验证集模型记住文本就能拿分。把近似重复清理掉之后准确率一下子掉到了88%。虽然数字变难看了但这个88%才是线上真正能兑现的效果。第三个坑是数据时间分布的漂移。初版模型用较早的数据训练过了几个月用户用语习惯变了冒出来很多新词模型对这些新词非常不稳定。后来我在监控里加了一个简单的文本特征分布检查定期对比线上文本长度、高频词和类别分布与训练集的差异明显超过阈值就提醒该考虑重训了。这段经历给了一个很深的体会数据管线不是一次性工程它和代码一样需要持续维护。数据版本清单和changelog从一开始就坚持写后面排查问题会感谢当初的自己。3. 模型训练的实验骨架把训练代码写成训练系统3.1 最小训练工程骨架长什么样数据准备好之后进入模型环节。这个项目我刻意没有一开始就去追SOTA而是用一个能让整条链路先跑起来的最小骨架。项目目录结构大致如下configs/baseline.yaml data/ # 不直接提交数据集用下载/准备脚本生成 train.py evaluate.py models/checkpoints/ runs/logs/ experiments.md核心思路是训练脚本从配置文件读参数不写死在代码里。一份配置文件长这样data: dataset_id: feedback_cls_v1.2 train_path: data/train.csv val_path: data/val.csv model: name: bert-base-chinese max_length: 128 train: batch_size: 32 epochs: 5 learning_rate: 2e-5 seed: 42固定随机种子这一步看起来是在给自己找麻烦实际价值非常大。不固定种子的话那么同一份代码跑两次结果不同你根本无法判断效果变化是来自参数调整还是运气波动。和依赖锁定配合使用别人clone下来跑结果基本能对上。训练部分我用了带权重的损失函数来处理类别不平衡。数据里咨询占40%表扬只占10%如果不加权模型天然偏向样本多的类别。加权之后的直观变化是少数类的召回率上去了整体F1也涨了一点。这是第一次感受到模型结构不是唯一杠杆数据配比和损失函数设计同样重要。3.2 实验记录没有实验管理的调参就是盲人摸象训练过程中我坚持在experiments.md里维护一张实验记录表。这个习惯救了我很多次。表格长这样实验数据版本模型/改动验证F1结论E001v1.0词向量LR0.72基线跑通作为参照E005v1.0BERT微调0.88预训练模型优势明显E008v1.2BERT微调加权损失0.91报障类召回显著提升E010v1.2E008基础上调阈值0.92阈值0.62比默认0.5更优记录的重点不光是精度指标还有每个实验的badcase观察。比如E005虽然整体F1到0.88但去看混淆矩阵会发现建议和咨询经常混淆原因是很多建议类文本确实带着咨询语气。这个观察直接推动了后续在阈值和规则上做调整。我甚至有一次因为没记实验参数吃过亏。当时调了一版hidden_size以为顺手能记住结果一周后想回滚找不回来只能靠git历史反推。从那以后任何一次运行不管效果好不好都要在表格里留下一行写着改了什么、为什么改、结果如何。没有实验管理的调参本质上就是盲人摸象。3.3 评估里的学问准确率达标上线不一定达标初学者评估模型最爱报准确率但在不平衡分类任务里准确率很容易骗人。假如90%的样本都是咨询模型全都预测成咨询准确率也有90%看起来很美但真正的报障全被吞了业务上是不可接受的。所以评估一定要看每类别的精确率和召回率外加混淆矩阵。我定义了一个漏报率指标真正需要人工介入的报障类模型漏掉了多少比例。漏掉的报障意味着用户投诉被错误归档这比误报成本高得多。这块想清楚之后很多技术选型自然有了方向。阈值调整也是评估里很重要的一环。默认的0.5概率阈值对业务不是最优的我最终的策略是对报障类降低阈值宁可偶尔误报也不要漏掉真正的问题同时设了一个不确定通道置信度低于0.4时模型不硬给分类结果而是走人工处理。这相当于让模型知道自己的知识边界比逼它强行输出一个答案要稳得多。最后我还做了一次模拟上线评估把最近一个月新产生的数据单独作为测试集不看训练时见过的任何样本。这个测试集上的结果才是我愿意写进上线汇报里的数字。4. 从离线到在线模型服务化与监控闭环的落地过程4.1 最小可用的推理服务接口、预热、批量与缓存的取舍模型在离线评测里达到了预期之后就要解决怎么让业务真正用起来的问题。我写了一个最小的推理服务直接用FastAPI搭建宗旨是简单、可依赖、能观察。一个简化版本长这样from fastapi import FastAPI from pydantic import BaseModel from model_wrapper import ModelWrapper app FastAPI() model ModelWrapper() class PredictRequest(BaseModel): text: str app.get(/health) def health(): return {status: ok} app.post(/predict) def predict(req: PredictRequest): return model.predict(req.text)这里面有个很容易被忽略的细节模型预热。模型加载和第一次推理通常比后续慢很多如果服务一注册到负载均衡就被打进生产流量第一个请求往往直接超时。所以服务启动时要主动调一次预测函数把模型组件全部加载初始化完再对外宣称健康。在线推理和批量推理的取舍也很实际。对实时性要求高的场景走HTTP接口对成本敏感的离线场景走批量脚本。还有一个容易被忽略的点是缓存高频的重复请求比如同一段短文本被多次查询直接用缓存打回去成本和延迟都能大幅下降。我在服务里加了一层简单的LRU缓存命中率大概在12%左右别小看这12%高峰期能把后端压力降下来不少。4.2 上线不是终点日志、监控与badcase回流模型服务上线的那一刻我完全没有终于结束了的感觉因为真正的工作才刚刚开始。我给每个请求增加日志记录按JSON lines格式输出包含request_id、模型版本号、输入内容做了脱敏和截断、预测结果、置信度、延迟、时间戳。日志是监控的基础更是后面回流训练数据的重要来源。没有日志线上出任何问题都只能靠猜。监控主要看几个指标请求量、P95延迟、置信度分布、人工介入率、以及预测类别的分布变化。人工介入率尤其重要这个指标一旦持续升高通常意味着线上出现了模型把握不住的新情况。我按维度建了监控看板设置了告警阈值比如P95延迟超过200毫秒或者置信度均值明显走低。这里特别强调一下badcase回流机制。我设计了一个非常轻量的人工打标流程业务方日常使用时可以对模型结果点纠错这些纠错记录定期导出人工确认后合入训练集形成线上badcase - 新标注样本 - 增量重训 - 灰度上线的闭环。这个机制不复杂比任何花哨的平台都实用它保证了模型会随着时间推移越来越贴近真实数据分布。4.3 给模型上CI/CD从手动跑脚本到自动化流水线模型交付到生产环境不能靠今天手动跑一下脚本、明天手动复制文件。我给项目搭了一条最小化的流水线阶段划分很简单数据变更触发训练训练完自动跑评估脚本评估满足门禁后构建包含模型和服务的镜像再推送到部署环境做灰度。一开始我没有急着上框架而是用命令行加定时任务手动执行等流程完全稳定了才加了自动触发。很多团队踩过的坑是第一天就配置了一大堆自动化任务结果脚本本身不稳定自动化任务常年挂红灯慢慢就没人看了。我的建议是先让流程在手动状态下稳定跑两周再自动化。灰度发布也是必须的一环。新版本模型不会直接替换全量流量而是先切10%流量跑一段时间对比新旧版本的日志指标确认没问题后再逐步放量。整套CI/CD实践下来最大的收获是模型交付从一件令人紧张的事情变成了一个走标准流程的日常操作。5. LLM应用工程化同样的地基换了一批上层建筑5.1 传统ML工程与LLM应用工程的对比做完前面几章的地基之后正好赶上大模型应用爆发我开始把同样的工程思维迁移到LLM应用上很快就发现一个结论地基完全还是那套东西但上层玩法变了。我整理过一个对比表格来表达这个感受维度传统ML工程LLM应用工程数据准备瓶颈在人工标注成本高瓶颈在文档解析、切分、清洗模型自己训练参数可控调用外部API或基础模型行为不完全可控评估标签可枚举指标精确生成结果开放需要规则模型裁判服务固定输入输出结构需要拼接上下文令牌吞吐和成本都要看迭代重训周期长依赖标注闭环Prompt和检索优化迭代节奏快最核心的工程骨架也就是数据管理、实验记录、可观测性、灰度发布、badcase回流一点都没变。变的只是每个环节的具体处理对象。5.2 RAG系统的最小工程闭环与评估陷阱LLM应用里最常见的一类系统是RAG也就是检索增强生成。它解决的是让模型基于自有知识库回答问题的需求。我从零搭过一个最小的RAG服务整个闭环分成四块文档库构建、离线检索评估、在线增强生成、badcase回流。文档库构建的第一步是解析和切分。我一开始图省事按固定长度直接切文本结果发现很多答案被拦腰切断检索效果很差。后来改成优先按语义段落切段落太长再二次切分保留小标题和上下文引用信息检索命中率明显提升。这一步和传统ML里的数据清洗性质完全一样看着不起眼实际决定上限。评估是RAG系统里最容易自欺欺人的地方。我构建了一份评估集包含几百条典型业务问题每一条都标注了标准答案应该出自知识库的哪个文档块。离线评估主要看top-1和top-5的检索命中率。只有检索阶段能稳定找到正确内容生成阶段才有意义。更复杂的是对生成结果的评估。生成回答不像分类任务有标准标签很难直接算准确率。我采用的方案是规则LLM裁判混合评估规则检查引用是否真实存在、答案是否包含关键实体对于内容质量让一个大模型按照给定维度打分同时要求它输出打分理由。LLM裁判本身有偏好偏差所以还需要人工抽检一部分结果做校准这个过程和传统ML的人工复核几乎一模一样。RAG系统的可观测性也很重要。在线日志里要记录检索到了哪些文档块、每个块的得分、最终输入给模型的是什么prompt片段。这样一旦答案质量下降我们至少能定位是检索问题、提示词问题还是模型本身问题。5.3 给后来者的路线建议先打通一次最小闭环如果让我给也想做from-scratch式AI工程的人提一条最朴素的建议那就是不要先囤工具不要先啃框架找一个你能拿到真实数据和真实反馈的小业务场景把前面四章的内容手工打通一遍。这个项目做下来我最大的体会是AI工程不是某个瞬间的顿悟而是一长串枯燥但必要的决策和复盘。今天你可能在清理数据中的重复项明天在调整阈值降低漏报率后天在日志里发现一种之前没见过的输入格式——这些事情单看都不起眼但串起来就是一个系统真正稳定的原因。最后再分享一个很小的实操习惯维护一份自己的decision log每次遇到问题、尝试方案、做出决策都随手记几行。它的价值要到一个月后、半年后才显现出来——当你能准确说出当时为什么这么选的时候你才真正算得上在做一个工程而不是在碰运气。这份日志就是我所谓from scratch最实在的产物。
返回列表