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

用大模型API给客服工单做文本分类:从Prompt设计到成本优化全记录

用大模型API给客服工单做文本分类:从Prompt设计到成本优化全记录 ★ FEATURED ARTICLE
在智能手表产品的售后工单里每天涌入的用户反馈五花八门“手表充不进电”“连不上手机”“表盘能不能换个样式”“游泳能戴吗”“摔了一下屏幕花了怎么办”……这些内容散落在客服文本、表单备注、社区帖子里光靠客服同学一条条去读、去归类不仅慢而且归类口径经常不统一。我接到过一个小需求把这类用户问题自动归类成售后维修、功能咨询、App连接、账号同步、配件购买等类别方便后续统计故障率和优化产品。当时的第一反应是写一套关键词规则但试了几条真实工单之后我就放弃了。后来我改用大模型API来做这个分类任务效果比预期好很多而且从搭建到上线只花了一个多星期。这篇文章就把我完整的做法、Prompt迭代过程、踩过的坑、成本和性能的平衡策略都讲一遍。适合正在做客服工单自动化、文本分类、舆情归类的开发者和产品经理参考不需要你有深厚的机器学习背景只要能写一点Python就能复用这套思路。1. 为什么我一开始就排除了关键词规则和传统分类器很多人拿到文本分类需求第一反应是写正则、维护关键词表。我最初也是这么想的但把真实工单拉出来看了一遍之后发现用户说话的方式远比想象中复杂。1.1 用户反馈的“口语化”让规则引擎败下阵来用户不会按照产品手册的措辞来提问题。比如“连不上手机”这类问题实际工单里会出现各种表达方式“我手表蓝牙连不上”“手机找不到手表”“配对一直转圈”“扫码没反应”“它一直显示请重试”。如果只维护“连不上”这个关键词后面这些表达全部会漏掉。更麻烦的是省略主语和口语缩略。有用户写“老是断气死了”没有出现“手表”或者“蓝牙”字样有用户写“戴着睡觉手腕红了一圈”表面上像健康咨询实际上是表带过敏的售后问题。这种省略和指代关键词规则很难覆盖全。我做过一个简单统计随机抽500条真实工单如果能用“关键词规则”准确定位类别的不到60%剩下40%依赖上下文推断。这个比例决定了规则引擎不适合作为主力方案最多只能用来做前置过滤。1.2 标注成本、冷启动与大模型API的差异那是不是可以采用传统机器学习分类器比如朴素贝叶斯、SVM或者BERT微调。这类方案效果其实不错但有一个绕不开的前置条件标注数据。要训练一个能覆盖十几个类别、稳定上线的分类器至少需要几千条人工标注样本而且每种类别都得有足够的正例。冷启动阶段根本没这么多历史标签。就算有标注数据后续维护也不轻松。智能手表的用户问题会随产品迭代变化比如新系统版本出了个“抬手亮屏失效”的bug、新表带配色引发“颜色和图片不符”的投诉类别边界随时要调整传统方案每次都要重新训练和评估。大模型API走的是另一条路不需要训练只需要在Prompt里把分类说明、示例、输出格式写清楚就能在“零样本”和“少样本”场景下直接分类。实际测试下来对常见类别的识别准确率能到90%以上长尾表达也能靠语义理解兜住。下表是我在项目初期做的三种方案对比维度关键词规则传统分类器微调大模型API冷启动成本低但要长期维护词表高需要大量标注数据低靠Prompt即可口语化/省略表达差漏召回严重中取决于标注覆盖较好能理解语义类别调整改词表即可要重新训练改Prompt即可单条成本几乎为零训练后推理便宜有API调用费用最难维护的点词表膨胀标注集过期Prompt边界漂移结论很清晰在“工单量每天几百到几千条、类别可能随版本变化”的场景下大模型API是投入产出比最高的选择。当然它不是万能的后面我会讲它有哪些需要人为兜底的地方。2. 从原始工单到结构化分类的数据清洗与分类体系设计想拿到稳定的分类结果不能直接把工单原文丢给模型。我花了近一半时间在“输入侧”和“体系侧”下面这部分的坑比调整Prompt本身更值得看。2.1 输入归一化去重、脱敏、截断工单来源不同格式也很乱有的是客服手动录入有的是用户在App表单里提交有的来自社区帖子爬取。直接投喂会带来两个问题一是脏数据干扰判断二是个人隐私字段被送进API。我做的清洗分三步。第一步是去重按“用户ID 问题内容前50个字符”算一个哈希重复工单只保留时间最新的一条。第二步是脱敏把手机号、邮箱、IMEI号全部替换成占位符比如把“138xxxx1234”替换成“手机号”。这一步不只是合规问题也防止模型把一串数字误判成“系统错误代码”。第三步是降噪HTML标签、超链接、无意义的重复标点直接删除。长度控制也很关键。工单最长能到几千字但大模型API的输入长度和处理成本都有限。实测下来一条用户问题最核心的信息通常集中在前200个字和最后100个字中间大量是对客服吐槽、重复描述。我对超过600个字的工单会做截断保留前400字和最后150字中间用省略号连接。这样既保住了关键信息又把token开销压下去了。2.2 分类体系先定边界再让模型输出JSON别急着写Prompt先花时间把分类体系设计清楚。我最初只给了6个一级分类结果发现很多工单处在模棱两可的地带。比如“手表进水了”既可能是功能咨询防水等级也可能是售后维修故障申请“表带长了想换一根”既可能是配件购买也可能是尺码咨询。我的做法是两级分类一级分类决定责任部门二级分类决定具体处理动作。下面是最终定下来的体系一级分类二级分类示例典型表达售后维修充不进电、屏幕损坏、电池续航、进水故障、按键失灵“手表黑屏了”“充电没反应”功能咨询设置操作、健康监测、运动记录、表盘更换、固件升级“怎么看心率”“步数不显示”连接与App蓝牙配对、扫码绑定、消息通知、数据同步、Wi-Fi设置“手机找不到手表”“同步一直转圈”账号与数据登录异常、账号绑定、数据丢失、多终端同步“登不上账号”“步数被清空了”购买与配件表带购买、充电器丢失、保修查询、价格咨询、尺码疑问“表带哪里买”“能换皮带吗”健康安全与过敏表带过敏、皮肤不适、辐射担忧、佩戴不适“戴久了手腕发红”其他无效内容、与产品无关、表扬“谢谢”“你们公司地址”每个类目我都写了“包含/不包含”的边界说明。比如“健康安全与过敏”明确包含“皮肤发红、瘙痒、红肿”不包含“心率测不准”这是功能咨询。这一步很枯燥但能显著降低模型乱分类的概率。输出格式上我要求模型必须返回JSON包含main_category、sub_category、confidence、reason四个字段。main_category是上面的一级分类confidence是一个0到1的打分reason是模型判断的依据。2.3 让脚本跑起来的最小实现分类体系的Prompt定好之后接口调用本身并不复杂。下面是一个最小可运行示例我做了简化但核心结构跟线上一致import requests import json API_URL https://api.example.com/v1/chat/completions API_KEY sk-你的密钥 system_prompt ( 你是一个智能手表客服工单分类助手。\n 根据用户问题文本判断其所属分类。\n 分类体系\n 售后维修包含充电、屏幕、电池、进水、按键等故障。\n 功能咨询包含设置、操作、健康监测、表盘、固件升级等咨询。\n 连接与App包含蓝牙配对、扫码、同步、通知等连接问题。\n 账号与数据包含登录、绑定、数据丢失等。\n 购买与配件包含表带、充电器、保修、价格等。\n 健康安全与过敏包含皮肤过敏、不适等。\n 其他以上都不属于的。\n\n 只输出JSON格式为\n {main_category: 一级分类, sub_category: 二级分类, confidence: 0.0, reason: 判断理由} ) user_content 我手表充不进电一直显示红色闪电标志 payload { model: small-model-for-classification, temperature: 0, messsages: [ {role: system, content: system_prompt}, {role: user, content: user_content}, ], response_format: {type: json_object}, } resp requests.post( API_URL, headers{Authorization: fBearer {API_KEY}}, jsonpayload, timeout30, ) data resp.json() result json.loads(data[choices][0][message][content]) print(result) # 预期输出 # {main_category: 售后维修, sub_category: 充电故障, # confidence: 0.98, reason: 用户提到充不进电和红色闪电标志属于充电故障}这段代码要注意两个点。第一是temperature必须设为0。分类任务是确定性任务温度越高越容易把同一个问题分成不同的类别这对后续统计是灾难。第二是response_format请求JSON对象输出能大幅减少“模型返回了一段带解释文字的非结构化文本”的情况。3. Prompt迭代实录三版方案的误判对比与最终模板Prompt是整个项目里迭代次数最多的部分前后改了三版。每一版都解决了一个具体问题。3.1 第一版只给分类名模型发挥得太“自由”第一版我偷懒Prompt只写了“你是智能手表客服分类员请把下面问题分类到最合适的类别售后维修、功能咨询、连接与App……”。结果问题很多。常见错误有三种。第一种是分类粒度太粗“手表掉水里开不了机”被分成“功能咨询”因为模型觉得用户在问“能不能开机”而不是在报修第二种是输出格式不稳定有时候返回“这个属于售后维修”有时候返回“售后维修”有时候还带一段解释第三种是“其他”类别被滥用只要表述稍微绕一点模型就倾向于丢到“其他”。我拿200条人工标注的验证集跑了一遍第一版整体准确率只有78%而且“售后维修”和“功能咨询”之间混淆最严重。3.2 第二版加few-shot示例与边界定义误判大幅下降第二版我把每个类别的包含、不包含定义写进去每个类别还加了两个正例和一个反例。比如连接与App包含蓝牙配对失败、扫码失败、手机找不到手表、数据同步中断。 不包含App功能怎么用这是功能咨询、手表上某功能不显示这是功能咨询。 示例 正例“我手机搜索不到手表蓝牙” 反例“手表上心率怎么测”应归为功能咨询这版效果立竿见影验证集准确率从78%提升到86%。剩下的误判主要是两类一类是模型把“用户要求退款投诉”分到“购买与配件”而不是“售后维修”另一类是“表盘样式不好看”这种主观意见被误判成“功能咨询”。我需要说明一下给Prompt里加示例会增加大约600到1000个token的输入但换来的是更稳定的分类结果这笔账很划算。如果你后续换模型或者调体系这版Prompt可以直接当作基线。3.3 第三版结构化输出、置信度与拒识兜底第三版我做了两件事。一是严格落实JSON结构化输出字段固定为main_category、sub_category、confidence、reason并且要求confidence低于0.3时main_category必须填“其他”并标记为uncertain。二是增加了一条“拒识”指令如果用户问题信息不足无法确定任何类别请将 main_category 置为“其他” 并在 reason 里写明缺少什么信息。不要强行猜测。为什么要加拒识因为让模型硬猜出一个类别比让它承认“不知道”后果更严重。硬猜的标签会污染统计数据比如把一条投诉“客服态度差”分到“功能咨询”产品经理看了报表会很困惑。拒识工单可以单独进入人工队列处理。第三版验证集准确率最终到了91.2%。剩余接近9%的误判一部分是真实歧义工单比如“手表进水了”这种既像咨询又像报修的一部分是模型理解偏差。后者我通过持续补充示例来压前者只能接受因为人工处理本身也未必能达成一致。最终Prompt模板大概是这个结构角色定义、分类体系含边界、输出格式、拒识规则、少量正反例。模板头可以复用个别长尾示例按需增删。4. 成本与吞吐量模型选型、并发和缓存怎么配分类任务用不起高端大模型这是很多人一开始就会担心的事。实测下来只要选对模型和参数成本完全在可控范围内。4.1 不同档位模型的实际效果与价格拆解我在测试中用两档模型做了对比。A档是推理能力更强的旗舰模型B档是面向批量任务设计的低成本小模型。在同样的Prompt和验证集下A档准确率约93.5%B档约91.2%。差距只有2.3个百分点但B档单价低得多。为什么差距这么小因为分类任务是“理解映射”不需要复杂推理和多步思考。模型的“常识理解”能力在小模型上也已经成熟真正决定准确率的是Prompt定义清不清楚而不是模型参数大小。我的建议是先用旗舰模型跑小批量验证集确定Prompt没问题确定之后切换成低成本模型跑正式数据。如果发现低成本模型在某个类别上准确率明显下降再从Prompt角度补示例或边界说明而不是轻易换回旗舰模型。4.2 上下文瘦身、缓存与并发参数估算成本主要消耗在输入token上尤其是那段分类体系说明。我在线上做了几个优化。第一把系统提示词拆成固定部分和可变部分固定的分类体系可以做请求级缓存避免反复计入计费第二工单文本截断前端先做长度清洗避免把冗长投诉全文塞进去第三批处理时把多条短工单拼接成一个batch每条工单之间用分隔符隔开批量返回后再拆解能显著降低每条的“固定开销”。举个粗略的估算例子一条工单平均150字约200到250个token系统提示词约1200个token。一次调用的总token数大约是系统提示词加上工单token。10万条工单B档模型如果用每百万token几十元的单价水平粗算总成本大概在一万多元量级如果每条都重新发送一遍超长系统提示词这个数字会再翻一倍。所以“让系统提示词变短、可缓存”不是技巧问题而是直接决定项目能不能落地的成本问题。并发方面我的经验是设置15到20个并发请求比较稳妥。太快会被API限流报429错误反而拖慢整体速度。1万条工单在这种并发下单条大约2到4秒返回全部跑完大概半小时到一小时完全能接受。4.3 离线批处理与实时分类的取舍这个项目里有两种场景。离线批处理是处理历史存量工单用于统计故障率、复现高频问题对延迟完全不敏感用队列慢慢跑就行。实时分类是客服系统接到新工单时即时判断来路要求响应时间尽量短。实时场景我建议做两个降级策略一是confidence低于阈值的工单直接转人工不等模型纠结二是如果API调用超时先把工单丢进“待分类”队列定时补跑而不是阻塞客服操作系统。自动化分类的目标是减轻人工负担不是制造新的阻塞点。5. 上线后我才实际遇到的那些坑上面说的都是“正常流程能跑通”的部分。接下来这些坑是我上了正式环境之后才陆续遇到的每一条都曾经让我半夜爬起来看日志。5.1 编码、表情符号与“看起来正常”的乱码第一次全量跑历史工单我发现有大约2%的工单分类结果全是“其他”。检查原始文本后发现问题出在编码上一部分老工单来自网页端中文被错误编码成了乱码还有一部分带有特殊表情符号模型看到一串未知符号直接判定为“其他”。解决方式是在清洗阶段增加白名单字符过滤保留中英文、数字、常见标点把未知字符替换成空格再做一次URI解码。清理之后乱码工单的比例降到0.3%以下。5.2 重复工单和“同一条问题追问两遍”线上有用户会反复提交同一问题“手表突然黑屏了”“手表还是黑屏”“怎么没人回复我”。这三条工单单独看都会被分类但统计时会出现同一个问题被计了三次导致故障率虚高。后来我把去重逻辑往前挪了一步。按“用户ID 内容语义相似度”做合并简单实现是对处理后的工单文本计算哈希内容完全相同的直接合并相似度高但表述不同的按时间顺序只保留最新一条并附带“重复提交次数”字段。这样既保证了分类效率也保住了统计口径。5.3 偶发非法JSON与超时重试虽然我设置了JSON输出格式但正式环境里还是偶尔会返回非JSON内容比如{main_category: 售后维修, sub_category: 屏幕故障, ...少了一个右括号。这类情况比例很低大概千分之一但在全量跑10万条时就会累计100次报错。我的处理方式很简单捕获解析异常后自动重试一次如果重试后仍然失败就把原始文本和模型返回内容一起写入人工队列。单独为这种极小概率场景去写复杂的JSON修复逻辑性价比不高。超时重试也有讲究。我的超时设置为30秒重试间隔按指数退避第一次等3秒第二次等9秒最多重试3次。不要用固定间隔硬冲否则遇到API偶发抖动时你的重试请求会把自己限流。5.4 一条工单里多个主题main/related怎么处理用户经常在一条工单里说好几件事“手表进水了表带也过敏还想问问能不能退货。”这种多主题工单用单一分类字段会丢掉次要信息。我的方案是分类输出不只有一个主类而是增加两个字段main_category和related_categories。main_category表示最紧急、最需要优先处理的类别related_categories是数组表示这条工单同时还涉及哪些类别。上面的例子会输出主类是“售后维修”相关类是“健康安全与过敏”和“购买与配件”。实际使用下来客服组觉得这个结构比单分类好用得多因为一眼就能看到需要同时协调几个环节。另外还要注意一个细节类别之间可能有关联但Prompt里不要写“如果既属于A又属于B请选择A”这种模糊指令。更好的写法是明确优先级比如“账号与数据”当用户提到“账号数据丢失”时即使也有“无法登录手机App”的连接问题主类仍然优先归“账号与数据”。项目上线以后客服同学每周节省出来的时间至少有几个小时故障类报表的归类口径也比以前统一了。但说实话真正让我觉得这套方案站得住脚的不是API本身的效果而是“清洗——体系设计——Prompt迭代——兜底策略”这一整条链路都走通了。最后分享一个我反复验证过的体会不要急着调参数、换模型先把分类体系和边界定义写清楚再用两三百条人工标注数据做验证模型不管换哪一档都不会差太多。而每次改过Prompt之后都要在同一份验证集上重跑一遍别凭感觉说“感觉变准了”——数字不会骗人。如果你也准备做类似的事情建议从最脏最真实的工单开始处理而不是用自己写的干净样例来测试那才是这套方案真正要被考验的地方。
阅读完成 · 觉得有帮助?
咨询建站