ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

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

在智能手表产品的售后工单里,每天涌入的用户反馈五花八门:“手表充不进电”“连不上手机”“表盘能不能换个样式”“游泳能戴吗”“摔了一下屏幕花了怎么办”……这些内容散落在客服文本、表单备注、社区帖子里,光靠客服同学一条条去读、去归类,不仅慢,而且归类口径经常不统一。我接到过一个小需求:把这类用户问题自动归类成售后维修、功能咨询、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": f"Bearer {API_KEY}"}, json=payload, timeout=30, ) 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之后,都要在同一份验证集上重跑一遍,别凭感觉说“感觉变准了”——数字不会骗人。如果你也准备做类似的事情,建议从最脏最真实的工单开始处理,而不是用自己写的干净样例来测试,那才是这套方案真正要被考验的地方。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进