首页 / 资讯中心 / 文章详情

从零搭建AI工程:文本分类系统的完整落地实践

从零搭建AI工程:文本分类系统的完整落地实践 ★ FEATURED ARTICLE
去年年中接了一个挺有意思的任务在现有系统里从零搭一套AI能力。当时技术圈正流行从零构建推理模型、从零手写大语言模型这类话题我也一度以为项目的难点会在模型架构上结果真做下来才发现所谓ai-engineering-from-scratch绝大多部分不是“从零实现一个算法”而是“从零把一条链路跑通”。这个区别决定了项目是准时交付还是一拖再拖。这篇内容不讲数学推导也不带着你把某个知名模型复现一遍而是记录我从空白目录开始完成一个真实AI工程项目的完整过程。我会以一个小型客户反馈文本分类系统为例讲清楚问题定义、数据与环境准备、模型训练、评估、部署上线、持续维护这几个环节里哪些地方最容易浪费时间哪些决策会直接影响最后能不能用起来。适合正打算在自己团队里从零搭AI能力、或者第一次独立负责AI项目的工程师和技术负责人参考。1. 先别碰模型从零做AI工程的第一张图纸1.1 为什么大多数人启动AI项目时都把问题定义反了我见过很多团队包括我自己最早踩的坑都是拿到一个需求后立刻开始找模型要做客服工单分类就去搜“文本分类SOTA”要做销量预测就去看各种时序模型论文。结果模型还没跑通业务方和工程师先吵起来了因为大家根本说的是两件事。业务方说“帮我做AI”心里想的是“系统能自动判断这条反馈该归谁处理”。工程师听到“做AI”脑补的是“训练一个端到端模型效果惊艳”。这中间缺掉的就是一个能把业务语言翻译成机器学习任务的过程。任何从零开始的AI项目第一步都不应该是选模型而是先把下面这些问题白纸黑字写下来这个功能的输入是什么输出是什么谁消费这个输出当前没有AI的时候这件事是怎么做的规则写的还是人肉处理的做出来之后用什么标准判断它“做好了”我当时跟业务方聊完才发现所谓的“客户反馈自动分类”其实已经有了一套非常粗糙的规则按关键词匹配来分流工单。准确率不算高但能用。AI要做的不是“造一个新轮子”而是“把现有流程里人肉判断最多的那部分自动化”同时把准确率做到可以接受的水平。这个认知一旦对齐整个项目的范围立刻变小了。我们不需要做一个通用对话系统也不需要理解所有语言只需要做好一件事给定一段客服工单文本预测它属于哪一类并给出置信度。仅此而已。1.2 把业务语言翻译成机器学习语言的四步法把问题定义清楚我一般用四步走这个套路在多个项目里验证过基本不会跑偏。第一步明确预测目标。客户反馈分类对应的监督学习任务是“多分类文本分类”类别直接沿用现有业务线的工单类型。我当时梳理下来是五类质量问题、物流问题、售后问题、价格疑问、其他/表扬。其中“其他/表扬”非常重要它给了模型一个垃圾桶避免上线后遇到未知文本被迫硬分。第二步明确数据来源。历史工单系统里积累了上万条人工打标的反馈记录字段包括用户描述、客服处理结果、结案标签。这里要注意结案标签不一定等于用户反馈的类别因为客服在操作时可能选了“已退款”而不是“质量问题”这种标签含噪问题后面还要处理。第三步明确输出形态。业务方真正想要的是一个“带置信度的自动预分类结果”高置信度的直接自动分流低置信度的进人工队列。所以我们的模型输出不能只是类别ID还必须有概率。这直接影响了后面选模型和loss函数时的决策。第四步明确上线后的使用边界。哪些文本允许自动分流答案是一开始只允许那些置信度超过0.9、且不在拒绝类“其他/表扬”里的文本自动走流程。剩下的全部丢回人工。宁可自动率低一点也不能让准确性翻车。这四步做完整个项目才算是有了图纸。后面所有的模型选型、数据处理、评估标准都围绕这张图纸展开。1.3 指标先行离线指标与线上业务指标如何挂钩很多AI项目烂尾是因为离线指标和线上业务指标脱节。模型报告里写着“准确率98%”上了线业务方却说“你们这东西完全不能用”。为什么因为准确率这个指标在样本不均衡的情况下会骗人。如果全量数据里90%是“其他/表扬”模型全预测成这一类准确率也有90%。我当时和业务方一起把指标分成了两层第一层是离线评估指标。多分类场景我主要看两个macro F1 和每个类别的PR曲线下面积PR-AUC。macro F1对样本少的类别更公平不会让大头类别掩盖小类别的烂表现。置信度阈值的选择则通过约登指数或者直接人工划阈值找到一个“自动处理”和“准确率”之间的平衡点。第二层是线上业务指标。这比离线指标更重要自动分流率、自动分流后的准确率、人工转出率。具体来说就是每天进来1000条新反馈系统自动处理掉多少条其中处理对的占多少。这才是业务方真正关心的东西。两层指标之间要有对应关系。比如我们最后定的规则是模型输出概率大于等于0.9才自动分流这个阈值在离线验证集上对应precision大约95%那线上如果实际只有85%说明存在分布漂移需要回头查数据和特征。1.4 定义MVP边界哪些模块第一次必须砍掉从零做AI工程最大的敌人是“什么都想做”。第一个版本如果想把知识库、实时学习、A/B实验平台全部搭好项目大概率会延期到失去意义。我更愿意在定义阶段就把范围砍到最小。我砍掉的第一类东西是在线学习。第一个版本先做离线训练、每日或每周批量更新模型。原因是在线学习涉及样本回流、实时标注、模型版本管理复杂度一下子抬高一个量级但业务上并不急需。砍掉的第二类东西是复杂的模型服务化基础设施。我们第一版直接用FastAPI包一个预测接口不做微服务编排、不做单独的特征平台、不做模型注册中心先保证能跑通闭环。砍掉的第三类东西是超出文本范畴的输入。业务方后来提过要不要识别用户上传的图片证据我直接放到了V2。MVP阶段只处理纯文本输入形式统一为字符串输出统一为类别和概率。砍掉这些模块之后最小闭环非常清晰历史工单数据 → 清洗和特征化 → 训练一个文本分类模型 → 导出模型文件 → 包一个预测服务 → 接入现有工单系统。后面你会发现这条链路虽然简单但里面每一个环节都有足够多的坑等着你踩。2. 环境、数据与工程骨架真正棘手的是让数据可复现2.1 从零开始的目录结构让未来的自己三分钟定位文件从零搭建AI工程我最先做的一件事不是装深度学习框架而是把项目目录结构定下来。这个动作看起来无聊但它决定了三个月后你还能不能找到自己写过的代码。我用的目录结构比较传统你可以直接抄project_root/ ├── data/ │ ├── raw/ # 原始数据永不修改 │ ├── processed/ # 清洗后的数据 │ └── samples/ # 小批量样本用于快速调试 ├── src/ │ ├── data/ # 数据加载、清洗、特征工程 │ ├── models/ # 模型定义、训练、预测代码 │ ├── eval/ # 评估脚本、错误分析工具 │ ├── api/ # 部署服务代码 │ └── utils/ # 公共工具 ├── configs/ # 所有配置用yaml统一管理 ├── experiments/ # 每次实验的记录、指标、输出 ├── models_store/ # 训练产物目录按版本组织 ├── notebooks/ # 探索性分析脚本 ├── pyproject.toml ├── README.md └── .gitignore这个结构最关键的地方是三条铁律原始数据只读、配置和代码分离、每个实验有独立目录。这三条如果守住项目后期不管来了新人还是自己休假回来接手都不会太痛苦。2.2 Python环境与依赖锁定重复可复现是第一原则从零搭AI工程环境的坑往往比模型的坑更早出现。我见过太多项目因为“在我电脑上是好的”这句话而卡壳。我这边现在的标配是pyenv uv pyproject.toml。选uv是因为它比pip快很多而且能生成 lock 文件锁定所有依赖的精确版本。具体初始化步骤很短# 安装 Python 3.11 uv python install 3.11 # 初始化项目会自动读取 pyproject.toml uv init # 添加核心依赖 uv add pandas numpy scikit-learn fastapi uvicorn pydantic joblib uv add --dev pytest jupyter black ruff # 生成 uv.lock所有依赖版本被锁定 uv lock这里我想强调一个很多人忽略的点任何模型训练代码都用到的随机种子必须写进配置并固定。我在 src/utils/seed.py 里写了个简单函数训练入口统一调用保证相同数据、相同参数下能复现结果。这样做实验对比时差异才真正来自代码/参数变化而不是随机波动。对了还有一点不要把.venv和data/raw提交到 git记得写进.gitignore。原始数据大文件我后面会用DVC管理而不是git这个在第2.5节讲。2.3 数据获取与清洗真实业务数据里的典型深坑我们项目的数据来自客服工单系统的导出。导出的CSV有接近五万行字段包括用户反馈文本、客服处理记录、结案标签等。拿到的第一眼我就发现数据比想象中脏得多。踩到的第一个坑是大量空文本。一部分工单是用户上传图片后由客服代填备注正文部分是空的。这种样本没有任何文本特征留着只会变成噪声。处理方式是直接过滤掉字符长度小于10的样本因为太短的内容本身信息量不足强行训练只会让模型学到无意义的倾向。第二个坑是标签不一致。同一类问题在结案标签里却有几种叫法比如“物流慢”“快递延误”“迟迟不发货”其实都属于物流问题但因为历史操作习惯不同被打上了不同标签。处理方法是先把所有标签做一次归一化映射全部统一到我们定义的五大类里。这一步需要业务方参与确认不要自己瞎猜。第三个坑是文本里的空白字符和乱码。有些工单是从网页直接复制过来的包含大量换行、全角空格、特殊符号中文文本里还混着英文单号、数字、客服工号。做第一版分类器时我不打算搞太复杂的清洗只做最小处理去掉边界空白、统一全角转半角、保留中文英文数字和基本标点、把连续换行压缩为单个空格。数据清洗的完整思路我建议用管道式写法每一步都独立可测。下面是一个简化的示例import re def clean_text(text: str) - str: if not text or not isinstance(text, str): return text text.strip() # 全角转半角仅针对常见中文标点场景 text text.replace(, ,).replace(。, .).replace(, !).replace(, ?) # 去除控制字符 text re.sub(r[\x00-\x1f\x7f], , text) # 统一空白 text re.sub(r\s, , text) return text.lower() if text.isascii() else text清洗完后我统计了类别分布五类样本数量从两千到一万五不等典型的类别不均衡。这个分布直接决定了后面模型选型和评估策略而不是等训练完才后悔。2.4 特征工程的最小闭环先做可解释的再做花哨的第一版模型我刻意没有上BERT而是先用TF-IDF向量 分类器。原因很简单从零搭的AI工程第一版要的不是极限效果而是一个能跑通、能定位问题、能解释结果的基线系统。TF-IDF虽然“传统”但它有两个不可替代的好处训练速度快迭代一版只要一分钟特征可解释模型出错时你可以直接看到是哪个词把样本带偏了。特征工程上我只做了两件事第一用TF-IDF做文本向量化。参数我用了比较保守的选择ngram_range(1, 2)max_features20000min_df3。这样做的好处是保留了一些常用二词组比如“快递没到”“售后不理人”这类表达光靠单个词是抓不住的。第二加了几个简单的手工特征。文本长度、是否包含订单号、是否包含感叹号、是否包含“投诉”“起诉”等强意图词。这些特征在逻辑回归里会被赋予明确权重方便上线上做bad case分析时定位问题。我不建议从零项目一上来就上BERT embedding或者图神经网络特征。原因在后文模型部分还会提到复杂度本身就是成本第一版没必要给自己增加排障难度。2.5 数据版本控制为什么我建议用 DVC 而不是存CSV模型训练最怕的一件事是数据改了但没人记得导致旧模型和新模型根本不在同一个数据集上比较。我们把CSV放在网盘共享目录里改来改去的做法迟早会出事。我的做法是用DVCData Version Control管理数据。DVC的使用思路很直接“git管代码DVC管数据和模型产物”。具体操作# 初始化DVC dvc init # 把数据目录纳入版本管理 dvc add data/raw dvc add data/processed # 生成 .dvc 文件后提交git原始数据本身可以放本地或对象存储 git add data/raw.dvc data/processed.dvc git commit -m init data这么做还有个额外好处每次模型训练时我可以在训练日志里记下当前数据的DVC版本号相当于给每一轮实验打了一个“数据指纹”。回归问题时随时能查出“这个实验用的是哪一版数据”。这个习惯在后期排查模型效果波动时帮了大忙。3. 模型训练从零开始不等于从论文开始3.1 第一步永远是强 baseline而不是新模型“从零开始”这个词在技术社区很流行动不动就有人要“从零构建推理模型”“从零手写大语言模型”。但在真实工程里从零开始应该是“从最简单的有效模型开始逐步逼近业务目标”而不是从一篇最新论文的复现开始。我第一版模型用的是TF-IDF Logistic Regressionscikit-learn 的 Pipeline 封装十五分钟就写完训练脚本。让我惊讶的是这个简简单单的模型在验证集上的 macro F1 就到了0.82已经超过了业务方现有的关键词规则不少。这让整个团队对“AI能落地”有了第一手信心也让我后续再上复杂模型时有了对比的锚点。Baseline的选择有个原则宁可简单到被业务方说“这不算AI”也不要复杂到连自己都调不明白。从零项目缺的不是一个炫酷模型而是一条可靠的对比基线。3.2 训练主循环的设计验证集拆分是技术活训练代码本身不复杂真正复杂的是“如何把数据拆成训练集、验证集、测试集”。文本分类项目最常见的错误是随机打乱后拆分但在我们的场景里数据是带时间属性的工单记录随机拆分会导致严重的泄漏。我当时的拆法是这样的按工单创建时间排序取前80%做训练集接下来的10%做验证集最后10%做测试集。把所有样本按时间排序是为了模拟真实场景——模型训练用的永远是历史数据预测的永远是未来数据。from sklearn.model_selection import train_test_split # 按时间排序后的dataframe假设有created_at列 df_sorted df.sort_values(created_at).reset_index(dropTrue) n len(df_sorted) train_df df_sorted.iloc[:int(n * 0.8)] val_df df_sorted.iloc[int(n * 0.8):int(n * 0.9)] test_df df_sorted.iloc[int(n * 0.9):]这个拆分逻辑必须在项目一开始就固化不能被随机采样替代。否则训练时样本分布和上线时遇到的真实分布很可能不一致离线指标白调了。训练循环我也加了点小巧思每次epoch后在验证集上算macro F1同时把训练轮次、学习率、当前F1记录到一个TXT日志文件里。这样哪怕训练中途断了也能看出模型效果的爬升趋势而不是盲目重来。3.3 超参数调试的最小可行做法常规的超参数搜索工具很多比如Optuna、Ray Tune但对于从零项目来说一上来就引入分布式调参框架反而是负担。我第一版只做网格搜索加少量随机搜索把需要调的参数控制在一手能数得过来的范围内。文本分类这个场景真正需要调的参数不多TF-IDF 的max_features和ngram_range逻辑回归的正则化强度 C。我一般把 C 的搜索范围设在 0.01 到 10 之间每间隔一个数量级试一次先做粗调锁定范围内再细调一次。这里有一个很实用的经验调参幅度远不如数据质量和标签质量对效果的影响大。我试过把C从1调到0.1F1只涨了0.003但后来花了两个下午清理了一批标签标错的样本F1直接涨了0.05。所以遇到模型效果瓶颈我第一反应永远是回去看数据而不是继续压超参数。3.4 为什么偏保守的模型选择在AI工程里更划算项目做到第二轮时我其实也试过换成中文BERT类模型用的是一种轻量预训练模型。效果确实有提升macro F1从0.82涨到了0.88但代价同样明显模型体积从几MB涨到几百MB推理速度下降近一个量级GPU资源的管理、依赖库的兼容、量化压缩这些新问题全来了。在从零团队里这些复杂度是要算进项目成本的。我当时做了一个很实际的选择第一版先上线逻辑回归BERT模型作为候选方案放在实验记录里等自动分流准确率确实压不住时再升级。这个决策让整个项目提前了两周上线而且由于逻辑回归模型体积小直接在普通CPU服务器上就能承载全部线上流量运维成本几乎为零。项目上线后我们拿线上bad case测了一下BERT模型发现它确实能多解决一部分难例但增加的业务价值有限。那次经历让我形成了一个判断AI工程里的模型选型本质上是在精度、复杂度、可维护性三者之间做权衡而不是越强越好。4. 评估与误差分析离线指标会骗人4.1 数据泄漏你可能在拿未来预测过去数据泄漏是离线评估里最隐蔽的坑尤其容易出现在我这种带时间属性的文本数据里。举一个我真实犯过的错最初做特征时我把“这条工单对应的客服处理结果”也拼进了特征列模型在验证集上表现惊人macro F1直接超过0.95。后来检查才发现处理结果是在文本分类完成后由人工填写的属于未来信息模型上线后根本拿不到这个字段。这类泄漏的特征最容易出现在“特征工程随手加”的阶段。我后来给自己定了一条规矩任何特征的可用时间必须早于或等于预测发生的时间。上线前把特征列表单独列一份标注每个特征的可用时间逐项过一遍确定没有引入未来信息再让模型进入部署环节。时间维度的另一个泄漏是数据重复。同一个用户反复投诉同一问题会在历史数据里出现多条几乎一样的文本。如果这些近似重复的样本同时出现在训练集和测试集模型其实是靠“背答案”而不是“学规律”拿到高分的。我的应对方式是先对文本做SimHash去重把相同或近乎相同的反馈保留其中一条。注意是在整个数据集去重后再按时间拆分不能拆分后分别去重。4.2 阈值与样本不均衡准确率是骗人的PR曲线才靠谱五分类中样本最多的一类超过一万条最少的一类只有两千条。直接优化整体准确率会让模型“懒得”学小类别把所有样本都往大概率类别上推。所以我们评估的第二板斧是逐类别看precision-recall曲线。我训练完每个模型都会做两件事第一输出一个按类别的classification_report记录每个类别的precision、recall、f1。重点盯样本少的类别比如“价格疑问”如果recall低于0.6说明模型根本抓不住这类问题哪怕整体准确率再高也要返工。第二画一张precision-recall曲线为每个类别选择一个合适的判定阈值。多分类里每个类别都会有自己的PR曲线我们可以在部署时给每个类别单独设阈值。比如“质量问题”的阈值设0.85“价格疑问”的阈值设0.9跟默认0.5比更像是做定制。样本不均衡的常规解法有上采样和下采样。我当时试过对少数类做SMOTE但文本稀疏向量的SMOTE效果一般还会增加代码复杂度。后来反而靠“类别权重”解决了问题逻辑回归里把class_weightbalanced打开让模型对少数类的误分惩罚更大效果比SMOTE干净得多。4.3 错误案例分析把模型失败当成观测业务的新窗口训练完模型后我最喜欢的环节是挨个看错误案例。不是用表格随便过一遍而是把预测错的样本单独导出按错误类型归类逐个阅读原文。有一次看错误案例时发现一个大问题很多用户反馈里写着“快递不派送客服说等三天但今天已经第五天了”而这条工单的结案标签是“售后问题”理由是客服做了退款操作。我再看模型预测结果模型预测的是“物流问题”——从用户文本看模型反而是对的。只是历史标签打错了。这个案例说明两个问题一是标签噪声比想象中严重二是模型预测和人工标签不一致时不一定模型错。通过错误分析我还发现了一个业务洞察约15%的“质量问题”投诉文本里其实包含强烈的情绪词比如“垃圾”“再也不买”“欺骗”。这些样本往往是需要优先处理的投诉。后来我把这个发现反馈给客服团队他们专门调了一条规则检测到这类情绪词直接进高优队列。AI模型在这里扮演的不是替代者而是帮业务发现新规则的探测器。4.4 实验记录没有记录的调参等于随机游走从零项目最容易陷入的困境是“调了半天不知道自己试过什么”。我前几次调参就吃过这个亏一周后看到一份旧实验结果想不起来那份实验用的到底是哪个参数组合。后来我给自己定了一个轻量实验记录方案不需要MLflow这类重系统只需要一个CSV加一个命名规范。具体做法每次实验在experiments/下建一个独立文件夹名字格式是exp_日期_版本_关键参数_指标比如exp_20250112_lr_c1.0_f0.82。文件夹内放三样东西训练脚本的拷贝、参数配置JSON、实验结果JSON。实验跑完顺手更新一个总的实验汇总表实验模型特征数据版本macro F1备注baseline_lr逻辑回归TF-IDFv10.82初始基线bert_base中文BERTtokenv10.88效果最好但体积大lr_balanced逻辑回归TF-IDF手工特征v20.85修正标签后这套方法坚持下来第二个项目做的时候我翻历史记录就能直接找到“上次什么方案有效、为什么有效”不用重新试错。实验记录做得好本身就是一种复利。4.5 评估后面的成本账模型收益是否真的高于规则AI项目上线之前值得冷静做一次成本收益核算。我们拿模型和旧有的关键词规则方案做了对比实验在相同的1000条新反馈样本上规则方案的准确率是78%模型是91%。如果只看准确率模型赢得很明显。但考虑到规则方案不需要任何额外算力、维护成本极低、且行为完全可解释这个差距还不足以证明必须上模型。我当时做了个更细的测算假设模型自动分流率是60%准确率91%意味着每天1000条反馈里有600条自动处理其中约54条会分错。分错的结果是客户收到错误回复、引发二次投诉这条处理成本大概是一条正常工单的三倍。算完这笔账结论很清楚模型要上线阈值必须提高宁可自动率降到45%也要把precision压到95%以上。后来我们实际上线配置double确认了这一点置信度阈值设在0.92附近自动分流率约50%但线上准确率稳定在96%左右。这比一个号称99%准确率但上线后一堆bad case的方案健康得多。5. 部署上线与持续维护模型文件变成服务的那一步5.1 模型产物不是单个文件而是一整套交付物很多从零项目挂在部署这一步是因为只把模型文件比如 pickle/joblib交出去了没有把“预处理逻辑”一起交出去。真实场景中线上请求进来要先做跟训练时完全一样的文本清洗、向量化、特征拼接然后才能进模型。这些步骤如果散落在训练代码里部署时极容易漏。我的做法是把预处理模型推理封装成一个独立类训练好模型后直接连同配置文件一起导出。示例结构大概是这样models_store/20250112_lr_classifier/ ├── model.joblib # 训练好的分类器 ├── vectorizer.joblib # 训练好的 TF-IDF 向量器 ├── preprocess.py # 文本清洗函数和训练时完全一致 ├── config.yaml # 类别列表、阈值、模型版本号 └── metadata.json # 训练时间、数据版本、离线指标其中metadata.json特别重要它记录了这次模型训练用的数据版本和离线指标方便上线后回溯。我一般会用一个固定脚本来自动生成这套目录避免手动拷贝导致配置错位。5.2 FastAPI 服务的最小实现与接口设计模型服务我用 FastAPI 写理由很朴素它自带请求参数校验和交互式文档调试时省很多事。服务代码不需要复杂核心就一个预测接口外加一个健康检查接口。from fastapi import FastAPI from pydantic import BaseModel, Field app FastAPI(titlefeedback-classifier) class PredictRequest(BaseModel): text: str Field(..., min_length1, max_length2000) class PredictResponse(BaseModel): label: str probability: float threshold: float auto_handle: bool app.get(/healthz) def healthz(): return {status: ok} app.post(/v1/predict, response_modelPredictResponse) def predict(req: PredictRequest): prob classifier.predict_proba(req.text) # 返回各类别概率 top_label, top_prob pick_top(prob) threshold thresholds[top_label] auto_handle top_prob threshold and top_label ! other return PredictResponse( labeltop_label, probabilitytop_prob, thresholdthreshold, auto_handleauto_handle, )接口设计上有一个关键点响应里一定要返回auto_handle这个布尔值同时返回probability和threshold。为什么因为这给了下游系统一个明确的决策信号也让后续排查问题时有据可查。如果哪天线上分错了一个工单可以拿当时的probability和threshold来判断是模型问题还是阈值设置问题而不是靠猜。服务跑起来之后我建议做一次简单的压测确认单机QPS能满足线上峰值流量。逻辑回归模型在这个体量下通常能轻松扛住每秒几十甚至上百次请求完全不需要上GPU。5.3 上线后的监控特征分布、预测分布、数据漂移模型上线最容易被忽视的就是监控。没有监控的模型服务就像没有仪表盘的飞机飞起来全靠感觉。我不追求一步到位建设重量级监控平台而是先用最朴素的手段盯住三个指标。第一个是请求量和自动分流率。每天自动处理了多少条请求其中多少条被判定为高置信度自动分流。如果自动分流率突然从50%掉到20%大概率是线上文本分布变了比如来了一波新活动所有反馈都在聊新活动的优惠券模型没见过的表达变多了。第二个是预测类别分布。正常情况下模型预测的类别比例应该相对稳定。如果某一天“售后问题”的比例突然翻倍可能线下发生了什么事件比如物流大面积延误。这类变化有时候是业务机会有时候是风险信号值得及时响应。第三个是文本特征分布漂移。实现上很简单把最近N天的文本平均长度、关键词命中率记录下来跟训练集做对比。哪怕不做复杂的PSI计算只看几个关键特征的趋势也能提前发现分布漂移的苗头。我在项目里写了一个每小时跑一次的定时脚本把监控指标输出到一张简单的HTML报表里团队每天早上扫一眼就够了。这里我要提醒一句监控报表是给谁看的要提前想清楚。给业务方看的报表要自动化分流率、拦截率这些业务语言给工程师看的才放特征漂移、置信度分布这些技术指标。一份报表想同时满足两种读者最后往往谁都看不懂。5.4 回滚与版本灰度如何最小代价切换模型模型版本升级是必然的但升级的方式决定了风险大小。我见过有人直接在生产服务里替换模型文件结果新模型线上效果差又找不到旧模型文件只能急急忙忙重新训练。这个坑完全可以通过“模型版本化灰度发布”避免。我的做法是把模型和服务解耦服务启动时从models_store/读指定的版本目录每次发布新模型就是创建一个新版本目录然后在配置中心或者环境变量里指过去。回滚时只需要把版本号指回旧目录重启服务即可秒级完成。流量灰度方面第一版我做得很轻量在服务里加了一个版本路由开关比如先从1%的流量把预测日志打到新模型上人工检查一两天再逐步放开到5%、50%、100%。配合上面提到的监控指标这个灰度过程可以做到比较心里有底。这个方案的缺点是服务重启会有短暂中断。但对我们这个体量的内部系统完全够用。如果你未来需要更平滑的蓝绿发布可以考虑在服务出口加一层负载均衡新旧两个服务实例同时在线按权重切流。但那是后话MVP阶段不必为了“专业感”勇建复杂架构。5.5 从零上线的可操作清单经历了整个项目我总结了一份可以照着打钩的从零上线清单放在这里供参考[ ] 预测接口支持传入原始文本内部完成清洗、向量化、推理[ ] 每个请求返回类别、概率、阈值、是否自动处理[ ] 模型服务有健康检查接口能被容器编排或手动探活[ ] 模型版本目录包含预处理代码和配置不只有模型文件[ ] 有最小监控请求量、自动分流率、预测分布、特征漂移[ ] 有版本回滚机制切换模型不超过五分钟[ ] 离线测试集上的人工抽检记录留档方便线上对账[ ] 压测结果满足峰值流量需求CPU/内存余量充足这张清单看着普通但每一项都对应一个我或者同行踩过的坑。从零项目想要“不翻车”不是靠某一次精心操作而是靠这些不起眼的环节各自守住底线。6. 复盘从零做AI工程的时间花在了哪里6.1 时间占比实录训练模型只占我20%的时间项目结束后我做了一次时间复盘结果很有冲击力。整个项目从开始到上线我花时间最多的不是模型训练而是数据清洗和错误分析两者加起来占了接近一半的时间。模型选择和训练加在一起大约只占两成剩下的是环境搭建、服务部署、监控和沟通对齐。这个时间分布一点都不反常。AI工程里数据质量决定了模型天花板评估和误差分析决定了能不能稳地接近这个天花板部署和监控决定了上限能不能落进业务里。如果一个人说他把80%时间花在训练模型和调参上那大概率是前面几步没有做扎实后面上线会连本带利还回去。6.2 如果我再来一次会直接跳过哪些坑回看整个过程有几个坑如果能重来一定会避免。第一个是一开始就做时间序列拆分而不是等第一轮实验跑完才从随机拆分切到时间拆分。我当时在随机拆分上浪费了两天得出的离线指标虚高后面还得重来。第二个是更早引入业务方参与错误分析。我第一次看bad case时是自己一个人看看到很多标签错误但没有底气去改。后来拉上业务方一起看了两轮他们对标签规则做了三次修正数据质量立刻上了一个台阶。这个合作应该从第一天就开始。第三个是依赖锁定和数据版本控制从第一行代码就做。我是过了两周才补上DVC和uv.lock中间有一次数据文件被同事覆盖花了半天才找回。如果在项目一开始就建立好这套机制那一天就不会浪费。6.3 关于“从零构建模型”热潮的门外思考最近技术社区里“从零构建推理模型”、“从零编写大语言模型”的热度一直很高我自己也收藏了不少这类文章。它们对理解原理很有价值但我逐渐意识到这类项目和真实工程里的from scratch是两个不同维度的概念。模型内部的每一个tensor都可以从零写起但工程上的从零是看你敢不敢在一片空白里定义正确的目标、搭起数据基建、顶住各种环境问题最后把一个模型服务稳定地交给业务使用。后者不需要你手写反向传播但需要你理解业务、敬畏数据、重视可复现性并在每个环节做克制而清醒的选择。回到我自己这次项目里最有价值的产出可能不是那个分类准确率还不错的模型而是一套“以后再来项目可以直接复制”的流程感问题定义先把业务指标对齐数据基建保证可复现模型选型保持克制评估深挖bad case部署重视监控和回滚。下一次再接到“从零开始”的需求至少不会再心虚地泛泛而谈“我们可以上AI”了。
阅读完成 · 觉得有帮助?
咨询建站