资讯动态

NLP+微服务实现工单智能分类:从0到1的工程实践

发布时间:2026/9/24 22:47:35 来源:尧图企业网站定制
投诉工单智能分类这个需求最早不是技术团队提出的而是一线客服部门某次周会上的一句抱怨每天几千条工单靠人一条条判断归属加班都分不完。这句话成了整个项目的起点。基于NLP和微服务架构我们最终上线了一套能自动完成投诉工单业务分类、责任部门识别和紧急程度判定的系统把每张工单的平均处理时长从人工分拣的4分多钟降到了秒级分类准确率稳定在90%以上。这篇文章会把整个项目从0到1的实现过程完整拆开包括技术选型的考量、类别体系怎么定、模型怎么训练、微服务怎么拆分、上线后踩了哪些坑。适合正在做或准备做文本分类类项目的人参考也适合想了解NLP工程化落地的人阅读。我的原则是只讲实操不讲空话所有结论背后都有真实数据和踩坑经历支撑。1. 工单积压这个老问题比想象中更具体1.1 人工分类的三大问题慢、不一致、没法沉淀先说说项目立项前我们观察到的真实情况。公司每天产生的投诉工单量在三千到五千条之间波动旺季能冲到八千条。这些工单来自电话客服、在线客服、邮件、小程序反馈等多个渠道格式五花八门内容长短不一。过去全靠一个十几人的客服小组人工阅读后打标签决定工单该转给哪个部门处理。第一个痛点是慢。一条工单从收到到完成分类平均耗时四分钟这还只是读完文本、判断类别、填写流转信息的时间。遇到内容含糊、夹杂情绪化表达的工单客服人员还要停下来猜用户到底在抱怨什么有时候一条工单能卡十几分钟。工单一旦积压流转效率直线下降用户等待时间变长二次投诉率跟着上涨。第二个痛点是标注标准不一致。同样一句东西坏了没人管有的客服归为产品质量问题有的归为售后服务问题还有的归为物流配送问题。三个人能给出三种答案责任部门跟着乱转用户被来回踢皮球投诉升级成舆情事件的例子不是没有。我后来抽样核对了历史工单数据发现同一类内容被分到不同类别的比例大概在7%到12%之间这个数字在管理上看起来不高但对用户体验的伤害是实打实的。第三个痛点是经验没法沉淀。老客服靠记忆和语感判断分类新人上手慢培训周期长。一旦老员工离职带走的是一套说不清道不明的分类经验。我们当时就遇到过一个干了五年的老客服离职后新接手的团队连续两周分类准确率明显下滑的情况。所以业务方提出想要一套自动分类系统的时候需求其实非常明确他们不奢求完全替代人工只想把分类这件事标准化、自动化让人去做更有价值的复核和沟通工作。1.2 项目的目标边界先解决分得准再谈分得快立项会上业务方提了一堆想法自动回复、智能派单、情绪识别、风险预警、舆情监控……每个听起来都很好但我们技术侧坚持只做了一个核心功能——文本分类外加一个非常轻量级的紧急程度判定。原因很简单一口吃不成胖子分类是所有后续智能化能力的地基。分类不准自动回复就是胡言乱语智能派单就是添乱基于类别的数据分析全是错的。这里我想给所有做类似项目的同学一个建议在项目启动阶段一定要和业务方明确第一版本做什么、不做什么。我们当时拉了个简单的需求优先级表格把自动分类列为P0紧急程度判定列为P1其余全部放到P2以后。不是技术做不到而是团队精力有限任何一项功能上线后都需要人工复核和持续优化铺得太开必然顾此失彼。最终的验收指标也定得很朴素在十万条历史工单上分类准确率不低于85%单条工单分类响应时间不超过3秒系统支持灰度上线和人工复核回退。这三个指标后来成了我们设计架构和选型模型的硬约束每个技术决策都要拿它们来卡一下。2. 技术选型复盘为什么是NLP加微服务架构2.1 文本分类模型从传统机器学习到预训练模型的推移模型选型是最早要拍板的事情。说实话在2019年之前遇到这种需求我大概率会用TF-IDF加朴素贝叶斯或者支持向量机做个特征工程跑个简单的分类模型就能上线效果也不会太差。但我们这个项目启动时中文预训练模型已经比较成熟了BERT系列、RoBERTa、ERNIE这些都有现成的开源版本生态也足够健全这时候再拿传统机器学习方案就有点说不过去了。我实际做的对比实验也证明了这一点。拿同一份一万条标注好的工单数据跑了一组对照TF-IDF加SVM的准确率大概在78%左右瓶颈非常明显——工单文本里大量的口语化表达、错别字、网络用语传统特征工程根本扛不住。而基于预训练模型微调的方案同样的数据和训练轮次准确率能到91%以上。差距来自预训练模型对中文语义的理解能力它不是在匹配关键词而是在理解句子表达的真实意图。不过我要说句公道话传统机器学习方案并不是一无是处。它有不可替代的优势推理速度快、部署简单、不依赖GPU单条文本判定只要几毫秒。所以我们在架构上留了一手模型服务里做了一个动态路由的设计简单规则能判定的直接走规则引擎规则搞不定的才进模型。后来这个设计帮我们省了不少计算资源后面会详细讲。2.2 微服务架构不是为了时髦而拆再说微服务架构。很多人一听到微服务就头疼觉得小项目没必要拆这个观念我部分同意。我们这个项目最初确实是单体应用也能跑但拆成微服务不是赶时髦而是有几个很具体的业务原因。第一模型推理和业务逻辑的资源需求完全不同。BERT模型推理需要GPU业务接口只需要CPU把两者放在一个进程里要么GPU闲置、要么CPU浪费怎么都不合适。拆开之后模型服务单独部署在带GPU的节点上业务服务部署在普通CPU节点上各自独立扩缩容资源利用率高得多。第二模型要频繁迭代。业务方每个月都会提出新的类别需求或者补充新的样本模型几乎每周都要重新训练和发布。如果模型和业务代码耦合在一起每次模型更新都要重新发布整个应用风险大、效率低。拆成独立服务后模型服务可以单独替换业务侧完全无感知。第三团队协作需要清晰的工作边界。算法工程师负责模型训练和服务化后端工程师负责工单的接收、存储和流转前端负责管理界面的展示。服务边界清晰两边就能并行推进不用互相等。这个效率提升在项目后期非常明显。2.3 最终技术栈清单技术栈清单如下都是我们实际跑生产环境的版本号也是当时验证过的稳定组合新手可以直接抄模块技术选型选型理由模型框架PyTorch 1.10 Transformers 4.15生态成熟预训练模型加载方便社区资料多预训练模型BERT-base-chinese中文效果稳定显存占用可控推理速度快于大参数量模型模型服务FastAPI Uvicorn异步支持好性能足够写起来比Flask顺手服务注册发现Nacos 2.0团队已有运维经验同时支持配置管理API网关Kong支持限流、鉴权、灰度路由插件生态丰富消息队列RocketMQ吞吐量够用事务消息方便做数据闭环存储MySQL ElasticsearchMySQL存工单主数据ES做全文检索和查询分析容器化Docker Docker Compose环境一致性测试和生产部署用同一套镜像这套组合没有什么稀奇的全是社区里验证过的成熟方案。我的经验是做这类项目不要追求新奇的技术栈稳定性远比炫技重要。选团队熟悉的技术能少踩一半的坑。3. 分类模型搭建数据、类别与训练细节3.1 类别体系设计分类模型的地基模型训练的成败一半在数据另一半在类别体系设计。我们花了整整一周时间做这件事比训练模型本身还久。前期主导分类的是业务方他们用的是业务语言比如投诉咨询建议表扬这种按诉求类型划分的维度但我们看完历史数据后建议按问题归属来分因为系统最终的价值是让工单准确流转到对应责任部门。最终和业务方一起敲定的是三层结构一级类别7个二级类别23个三级类别由责任部门自行维护。一级类别包括产品咨询、购买与订单、物流配送、产品质量、售后服务、费用结算、其他综合问题。紧急程度分三级一般、紧急、非常紧急定义得非常明确比如涉及人身安全大规模故障媒体曝光风险属于非常紧急避免不同的人对紧急的理解不一致。这个三层结构的好处是灵活。一级类别对应用户的核心诉求稳定不变二级类别随业务调整可以增删三级类别完全交给责任部门自己管理跟系统解耦。模型只负责在一二级类别上做分类三级类别通过规则映射这样模型迭代频率和业务变化频率就不互相拖累了。3.2 数据清洗与标注策略类别定了之后数据侧的工作量立刻上来了。我们从工单系统里导出了最近一年的40万条文本经历了三个阶段才整理出可用的训练集。第一阶段是粗筛。先用简单的关键词规则把明显不属于投诉的内容排掉比如纯广告、纯系统通知、重复提交的测试工单。粗筛后剩了28万条左右。第二阶段是去重和相似度过滤。同一个用户反复投诉相同问题产生的近似文本如果不处理会让模型对高频投诉类型产生严重的过拟合。我们用了SimHash做去重再把相似度超过阈值的样本合并最后剩了约15万条。第三阶段是人工标注。这是我们整个项目里最花时间也最值得花时间的地方。标注规范是必须提前写死的。我们拉上业务方的老员工花了三天时间梳理了一份标注指南里面写清楚了对边界情况的处理办法。比如一条工单同时提到多个问题时怎么处理——我们的规则是找主线问题主诉和次诉并存时按主诉分类无法判断时标记为需人工复核绝不硬塞给模型。标注团队由两名老客服加两名算法工程师组成先标两轮不一致的地方拿出来讨论修正保证Kappa系数标注一致性达到0.85以上才开始正式标注。正式标注了四万条数据按7:2:1切成训练集、验证集和测试集。这里提醒一句数据准备工作看起来不性感但它决定模型效果的上限。模型参数调得再好数据脏的话准确率也就那样。3.3 模型微调的关键参数与训练过程模型方面我们用的是BERT-base-chinese在这个项目里已经绰绰有余。微调的参数是基于多轮实验确定的这里直接给出一组可复现的配置from transformers import BertForSequenceClassification, BertTokenizer, Trainer, TrainingArguments model BertForSequenceClassification.from_pretrained( bert-base-chinese, num_labels30, # 一级7个 二级23个模型直接预测二级类别 hidden_dropout_prob0.3 ) tokenizer BertTokenizer.from_pretrained(bert-base-chinese) training_args TrainingArguments( output_dir./results, learning_rate2e-5, per_device_train_batch_size16, per_device_eval_batch_size32, num_train_epochs5, weight_decay0.01, warmup_ratio0.1, logging_dir./logs, logging_steps100, eval_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, fp16True, # 混精度训练显存不够时非常有用 )训练细节里有两个我觉得特别值得说的点。第一是学习率BERT微调的学习率不要太激进2e-5是绝大多数场景下的安全值我们试过5e-5训练收敛是更快但验证集的F1掉了将近两个百分点典型的过拟合信号。第二是类别不平衡问题我们的数据里产品咨询占比30%而费用结算只有4%如果不做任何处理模型会倾向于把所有模棱两可的样本都预测成高频类别。我们用了两个办法解决一个是在损失函数里按类别频率的倒数为每个样本加权另一个是给低频类别的样本做简单的EDA增强比如同义词替换、随机删词、词序交换。两个办法叠在一起低频类别的F1平均提升了6个百分点。训练在单张NVIDIA V100上跑5个epoch大概花了三个小时。每次训练结束我们都会把测试集上每个类别的精确率、召回率、F1单独拉出来看而不是只看一个汇总数字。3.4 评估指标的坑别让准确率骗了你这里必须单独说一下评估指标。我们第一版模型在测试集上的整体准确率是91.5%当时觉得已经不错了结果上线前做细粒度分析发现了问题。问题出在精准率和召回率的失衡上。整体准确率高是因为高频类别产品咨询和物流配送的分类效果太好把平均值拉上去了。但低频类别里费用结算的召回率只有62%这意味着将近四成真正属于费用类投诉的工单被分到了别的类别。这种错误比类别内部混淆更隐蔽因为一眼看过去总正确率还行实际上个别类别的用户体验已经崩了。所以我们最终的评估标准是macro-F1而不是accuracy同时要求每个二级类别的F1不低于80%。达不到就继续调数据、调参数直到所有类别都过线才允许上线。后来我们统计过这个每个类别F1不低于80%的红线让系统上线后的实际效果比单纯看准确率预期好了很多。关于二三级类别映射我们也做了一个补救设计如果模型对一条工单预测二级类别的置信度低于0.6就标记为低置信度并转人工复核不直接流转。这0.6的阈值是在验证集上算出来的能覆盖约12%的工单把这部分的准确率从88%拉到了95%以上。宁可让一部分工单回到人工手里也不能让错误分类的工单在系统里流转。4. 微服务落地从模型文件到生产接口4.1 模型服务化用FastAPI封装推理接口模型训好之后接下来的问题是怎么把它变成生产环境可调用的服务。我们没有引入专门的模型服务平台而是用FastAPI写了一个轻量级的推理服务充分利用它是为了快速上线、方便调试。服务内部预先加载好模型和tokenizer放在全局变量里避免每次请求都重新加载。模型推理被封装成一个异步接口支持单条预测和批量预测两种模式。核心代码大概是这样的逻辑from fastapi import FastAPI, HTTPException from pydantic import BaseModel import torch from transformers import BertTokenizer, BertForSequenceClassification app FastAPI() tokenizer BertTokenizer.from_pretrained(/models/bert-chinese-ticket) model BertForSequenceClassification.from_pretrained(/models/bert-chinese-ticket) model.eval() model.to(cuda) class TicketRequest(BaseModel): text: str request_id: str class TicketResponse(BaseModel): request_id: str level1_category: str level2_category: str confidence: float app.post(/api/v1/classify, response_modelTicketResponse) async def classify(req: TicketRequest): if not req.text or len(req.text.strip()) 2: raise HTTPException(status_code400, detailtext too short) # 预处理与截断 text req.text.strip().replace(\n, )[:254] inputs tokenizer(text, return_tensorspt, truncationTrue, max_length128).to(cuda) with torch.no_grad(): logits model(**inputs).logits prob torch.softmax(logits, dim-1) conf, pred torch.max(prob, dim-1) level1, level2 id_to_category[int(pred)] return TicketResponse( request_idreq.request_id, level1_categorylevel1, level2_categorylevel2, confidencefloat(conf) )一个小细节是输入文本的长度处理。工单文本平均280字但BERT的限制是512个token直接截断到前128个token会丢掉关键信息。我们试验过几种方案最终采用的是头尾截断策略保留文本前100个token和后28个token。原因很朴素大多数工单的核心诉求在开头就说明了而末尾往往带着用户的具体要求或补充信息中间部分多是情绪化表达信息密度最低。这个简单的改动让验证集的F1提高了1.4个百分点。模型服务用Uvicorn启动配合Gunicorn做多worker管理。GPU节点上部署了四个worker单条推理的P95延迟大概在40毫秒左右完全够用。这里提一句如果你们公司没有GPU资源可以考虑用ONNX Runtime或OpenVINO做CPU推理代价是单条延迟可能涨到200到300毫秒但如果工单量不大CPU方案其实也能扛住。4.2 工单接入链路同步调用还是异步消息工单分类的调用链路我们最终设计成同步为主、异步兜底的策略。用户提交工单这个动作本身是异步的客服系统把工单落库后发送一条消息到RocketMQ模型服务消费消息进行推理再把分类结果写回工单主表和ES索引。这个链路的优点是吞吐量高不会被模型推理速度卡脖子缺点是工单入库和分类完成之间存在几秒钟的延迟。对于需要即时反馈的场景比如用户在App上提交投诉后马上看到处理分支这个延迟有点长。所以我们额外开放了一个同步分类接口供在线客服系统在坐席转交工单时实时调用拿到分类结果直接展示给坐席确认。两条路并存的效果是批量工单在后台自动分类在线工单实时预分类业务上完全覆盖。消息消费这块有个值得注意的坑消息的幂等性。RocketMQ天然支持消费者重试但重试意味着同一条消息可能被消费多次。如果消费逻辑里直接把分类结果写库不做判断就会产生重复数据。我们的解决办法是在工单表上建了唯一索引基于工单ID做插入冲突时更新同时消费逻辑里判断工单的当前状态如果已经是已人工复核状态就跳过自动分类避免覆盖人工结果。4.3 服务治理注册发现、限流熔断与降级服务拆了之后服务治理就是必须的功夫了。我们用Nacos做服务注册与发现所有微服务启动时自动注册调用方通过服务名从Nacos拉取实例列表再配合负载均衡调用。这样任何服务实例挂了只要Nacos健康检查发现异常就会自动摘除实例调用方无缝切换到其他健康实例。限流是另一个必须做的事。模型推理服务是最脆弱的环节GPU显存和计算能力有限如果突然涌入大量请求很容易被打爆。我们在Kong网关上配置了令牌桶限流默认每台模型服务实例每秒钟最多处理50个请求超过的直接返回429。网关层还有一些简单的规则请求体超过1MB的直接拒绝单条文本长度超过2000字的直接截断这两个规则拦截了大多数异常流量。熔断降级的经验是从一次小事故里学来的。某天ES集群因为磁盘写满导致响应变慢连带着工单查询接口也变慢了最终拖垮了依赖ES的服务。从那之后我们在所有服务间调用里都接入了熔断器用的是Resilience4j核心配置是失败率超过40%就打开熔断器后续请求快速失败不再等待下游超时。熔断打开后工单分类功能会临时降级为基于规则的关键词分类虽然精度差一些但至少不会让整条链路都挂掉。这个降级策略在生产环境的稳定性评估里加了不少分。4.4 数据闭环人工复核结果如何回流到训练集系统上线后我特别坚持做的一件事就是数据闭环。分类系统再准也会有一定比例的错分这些错分数据如果不回收利用模型的准确率天花板就永远锁死在首次训练的水平上。我们的做法是在管理后台加了一个人工复核功能。自动分类结果被展示给负责工单分派的客服人员客服可以一键接受或修改分类结果。所有被人工修改过的工单都会被标记为高置信度负样本或低置信度正样本定期回流到训练集。每两周做一次增量训练把新收集的修正样本和原训练集合并重新微调模型。别小看这个闭环它解决了AI系统最容易出现的问题——模型和数据脱节。第一版模型上线三个月后因为业务方调整了部分产品线出现了一批新类型的投诉文本模型完全没见过准确率一度跌到78%。正是靠着人工复核数据的持续回流我们才在两次增量训练后把准确率拉回了90%以上。这个机制比任何监控告警都有用。5. 压测、上线与运行中的真实问题5.1 压测数据与性能瓶颈定位上线前的压测是我们做的第一场硬仗。用JMeter模拟真实流量做了三轮压测目标是最低支撑每日一万条工单的自动分类。实测结果是模型服务单实例在4个worker、GPU为V100的情况下每秒稳定处理120个请求P95延迟42毫秒P99延迟68毫秒远优于我们设定的3秒目标。瓶颈不在模型服务而在ES写入。工单分类结果要写回ES供查询分析高并发下ES的bulk写入出现明显的延迟抖动索引refresh间隔默认1秒数据量大时会造成写入堆积。解决办法是调整了ES的refresh_interval到30秒同时把写入改成bulk批量模式一批最多攒500条或5秒刷一次。调整后ES写入稳定了再也没有出现过积压告警。整个压测下来我们还定了一个重要的容量规划结论单台GPU节点支撑每天十万条工单绰绰有余因此在双十一这类大促场景不需要额外加GPU机器只需要在网关层放开限流阈值同时给模型服务多挂两个worker就行。这个结论让运维团队省了不少预算。5.2 长文本截断导致的误判上线第一周就收到了一个让人哭笑不得的反馈一个用户投诉商品质量问题工单全文接近一千字模型却把它分到了产品咨询。我们排查后发现问题出在长文本处理上。那条工单开头大段在描述使用场景和购买过程真正的核心诉求埋在中后段我们的头尾截断策略把中间内容直接丢掉了导致关键信息缺失。这个问题比想象中复杂。简单地把截断长度从128提升到256能解决一部分问题但会让推理延迟翻倍而且BERT对超长文本的注意力分布本来就稀疏加长不一定有效。我们最终采用了分块投票的方案把超长文本按句子边界切成多个块每个块独立输入模型得到预测概率最后按概率加权投票得出最终分类。这个方案把我头痛了很久的长文本误判问题基本解决了长文本的分类准确率从82%提升到89%。不过我要提醒一下分块投票也不是万能的如果一段超长文本里涉及多个问题投票结果可能依然偏颇。我们的兜底方案是建议业务方在工单系统层面做限制让用户提交投诉时填写核心问题简述字段。这属于从源头解决问题比任何算法技巧都实在。5.3 错别字、网络用语对模型的干扰中文文本分类绕不开错别字问题。工单里的错别字比例高得惊人尤其是语音转文字产生的同音错字维权写成围权发货写成发惑五花八门。预训练模型本身对错别字有一定的容忍能力但遇到高频干扰词时还是会影响判断。我们做了一版针对性的词典纠错覆盖工单语料中出现的Top 500错别字和网络用语映射比如芭比Q映射到完了/糟糕yyds映射到非常厉害这类。这个词典在预处理阶段执行效果立竿见影验证集F1提升了1.8个百分点。这里有个小建议词典不要自己拍脑袋编直接从历史工单里用统计方法挖出错别字和正确字词的共现关系再人工审核确认效率和准确率都更高。网络用语这块我们发现一个有趣的现象工单里的网络用语往往带有强烈的情绪意图有时候反而能帮助判断紧急程度。比如气炸无语彻底失望这类词频繁出现的工单大概率属于情绪激烈的投诉。所以我们把网络用语词典同时接入了紧急程度判定模块作为一个正相关特征倒也算意外收获。5.4 一次差点翻车的服务雪崩项目上线两周后我们经历了一次差点翻车的事故。某天下午三点一条投诉在社交平台发酵大量用户集中涌入客服系统提交相同主题的工单。流量峰值瞬间冲到平时的十倍网关层的限流触发后大量请求被直接拒绝用户端看到的是系统繁忙。这还不是最糟糕的——限流拒绝的请求在业务方有个默认重试策略等于把流量又重放了一遍导致后续服务持续被压。复盘下来根因有两个一是限流策略只挡了模型服务没有挡整个工单链路前端系统把流量全部打到了后端二是业务方的重试机制没有退避策略失败后立刻重试造成了雪崩。修复方案是定期调整网关限流阈值同时给重试队列加上指数退避最多重试三次每次间隔翻倍。整改后又做了一次模拟极端流量的压测系统平稳扛住了二十倍日常流量。这件事给我们的教训是技术方案再完美上线前也要把流量治理的每个细节走一遍。特别是限流、重试、超时这三件套一定要通盘考虑否则任何一个环节的失误都可能放大成整个系统的灾难。6. 沉淀下来的经验以及下一步想做的事项目上线到现在差不多五个月系统已经稳定处理了几十万条工单分类准确率维持在91%左右人工复核比例从最初的30%降到了12%。如果让我复盘一遍这个项目最大的体会其实是NLP模型只是整个系统的十分之一剩下九成的工作量全在数据、架构、服务治理和业务流程配合上。几个具体的经验分享给大家。类别体系一定要和业务方一起定并且留出扩展空间这个事急不得数据标注规范要提前写明白质量把控的优先级永远高于数量模型服务一定要和业务逻辑解耦不然每个版本的模型更新都等于一次冒烟测试最后人工复核数据回流到训练集的闭环是模型持续进化的生命线一定要在架构设计阶段就预留好。接下来我们计划做两件事。第一是把二级类别的数量从23个扩到40个左右覆盖更多细分业务场景目标是让工单能直接派发到具体班组而不是部门第二是引入小样本学习框架用更少的标注数据完成新类别的冷启动减少每次业务调整带来的标注成本。模型层面也在关注一些轻量化的方案比如将BERT蒸馏成小的文本编码器让推理服务从GPU节点迁到CPU节点把基础设施成本再降一降。最后再说一个实际操作中的小技巧每次模型迭代一定要把训练集、验证集、测试集的划分文件固定下来用版本号管理。我们早期就是因为数据集没固定两次训练的评估结果对不上浪费了整整两天排查。数据集的版本管理和代码版本管理同等重要。这个教训值得所有做NLP工程化的人记住。

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

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

免费获取报价 →
↑