微调还是 RAG?42 类工单分类从 0.63 做到 0.91,我把每一步都记下来了

🔑 关键词:自然语言处理,LoRA微调,RAG检索增强,文本分类,大模型落地

📖 摘要:一个工业售后工单分类项目的完整复盘:为什么不先调模型、错误归因怎么做、LoRA 的真实显存账单和三个必踩的坑、RAG 什么时候才该上,以及我对「微调还是 RAG」这个二选一问题的反常识结论。

先说结论,免得你看到一半想骂我

图片

「微调还是 RAG」这个问题本身就是个陷阱。我做过 7 个 NLP 落地项目,真正决定效果上限的既不是 LoRA 的 rank 设多少,也不是向量库选 Milvus 还是 pgvector,而是你那 500 条 badcase 里错误的分布长什么样。把错误归因做对了,后面选什么都是顺水推舟;归因做错了,你就是在给一个错误的方向做参数调优。下面这个项目我完整记了过程,数字都是真的,坑也是真的。

一个 18 万条工单的项目,以及它一开始就不太对劲的地方

去年 11 月,一个做工业设备售后的朋友找我。他们想把客服工单自动分到 42 个类目,同时把设备型号、故障现象抽出来。当时的方案是 gpt-3.5-turbo 加一段大概 2000 字的 prompt,塞了 5 个 few-shot 例子,月账单 1900 多美元,准确率他们自己抽了 200 条测,0.63。

我看到的第一反应是:42 个类目、18 万条历史工单,这不是标准的监督学习场景吗?RoBERTa-base 才 1.25 亿参数,单张 3090 就能微调,推理成本几乎可以忽略。为什么要花钱调 API?

后来才搞明白,他们的技术栈是纯 Java 加一点 Python 脚本,团队里没有人做过模型训练和部署。用 API 不是他们选出来的最优解,是他们能力圈里唯一的路。这个细节挺关键的,很多技术选型讨论都忽略了一件事:方案的上限由算法决定,方案的下限由团队决定,而大多数项目活在两者之间的下半区。

我花了整整两周,做了一件看起来最不技术的事

我把他们最近一个月的 1200 条工单捞出来,人工标了 500 条当诊断集,然后一条一条看错在哪。这一步没什么技术含量,但它把后面的方向全定了。

500 条 badcase 大概这么分布:

图片

  • 大概 220 条,是分类体系本身在打架。「无法开机」和「电源故障」是两个类目,但标注员自己都分不清该选哪个,历史数据里这两类的比例是 3:1,内容上高度重叠。
  • 大概 140 条,是工单文本太短。这批工单平均只有 18 个字,最短的一条是「机器不转」,四个字。你让什么模型来都很难只凭这四个字判断是电机问题还是控制板问题。
  • 大概 90 条,是 prompt 里塞了太多类目定义。2000 字的 prompt 里,42 个类目的描述占了 1400 字,模型注意力被稀释得厉害。
  • 真正需要更强模型或者需要微调的,大概 50 条。十分之一。

对了,标注一致性 Kappa 只有 0.51。这个数字意味着,就算你把标注当成金标准,模型的上限也就那样了。

先动分类体系,42 个类目砍到 26 个

这一步我做得挺粗暴的。把混淆矩阵拉出来,macro-F1 低于 0.4 的类目全部人工过一遍,能并的并,能删的删。

「无法开机」并进「电源故障」,「屏幕黑屏」并进「显示异常」,三个描述不同故障现象的类目合并成一个「运行异常」。42 个类目最后剩 26 个,代价是标注规范重写了一遍,历史数据里有大概 2.3 万条需要重新映射。

结果:光这一步,准确率从 0.63 到 0.78。没动模型,没动 prompt,没加钱。

我知道你想说这个提升是「作弊」,类目变少了当然容易分。对,类目少了确实更容易,但这个简化是有依据的——那 220 条 badcase 说明这 16 个类目本来就不该存在。业务侧的损失是什么?他们原来的分类是为了路由到不同工单队列,合并之后只有两个队列的负责人来找过我,其他人都没意见。

图片

现在才轮到模型选型

分类体系稳定之后,剩下来的问题就简单了。我试了四条路线,用的是同一份 500 条诊断集:

  1. RoBERTa-base 微调,110M 参数左右。加了 3200 条训练数据(从历史数据里清洗出来的),3 个 epoch,单卡 3090 跑了 22 分钟。macro-F1 0.87。
  2. Qwen2.5-7B 做 zero-shot,prompt 精简到 600 字。macro-F1 0.79。
  3. Qwen2.5-7B 加 LoRA 微调,rank=16,alpha=32。macro-F1 0.91。
  4. 继续用 gpt-3.5-turbo,但 prompt 精简加类目砍到 26 个。macro-F1 0.83。

最后我选了 RoBERTa-base 做主分类器,Qwen2.5-7B 只做兜底——当 RoBERTa 的 softmax 输出最大概率低于 0.6 的时候,才丢给大模型复议。

为什么不是 LoRA 那版?F1 高 4 个点,但推理成本差了大概 30 倍,延迟差了 8 倍。我们线上 QPS 峰值 40 左右,7B 模型就算上 vLLM,单张 A100 也就勉强撑住。而 RoBERTa 在 CPU 上跑,单条 8 毫秒。那 4 个点不值得。

我知道这跟现在「一切皆大模型」的主流叙事不太一样。但分类任务和生成任务是两回事,判别式模型在这个场景下就是更划算。你用 GPT-4 去做情感二分类,不会比一个调好的 BERT 好多少,但成本是它的几百倍。

LoRA 的实际显存账单,以及我踩的三个坑

虽然最后没用 LoRA 上线,但过程中调了不少,把数字记一下,免得你重复算。

图片

7B 模型全参微调,bf16 权重 14GB,梯度 14GB,Adam 优化器状态(fp32 的 m 和 v)大概 56GB,加起来 84GB 打底,再加激活值,单卡 A100 80G 是绝对不够的,得上 FSDP 或者 ZeRO-3。

LoRA 就友好得多。冻结的 bf16 权重还是 14GB,LoRA 参数如果在 q、k、v、o 和 FFN 的 gate、up、down 上都加,rank=16 时大概 4000 万参数,fp32 存也就 160MB 左右,优化器状态更小。激活值才是大头,seq_len=1024、batch=1 的时候几个 GB。实际跑下来,A100 40G 很舒服;4090 24G 上把 seq_len 降到 768、batch 设 1、梯度累积 16,也能跑,就是慢。

三个坑:

第一个,alpha 和 rank 的比值比它们各自的绝对值重要。很多人 rank 设 64、alpha 还是 16,等于把 LoRA 的更新幅度压得很小,训练半天没动静。我一般 rank=16、alpha=32,或者 rank=8、alpha=16,让 alpha/rank 保持在 2。

第二个,学习率不能用全参微调的量级。全参微调常用 1e-5 到 2e-5,LoRA 我一般用 1e-4 到 3e-4。用错了要么不收敛,要么 500 步就开始过拟合。

第三个,也是最坑的:max_seq_len 别照抄 512。我看了下他们的工单,95% 在 180 个 token 以内,我设成 256 之后,吞吐直接翻了快一倍,显存也降了不少,F1 一点没掉。这个调整带来的收益,比我调两周 rank 和 lr 加起来还大。

RAG 什么时候才该上

图片

这个项目其实不需要 RAG。工单分类是封闭类目问题,知识全在分类体系和标注数据里,没有外部知识检索的必要。

但 RAG 我做得比较多,说说判断标准。

一句话:如果你的模型答错的 80% 是因为「不知道事实」,那就上 RAG;如果是因为「格式不对」「指令没听懂」「风格不像」,那 RAG 帮不了你,得靠微调或者换更强的模型。

RAG 这条链路上,检索质量比生成质量重要得多,这个我说多少遍都不为过。我做过一个医疗问答的项目,同样的生成模型,只把 chunk 大小从 1024 调到 512(overlap 保持 64),召回率从 0.71 涨到 0.84。后来又加了一层 bge-reranker-v2-m3 重排,把 top-20 重排到 top-5,答案准确率从 0.62 到 0.79。

顺便说个坑。很多人以为 context 塞得越长越好,其实不是。Liu 他们 2023 年发在 TACL 上的那篇 Lost in the Middle 讲得很清楚:相关信息放在上下文开头或者结尾,模型能找到;放在中间,性能显著下降。所以宁可重排后只放 3 条精准的,也别塞 15 条进去碰运气。

评估这块,我的看法可能比较非主流

我不太信 BLEU 和 ROUGE。BLEU 最早是为机器翻译设计的,算的是 n-gram 重合度,对语义相近但用词不同的答案惩罚过重。ROUGE 用在摘要上稍微好一点,ROUGE-L 算的是最长公共子序列,对语序有容忍,但还是偏表面。

我的做法是:花两天时间,人工标 200 条黄金集,然后算三个东西——分类任务看 macro-F1(别用 accuracy,类目不平衡的时候 accuracy 会骗你),抽取任务看 entity-level F1(不是 token-level,边界对齐的惩罚不一样),生成任务如果一定要自动化指标,用 BERTScore 比 BLEU 靠谱,但我还是会抽 50 条人工看。

图片

做 RAG 的话,我会额外算 faithfulness——答案里的每一句话,能不能在召回文档里找到对应。这个指标比答案像不像更重要,因为答非所问的幻觉在客服场景里是致命的。

我现在遇到「微调还是 RAG」的决策顺序

  • 第一步,捞 300 到 500 条 badcase,人工分一遍类。错误类型没搞清楚之前,什么方案都别选。
  • 第二步,看错误里有多少是「知识缺失」。这个比例超过 60%,走 RAG;低于 30%,走微调。
  • 第三步,看数据量。标注数据低于 500 条,先别微调,用 few-shot 加 prompt 工程顶一顶。500 到 5000 条,LoRA 性价比最高。超过 5000 条且稳定,才考虑全参微调。
  • 第四步,看延迟和成本预算。如果要求 P99 低于 100 毫秒,或者 QPS 上百,别犹豫,上小模型。
  • 第五步,两个都要也不是不行。我见过效果最好的架构是:小模型做分类和路由,检索召回候选知识,大模型只在置信度低的时候介入。两段式,成本可控,效果也不差。

最后说点可能不太受欢迎的

现在很多团队一上来就想上 RAG,因为它听起来高级,demo 也容易做——把文档切一切、向量化、塞进 prompt,跑通只要半天。但要跑到生产可用,调 chunk、调重排、调 prompt、处理文档解析里的表格和公式,工作量一点都不比微调少。

反过来,微调也不是万能药。我见过有人拿 200 条数据去微调 7B 模型,结果模型变成了复读机——训练集的答案背得滚瓜烂熟,新问题一个都答不上来。数据量不够的时候,few-shot prompting 加一个靠谱的重排,效果往往比硬微调好。

真正的问题从来不是「微调还是 RAG」,而是「我的模型错在哪,为什么错」。回答不了这个问题,选什么都是赌。

🏷️ 标签: