资讯动态

从零搭建AI工程链路:客服工单分类的完整实践

发布时间:2026/10/3 16:06:26 来源:尧图企业网站定制
不借助任何现成的AI开发平台从一台裸机和一批原始文本开始亲手把一条完整的AI工程链路走通——这个念头催生了我去年最重要的个人项目 ai-engineering-from-scratch。项目覆盖数据清洗、模型微调、在线推理、容器化部署和监控告警五个环节最终落地的案例是一个客服工单智能分类系统。做完之后我最大的体会是AI工程真正难的不是算法而是把模型变成一项稳定、可观测、有人愿意用的服务。无论你是算法工程师、后端开发还是相关专业的学生只要完整跑一遍这个项目你对AI落地的理解都会比看一百篇教程更扎实。这个项目对硬件要求不高一张普通的消费级显卡甚至纯CPU都能完成大部分流程核心在于把每个步骤都做扎实。1. 为什么我选择“从零开始”搞AI工程1.1 所谓“AI工程”到底在解决什么问题现在聊AI的内容大多集中在模型结构、调参技巧、榜单分数上但真正到了业务线上模型只是整条链路里的一环。我习惯用一个类比来理解AI工程模型像一个厨艺高超的厨师工程则是从买菜、洗菜、备菜、掌勺到上菜的整套流程。饭店能不能稳定出餐不只看厨师一个人更要看整个流程有没有规范、有没有备份方案、有没有人盯着火候。AI工程解决的核心问题之一是“稳定复现”。在笔记本上跑出一个好结果和在一个干净环境里随时能重跑出同一个结果是两个不同层次的能力。很多算法同学在竞赛里成绩很好一进企业项目就感觉失灵原因就在这里竞赛给的是固定数据集和固定评测指标而真实业务的数据是流动的bad case是无穷的线上环境还会跟你闹脾气。工程化就是把“稳定复现”变成默认行为而不是靠运气。另一个核心问题是“可观测性”。模型上线之后你没法实时拿到人工标注去算准确率你必须借助间接信号来判断模型是否还正常。请求量、延迟、类别分布、置信度分布这些指标就像汽车的仪表盘。没有仪表盘你只知道车在跑却不知道引擎是不是已经冒烟了。这个项目让我最在意的就是把这块仪表盘从头到尾自己装了一遍。1.2 我把工程链路拆成了五个环节为了避免“什么都想学什么都没学透”我给自己划定了一个清晰边界只做五个环节每个环节必须有可检查的交付物。第一个环节是数据。交付物不是一堆CSV而是可复现的清洗脚本、带版本号的数据集、以及对每个字段含义的说明。数据是AI项目的命根子这个环节做得越扎实后面越省力。第二个环节是建模。我要求自己至少跑通一个基线模型再升级到预训练模型微调。交付物是可复现的训练脚本以及一份实验记录。实验记录要能回答一个问题这个模型是用哪份数据、哪版代码、哪组参数训练出来的。第三个环节是评测。交付物不只是一张准确率表格而是包含分类别精确率、召回率、F1的详细报告外加一份人工抽检的错误案例分析。评测不是给模型打分而是给模型“找毛病”。第四个环节是服务化。交付物是一个能通过HTTP调用的推理接口包含输入输出的字段说明、错误处理逻辑和基本的性能基准。模型躺在Jupyter Notebook里是没有价值的别人能调用才叫服务。第五个环节是监控。交付物是指标看板、日志和告警规则。我要能回答模型现在响应快不快、预测的类别分布正不正常、有没有出现置信度大规模偏低的情况。这五个环节首尾相连构成一个闭环。任何一个环节断掉项目都不能算真正完成。我这个项目从头到尾都是围绕这五件事展开的。1.3 为什么不用AutoML或平台化工具可能有人会问现在AutoML工具这么成熟拖拽几下就能出模型干嘛还要从零开始折腾这个问题的答案我是在项目做了一半时才真正想明白的。AutoML能帮你快速拿到结果但它会把“为什么”藏起来。你不知道数据分布如何影响阈值不知道线上请求和训练样本之间的分布差异不知道某个特征为什么重要。一旦线上出问题你连排查的切入点都找不到。从零开始走一遍本质上是给自己建立一套“故障直觉”看到某个现象能大概猜到是哪一环出了问题。另一个原因是选型能力。只有亲手对比过TF-IDF加逻辑回归和预训练模型微调的效果差异你才知道什么时候该用便宜方案什么时候必须上重武器。没有这种体验你很可能会在简单任务上过度设计在复杂任务上又过度自信。而且从零开始不等于拒绝工具。我在项目里也用了很多现成库比如PyTorch、FastAPI、MLflow。关键在于我要知道每一层工具在解决什么问题出了问题大概要去哪里查。这种掌控感是用平台工具替代不来的。等你有过一次完整经验再回头看AutoML反而能把它用得更好因为你终于知道它在替你做什么了。2. 搭建工具链与基础工程素养2.1 环境管理版本一致是省时间的第一步我在项目最开始差点被环境问题劝退。当时随手装了一个最新版Python结果某个数据处理库的二进制依赖没跟上一跑就报错。后来老老实实把环境固定下来后面几个月都清净了。我的建议是使用conda创建独立环境Python版本锁定为3.10.12。不要追最新版本AI依赖的版本兼容性非常敏感新版本可能引入breaking change排查成本极高。PyTorch方面我选用的是2.1.0版本搭配CUDA 11.8这是我在这个阶段试过的最稳定组合。如果你在Windows上做开发强烈建议用WSL2很多GPU相关的坑在WSL2里会少很多。环境写进requirements.txt还不够要锁定关键包的精确版本。比如conda create -n ai-eng python3.10.12 -y conda activate ai-eng pip install torch2.1.0cu118 --index-url https://download.pytorch.org/whl/cu118 pip install transformers4.36.2 datasets2.16.1 fastapi0.109.2注意torch2.1.0cu118这种写法它把PyTorch和CUDA版本绑在一起避免装错轮子。如果你用的是纯CPU环境那就装torch2.1.0不加cu118后缀。我踩过一个具体坑某次数据处理代码在numpy从1.24升到1.26后某个函数的默认行为发生了变化导致文本tokenize结果和之前不一样模型效果莫名下降。排查了两天才发现是环境变了。所以我的原则是新项目单独开环境老环境尽量别动每次跑实验前确认环境没被静默升级。2.2 数据版本化让每一版模型都有据可查很多人把数据当成一次性的输入文件跑完就扔。但模型效果很大程度上由输入分布决定。如果数据变了模型效果也变了你却不清楚是代码改的、参数改的还是数据改的那整个迭代就变成了盲人摸象。我的做法是给数据集一个唯一版本号并记录它的来源、清洗脚本版本、生成时间。最简单的方式是在数据目录下放一个dataset_version.json文件{ dataset_id: ticket_v3, source: raw_tickets_20240115.csv, clean_script: clean_tickets.pygit_abc1234, created_at: 2024-02-01T10:00:00Z, num_rows: 20345, sha256: 7b8f...a2c1 }其中sha256是数据文件的哈希值用来校验完整性。只要数据有任何改动这个值就会变化这样就能立刻发现“某个版本的模型到底用的哪份数据”。进阶一点可以用DVC做数据版本管理把数据文件的版本和Git commit关联起来但核心思想是一样的数据也要像代码一样被追踪。数据切分同样需要谨慎。对于带有时间属性的业务数据比如客服工单不要用随机切分而应该按时间切分。我用前80%时间段的工单做训练集后20%做验证集。随机切分会造成“未来信息泄漏”模型在验证集上虚高上线后被打回原形。清洗脚本和训练脚本也要分开维护。清洗脚本单独成一个文件每次修改都要同步更新数据集版本号。项目进行到后期我回头查看某个模型效果异常时靠的就是这些版本记录直接定位到“数据版本v2到v3之间清洗规则变了”而不是在代码里翻半天。2.3 实验追踪把每次尝试都记录下来没有实验记录的机器学习严格来说不叫实验叫碰运气。这是我做这个项目时给自己立的规矩。我一开始用的是轻量做法每个实验跑完自动往一个CSV文件里追加一行包含exp_name、git_commit、data_version、params_json、metrics_json、timestamp。效果很好后来数据量多了就切换到MLflow做实验追踪把参数、指标、模型文件一起记录下来。一个典型场景当你同时调了learning rate、batch size和数据清洗方式结果变好了。如果没有记录你根本不知道是哪个因素起了作用。我在项目里就吃过这个亏有段时间觉得“改了什么都在变好”后来发现其实是数据量增加了跟参数调整没关系。有了实验记录你能清楚看到每一次改动对应的指标变化迭代才有方向。这里有个执行细节实验记录里有一个字段容易被忽略就是随机种子。不固定种子同样的代码和数据跑两次结果会不一样。我每个实验都固定了seed42确保实验结果可复现。这个习惯后来帮我省了很多事。3. 核心环节拆解模型微调与评测体系3.1 数据标注的坑先定标准再谈模型客服工单分类这个任务看起来简单真正动手才发现数据里全是坑。第一个坑是标注标准含糊。一开始“支付问题”和“退款问题”两个标签在标注员之间反复摇摆同样一个工单两个人可能给出不同答案。我的解决办法是先让两位同学各标200条计算交叉一致率把冲突的case全部挑出来开会讨论最后写成一份标注手册。没有标注手册后续所有模型评测都不可信因为你连“正确答案”本身都是漂移的。第二个坑是类别不平衡。客服工单数据里“咨询”类占了约60%而“投诉”类只有5%。如果直接训练模型会倾向于把所有样本都预测为“咨询”因为这样就能拿到很高的准确率。处理办法是在损失函数里给少数类更大权重或者对少数类做加权采样。我还做了一个调整先把大类分出来再做细分类避免一开始就让模型面对极度失衡的类别。第三个坑是“其他”类的垃圾筐效应。如果“其他”类占比过高模型会把所有不确定的样本都扔进去形成一个什么都能装的垃圾桶。我后来把“其他”类比例控制在10%以内并且要求模型在置信度低于0.6时输出“需人工复核”而不是硬塞给某一个类别。这个阈值不是拍脑袋定的是看了大概500条错误case的置信度分布后选的。3.2 参数与模型选型从基线到预训练模型的路线图我的选型逻辑分两步走第一步是先用TF-IDF加逻辑回归跑一个基线模型。这个模型不一定最强但它快、可解释、能快速验证数据和标签是否靠谱。当时跑出来整体准确率大约0.82看起来还行但一看分类别指标“投诉”类召回率只有0.45大量投诉被吞进了“咨询”类。这时候我就知道问题不在模型而在数据分布和评测方式。第二步才是上预训练模型。我选的是基于BERT架构的中文预训练模型chinese-roberta-wwm-ext主要因为它在中文文本分类任务上表现稳定而且比更大规模的模型省显存。如果你算力更有限也可以选更轻量的中文模型核心逻辑是一样的。微调参数我一开始就定了一组安全值learning rate 2e-5batch size 16epoch 2到3max length 128warmup比例0.1。这些参数不是随便抄来的背后有原因预训练模型已经学到了通用语义微调只是用你的数据调整输出层和部分表示所以学习率必须远小于从头训练。2e-5这个数量级是实践沉淀出来的共识一上来就调到1e-4很容易灾难性遗忘。batch size也不宜太大文本分类任务通常16就够太大会让梯度更新过于平滑收敛反而变慢。epoch控制在2到3轮配合早停机制超过3轮基本就开始过拟合。max length设128对客服工单这种短文本够用再长只是浪费显存和推理时间。用这套配置微调后整体准确率从0.82提升到0.89“投诉”类召回率从0.45提升到0.78。关键不是分数涨了多少而是我知道每个涨跌来自哪里。这就体现了基线模型的价值没有基线你根本不知道预训练模型帮你解决了什么问题。3.3 评测指标准确率只是起点业务视角才是终点准确率在类别不平衡的数据上会严重骗人。如果90%的样本都是“咨询”类模型什么都不干准确率也有90%。所以我的评测报告至少包含分类别的精确率、召回率、F1以及宏平均和加权平均。更重要的是结合业务视角看指标。在客服系统里“把投诉误判为咨询”和“把咨询误判为投诉”代价完全不同。前者意味着用户问题被延误可能升级成更严重的客诉所以我必须重点看“投诉”类的召回率。指标不是越高越好而是要匹配业务风险。我还会做人工抽检。每轮实验结束后随机抽100条错误case逐条看记录错误原因。很多问题是指标看不出来的比如“复制粘贴的重复工单”“中英混杂的文本”“超长文本被截断后语义丢失”。这些case攒多了你会对模型的行为模式有非常直观的感觉比看任何报告都有效。一份典型的评测表格长这样类别样本数精确率召回率F1咨询18000.920.950.93售后4200.830.790.81投诉3000.760.780.77退款2200.880.850.86其他1600.740.610.67只看准确率0.89你会觉得模型不错。但看了这张表“其他”类的召回率只有0.61明显拉胯。下一步应该去分析“其他”类的错误case而不是继续调模型参数。4. 完整实操复盘客服工单智能分类项目4.1 问题定义与基线效果这个项目的业务场景很简单用户给客服发来一段工单文本系统需要自动判断应该流转到哪个部门。我用了约2万条脱敏后的工单数据目标是分到“咨询、售后、投诉、退款、其他”五个类别。第一步是清洗数据。看起来枯燥但很重要。我做了三件事去除无意义符号、统一简繁体、去掉长度小于10个字符的无效工单。清洗脚本单独存放并记录版本号。第二步跑TF-IDF加逻辑回归基线。当时我把整体准确率报给一个朋友看他说“0.82不错了”但我把分类别指标展开后问题一目了然“投诉”类召回率只有0.45。一半以上的投诉被模型送去了“咨询”部门这在业务上是不可接受的。第三步换成chinese-roberta-wwm-ext微调整体准确率0.89“投诉”类召回率0.78。虽然还有提升空间但已经能看出预训练模型对语义理解的优势。这套对比让我得到一个结论不要一上来就上大模型先把基线和错误分析做出来你才知道钱和时间花在哪里值得。4.2 推理服务封装与性能调优模型训完只是开始让别人能用才是工程。我用FastAPI封装了一个HTTP接口输入是文本输出是类别和置信度。核心代码如下from fastapi import FastAPI from pydantic import BaseModel import onnxruntime as ort from transformers import AutoTokenizer app FastAPI() sess ort.InferenceSession(model.onnx) tokenizer AutoTokenizer.from_pretrained(chinese-roberta-wwm-ext) class PredictRequest(BaseModel): text: str class PredictResponse(BaseModel): label: str confidence: float app.post(/predict, response_modelPredictResponse) def predict(req: PredictRequest): inputs tokenizer(req.text, max_length128, truncationTrue, return_tensorsnp) logits sess.run(None, dict(inputs))[0] probs softmax(logits[0]) label_id int(probs.argmax()) label id_to_label[label_id] confidence float(probs[label_id]) if confidence 0.6: label need_manual_review return PredictResponse(labellabel, confidenceconfidence)注意两个细节一是模型先转成了ONNX格式再放到ONNX Runtime里推理这样部署时不需要装完整的PyTorch更轻量二是置信度低于0.6时我不强行分类而是返回“需人工复核”。这个保底逻辑在真实业务里非常重要宁可让人介入也不要让一个低置信度的错误判断直接流转到业务线。性能方面我用一段脱敏测试集比较了PyTorch原版和ONNX版本。在我的CPU环境下P99延迟从120ms左右降到60ms左右吞吐也提升了一倍。中间我还试过torch.compile在这个任务上收益不大就放弃了。优化不是越高级越好够用就行。4.3 容器化部署与监控告警服务写好之后我用Docker把它打包。基础镜像用的是python:3.10-slim只装ONNX Runtime和FastAPI这些运行时依赖模型文件直接打进镜像。这样部署到哪台机器都能保证一致不会再出现“在我机器上能跑到你那儿就不行”的问题。Dockerfile大致长这样FROM python:3.10-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY model.onnx . COPY app.py . EXPOSE 8000 CMD [uvicorn, app:app, --host, 0.0.0.0, --port, 8000]容器起来后我加了一个/healthz健康检查接口返回200表示服务正常。这个接口还会顺带检查模型文件是否存在、推理session是否已经加载避免出现“进程活着但实际不能服务”的情况。监控方面我暴露了四个核心指标请求量、P99延迟、类别分布、平均置信度。用Prometheus采集Grafana展示。告警规则我设了三条P99延迟超过300毫秒持续5分钟告警请求成功率低于99%持续2分钟告警类别分布相对上一周变化超过20%告警。最后一条需要多说一句。线上没有实时人工标注你无法实时算准确率但类别分布能反映数据是否发生结构性变化。比如某天系统上线新功能用户问的问题方向变了类别分布就会偏移。这个信号通常比准确率下降更早出现是数据漂移的重要预警。5. 常见问题与排查技巧实录5.1 环境依赖与显存问题排查跑AI项目环境问题能占掉三分之一的时间。我遇到最典型的一个是CUDA out of memory。一开始我以为是模型太大后来发现是DataLoader里的num_workers进程持有模型引用导致显存一直不释放。把num_workers调成0或者减少worker数量就解决了。还有一个经验是不要动不动就升级环境。某个库升完级表面上看没变化实际上某个函数的默认行为变了模型结果就开始异常。我后来的原则是每个新项目单独建conda环境旧环境保持原样不动。排查问题时第一件事永远是检查当前环境有没有被静默改动。5.2 模型效果不符合预期的定位思路如果模型效果不行第一步永远看数据不要先怀疑模型结构。我会打印一批错误样本看看是不是存在标注错误、重复样本、上下文缺失。很多时候问题根本不在模型而在数据质量。“预测结果全是一个类”这个问题我会优先检查三件事训练集是否严重不平衡、标签映射有没有写错、损失函数是否被某个大类别主导。这些问题在之前的工单项目中全都遇到过每种的解决办法都不太一样。“训练Loss正常但验证效果差”则大概率是过拟合或数据泄漏。我会检查验证集是不是混入了训练集样本或者随机切分导致时间上相邻的样本出现在两边。改用按时间切分后问题一般会缓解。5.3 上线运维阶段的经典事故与速查表服务上线后问题的性质会变化。这里整理一份速查表都是我在项目里或者帮别人排查时遇到过的真实场景症状可能原因处理方式健康检查正常但推理超时批量任务与在线推理抢占CPU将批量任务挪到独立实例或限制并发数线上置信度普遍降低输入文本分布发生变化回看监控面板抽样检查新case准备增量数据类别分布突然倾斜上游业务规则变化联系业务方确认更新分类映射或准备重训Docker启动时模型加载失败模型文件路径错误或镜像过大导致内存不足使用相对路径同时查看启动日志中具体的异常信息推理结果偶尔返回空值输入文本为空或tokenize后长度为零在接口层增加输入校验返回错误码而不是硬跑还有一个非常重要的习惯shadow模式。每次新模型上线前不要急着切换流量而是让新模型和线上模型并行跑一段时间把两者的预测结果和人工抽检情况记录下来对比。我靠这个办法避免过两次可能让线上效果大幅回退的发布这个习惯一直保留到现在。这个项目做到最后我最大的体会是AI工程的核心能力不是会训练模型而是面对一版不达预期的结果能快速定位是哪一环出了问题——是数据、参数、代码还是部署。从零开始走一遍最大的收获不是你得到了一个多好的模型而是你对整条链路有了掌控感。最后再分享一个小技巧每次上线新模型之前先让它和线上模型并行跑一段时间把两类预测结果和人工抽检记录下来做对比再决定要不要正式切换。这个方法救过我两次真的值得一试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑