
做了这么久RAG检索增强生成项目我越来越觉得真正的难点往往不在检索和生成环节而在入口处。很多朋友把知识库问答系统部署上线之后发现用户根本不按套路出牌有人上来就问“你叫什么名字”有人直接甩一句“帮我把这段翻译成英文”还有人把一堆合同文本粘贴过来要求写摘要。这些诉求跟你的知识库内容可能半毛钱关系都没有但RAG主流程根本分不清它只管把你的问题送去向量检索、重排、拼接、生成。最后的结果就是系统辛辛苦苦从库里捞出一堆无关片段大模型再一本正经地编答案。问题就出在链路入口缺了一道“前置路由”。我现在的看法是一个合格的RAG系统不应该是单行道而应该像高速公路的匝道口先识别用户到底想干什么再把请求分流到对应分支。这也是为什么我一直坚持意图分类微调是RAG落地过程中最值得投入的一环。手写规则只能覆盖有限场景用大模型做分类又贵又不稳定最稳妥的方案就是训练一个独立的、轻量的意图分类小模型专门承担前置路由职责。这篇文章不绕弯子围绕“RAG意图分类微调”和“前置路由”这两件事从标签体系设计、训练数据准备、模型选型、微调实操、管线接入到线上问题排查把完整流程走一遍。正在做知识库问答、客服助手、智能问答机器人或者正被RAG“答非所问”折磨的朋友可以直接把整套流程拿去参考。1. 为什么非要在RAG入口做意图分类1.1 没有路由的RAG就像没有分诊台的医院先讲一个我实际遇到的场景。某客服知识库系统上线后用户问得最多的其实是“你工资多少”“你是真人吗”这类闲聊问题真正来查业务条款的反而被淹没在无关请求里。系统统一走“向量召回-重排-生成”的链路结果就是闲聊问题被当成知识问题去检索回答自然牛头不对马嘴。你会发现真实用户的输入大致可以分成几类知识问答查询产品条款、政策说明、闲聊寒暄打招呼、情绪表达、文本处理翻译、润色、总结摘要、任务指令设置提醒、生成表格、写邮件、拒答类涉及隐私、超出政策范围。这五类请求的后续处理逻辑完全不同。知识问答需要检索知识库闲聊需要走闲聊模块文本处理需要调用专门的生成流程拒答类甚至根本不该进RAG。没有前置路由所有请求都挂同一个科相当于医院不收分诊台把头疼脑热和心梗病人全送进骨科。RAG链路本身做得再精致入口没分流整体体验都会被拖垮。所以我在新的项目里都坚持先做意图分类再决定后续链路怎么走。1.2 为什么不用大模型提示词直接分类有人会觉得意图分类而已直接写一段系统提示词让大模型判断用户属于哪类不就行了我早期也这么干过但实际跑下来问题不少。第一是不稳定。同样一句话大模型今天判成“知识问答”明天温度调低一点就判成“闲聊”没有确定的输出边界。做路由最怕不确定因为下游链路是根据分类结果决定走检索还是走摘要的分类飘一次整个回答就歪了。第二是延迟。每来一条用户问题先调一次大模型做分类再走RAG链路生成多了一次网络来回在线体验至少多几百毫秒。第三是成本。高并发场景下多一次大模型调用就多一笔token开销日积月累非常可观。第四是上下文污染。为了分类准确提示词里经常要塞各种规则和示例这些信息会占用上下文窗口影响后续生成质量。微调一个轻量级意图分类模型就能很好规避这些问题。小模型输出是确定性的没有随机采样带来的漂移模型体量小推理快可以本地部署单次推理延迟能压到几毫秒到十几毫秒一次训练好之后调用成本几乎可以忽略。我在实践里的做法是小模型负责粗粒度意图分流大模型只在特定分支里做深层理解和生成。两者各管一段既能保证路由稳定又不牺牲生成质量。1.3 前置路由带来的实际收益加了前置路由之后整个RAG系统的行为会发生明显变化。检索压力显著下降。非知识类请求根本不会进入向量检索向量库的QPS会降一个量级检索超时和资源竞争问题随之缓解。回答质量提升。知识类请求可以增加必要的上下文确认和检索策略闲聊类请求可以走轻快的对话分支不再出现驴唇不对马嘴的失败回答。流程编排变灵活。很多场景需要多步骤处理比如用户先问“这个条款是什么”再问“那如果违约呢”这类前后依赖在路由层就能识别把上一步的结果带入下一步。另外拒答类和安全合规类请求可以在这层直接拦截不用等生成阶段再去救火。我见过不少团队把大量精力花在调优embedding和重排模型上效果却一直上不去最后发现最大的瓶颈就在入口。这不是说向量检索不重要而是说在没有分流的情况下检索质量再高也救不了根本不该检索的问题。前置路由做扎实之后后续优化才有意义。2. 前置路由的标签体系与训练数据准备2.1 标签设计四原则意图分类的起点不是模型而是标签体系。标签定错了后面全白做。我自己的经验是设计标签必须满足四个原则互斥、完备、可操作、可扩展。互斥指的是类与类之间边界要清晰。曾经有个同事把标签定为“业务咨询”和“产品咨询”结果发现大量请求既算业务也算产品标注员经常吵起来。后来干脆合并成一个“知识问答”再通过下游检索去区分对应条目。完备指的是标签覆盖面要够实际出现的高频请求都能落到某个标签里。可操作指的是每个标签必须有明确的后续响应动作比如“翻译请求”对应调用翻译引擎“闲聊”对应闲聊话术如果一个标签背后没有对应动作那这个标签就没有存在价值。可扩展指的是整个体系能随着业务发展增加新类而不是推翻重来。举一个实际可用的标签集合query_class。知识问答聊闲寒暄文本处理翻译润色总结摘要任务指令拒答类。其中“文本处理”“翻译润色”“总结摘要”可以根据业务需要合并或拆分核心原则是宁可粗一点不要为了分类而分类。2.2 数据采集与标注流程标签定好之后接下来就是凑训练数据。这一步最花时间也最影响最终效果。数据来源通常有三个历史会话日志、知识库候选问题、人工构造的扩展样本。历史会话日志是最有价值的因为用户真实输入五花八门涵盖各种口吻、错别字、口语化表达。我会先把日志里的query全部捞出来按频次排序再对高频query做初步筛选。筛选规则很简单小于两个字符的去掉纯表情符号的去掉重复的合并明显是测试数据或压测脚本的去掉。然后请人做双人标注A标注、B复核不一致的交给第三人仲裁。标注时要给标注员明确几件事按用户原始意图判断不要猜测对模糊请求能归类就归类只有确实无法判断才标“其他”必须保持原始文本不要改写、不要修正错别字。知识库候选问题是很好的补充它们能覆盖用户可能问但历史日志里还没出现的表达。我会从知识库的FAQ条目、文档标题、章节摘要里提取候选问题再用同义改写的方式扩充写法。比如“保修期多久”“这个产品保修多长时间”“坏了给修吗”本质上是同一意图作为同一条目下的多份样本。2.3 数据增强与难负样本处理实际标注完一轮之后你会发现类别严重不平衡。知识问答类样本一天能攒几百条闲聊和拒答类可能一周才凑几十条。这种情况下模型很容易把所有输入都判成大类。我的做法分两步。第一步是模板插槽扩充针对小样本类别。闲聊类可以设计一批模板比如“你是{称呼}吗”“今天{天气/心情}怎么样”“你{吃饭/睡觉}了吗”配合不同人称和具体名词做组合扩展。拒答类可以从业务合规文档里提炼典型触发词构造“你们能帮我查{隐私内容}吗”“我要投诉{具体对象}”这类表达。第二步是难负样本挖掘。有些query长得像闲聊实际是知识问题比如“你还活着呢”这种话在业务机器人对话里可能只是一种调侃路由判成闲聊是合理的但“你误工费怎么算呢”前半句像闲聊后半句是正儿八经的业务问题这类边界样本需要单独挑出来反复校正。数据增强之后还要做一步乱序验证。把同一类样本按相似度聚类检查是否存在大量重复模板如果发现某类样本高度同质就需要补充更多原生态语料否则模型只会记住模板结构换个说法就不认了。2.4 训练数据的数量与质量底线总有人问每类样本准备多少条才够。我给一个经验区间核心大类比如知识问答建议1500条以上次要类别比如翻译润色、总结摘要至少500条冷门类别比如拒答300条起步。整体训练集在3000到8000条之间对这类轻量分类模型来说通常都能训出不错的效果。少于200条的时候与其硬训不如先靠规则兜底等数据攒够了再上模型。比数量更重要的是质量。最容易犯的错是标注员“洁癖”把用户输入改得特别规范。比如用户明明输入“你们这个怎么退呀”标注员非要改成“请问如何办理退货流程”结果模型学到的全是规范用语线上遇到原生态表达就懵。我在标注规范里明确要求保留原样连错别字、网络用语、中英混写都原文保留。另外一个容易被忽略的点是长度问题。用户query有长有短短的可能就几个字长的会粘贴整段合同文本。训练时不能把超长文本直接截断丢进去而是要根据业务场景限制输入长度上限并决定超长文本应该如何采样。3. 模型底座与微调环境准备3.1 选型思路分类任务不要用生成模型意图分类本质上是文本分类任务。这类任务优先选择encoder-only结构的轻量文本分类底座而不是生成式对话模型。原理不复杂文本分类只需要建模输入序列的语义表示输出一个类别标签就够了用decoder模型生成式架构相当于杀鸡用牛刀推理慢、显存大、成本高。我会选择一个中文语料表现稳定的轻量底座参数量在1亿到3亿量级支持定长序列输入。这类模型对硬件要求低CPU也能跑后续转ONNX或者做int8量化也容易。可能有人想用更大的底座追求准确率但从实际经验看意图分类任务语义空间并不复杂5000条数据下大底座带来的增益非常有限反而会把训练和推理成本推高好几倍。打个比方路由只需要判断“前面是岔路还是直行”用不上高级自动驾驶的全套传感器。3.2 环境准备与依赖安装训练环境要求不算高一张12GB显存的显卡就能跑得很舒服数据量不大时8GB也够。建议Python 3.9以上核心依赖包括transformers框架、peft参数高效微调库、datasets数据集处理库、scikit-learn评估工具。安装命令很简单pip install transformers peft datasets scikit-learn建议再装一个accelerate控制分布式训练和混合精度会更方便pip install accelerate注意这几个库的版本要互相兼容。我踩过的一个坑是transformers版本升级后部分类名变了旧代码直接报错。建议大家建一个独立的虚拟环境不要跟我一样在全局环境里裸奔装坏了还得花一下午排查依赖冲突。3.3 训练数据格式标准化训练前要把标注好的数据转成模型能读的格式。主要做三件事标签转id、文本切分、统一长度。标签转id很简单建立一个字典映射比如{知识问答: 0, 闲聊寒暄: 1, 文本处理: 2, 翻译润色: 3, 总结摘要: 4, 任务指令: 5, 拒答类: 6}。文本切分则要看底座的最大序列长度如果用户原始query超过长度限制不能直接硬截否则会丢失关键语义。我的做法是优先保留开头和结尾的信息因为对意图判断影响最大的往往是句首动词和句尾宾语。下面这段代码负责把原始样本组装成模型输入格式from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(your-base-model) label2id { 知识问答: 0, 闲聊寒暄: 1, 文本处理: 2, 翻译润色: 3, 总结摘要: 4, 任务指令: 5, 拒答类: 6, } id2label {v: k for k, v in label2id.items()} def encode_sample(text, label, max_len64): encoded tokenizer( text, truncationTrue, max_lengthmax_len, paddingmax_length, return_tensorspt, ) return { input_ids: encoded[input_ids][0], attention_mask: encoded[attention_mask][0], labels: label2id[label], }max_len设64够用因为绝大多数用户query在二三十个字以内。数据集中如果存在大量长文本可以把max_len调到128但不要无脑调大长度越长模型越容易把注意力分散到无关内容上训练时间也会增加。4. 微调训练实操从指标看出门道4.1 关键超参数设计思路训练参数没有万能组合但针对这类小规模文本分类任务有几个经验值可以参考。Epoch设为3到5数据量不大时多了容易过拟合少了欠拟合。Learning rate在2e-5到5e-5之间使用LoRA参数高效微调时可以适当提高到1e-4。Batch size根据显存定16到32都可以。Warmup比例建议设为总训练步数的10%让模型在初期保持平稳。我见过一些人特别喜欢堆epoch训练集2000条数据硬训20个epoch最后模型把训练集里的每个模板都背下来了线上换一种问法就翻车。判断是否过拟合很简单观察验证集Loss如果训练Loss持续下降而验证Loss回升就要果断停。4.2 核心训练代码下面给出一个可直接改改就用的训练脚本框架。这里用LoRA做参数高效微调只训练一小部分参数显存占用小、训练速度快适合意图分类这种任务。from transformers import ( AutoModelForSequenceClassification, TrainingArguments, Trainer, ) from peft import LoraConfig, get_peft_model, TaskType model AutoModelForSequenceClassification.from_pretrained( your-base-model, num_labelslen(label2id), id2labelid2label, label2idlabel2id, ) lora_config LoraConfig( task_typeTaskType.SEQ_CLS, r8, lora_alpha16, lora_dropout0.1, target_modules[query, value], ) model get_peft_model(model, lora_config) training_args TrainingArguments( output_dir./intent_router, evaluation_strategyepoch, save_strategyepoch, learning_rate3e-5, per_device_train_batch_size32, per_device_eval_batch_size64, num_train_epochs5, weight_decay0.01, load_best_model_at_endTrue, logging_steps50, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, ) trainer.train()LoRA的target_modules选择“query”和“value”是我在分类任务里的默认配置大多数情况下训出来的效果都够用。如果想在准确率上再抠一点可以加入“key”和“dense”层但训练时间会明显增加。训练过程不会太长。5000条样本、5个epoch、单张12GB显卡大概几十分钟到一小时左右跑完。如果训练时间短到离谱比如一分钟就结束了先检查数据集是不是为空或者tokenizer有没有把输入全截成空序列。4.3 效果评估与阈值调整训练完之后不要只看整体准确率分类任务最需要关注的是每类别的精确率和召回率。我用scikit-learn直接算macro F1和分类报告同时输出混淆矩阵哪个类容易被哪类带偏一目了然。from sklearn.metrics import classification_report, confusion_matrix preds trainer.predict(eval_dataset) y_pred preds.predictions.argmax(-1) y_true preds.label_ids print(classification_report(y_true, y_pred, target_nameslist(label2id.keys()))) print(confusion_matrix(y_true, y_pred))这里有一个关键技巧不要直接用argmax决定最终路由。模型输出的是每个类别的概率分布实际部署时我会额外设置一个置信度阈值比如0.7。如果最大概率低于阈值说明模型自己也没把握这时候宁可让请求走默认分支或者要求用户澄清也不要强行分类。这个阈值要根据验证集上的准确率-覆盖度曲线来定而不是拍脑袋。我实测过一组对比阈值从0.5提到0.7之后整体准确率上升了一到两个百分点虽然有一小部分请求被归入“不确定”分支但这类请求本来也是边缘情况强分类反而容易翻车。5. 把微调模型缝进RAG主流程5.1 路由模块的整体调用逻辑模型训练好之后最终要嵌入到生产环境。前置路由的调用逻辑不算复杂核心就三步接收用户query意图分类模型预测概率分布根据分类结果和置信度走不同分支。生产实现里还要考虑两件事。第一路由模块要有超时保护和失败兜底。如果模型推理失败或者响应超时不能直接报错给用户要降级到默认检索流程。第二路由的结果要记录日志。日志里至少包含原始query、预测类别、各类别概率分数、实际走的分支、响应耗时这些日志是后续样本回流和模型迭代的主要来源。5.2 路由分流的代码示例下面这段代码展示了路由模块最简单的实现方式def route_query(query: str): inputs tokenizer(query, return_tensorspt, truncationTrue, max_length64) outputs model(**inputs) probs torch.nn.functional.softmax(outputs.logits, dim-1).squeeze() conf, pred_id torch.max(probs, dim-1) intent id2label[pred_id.item()] conf_value conf.item() if conf_value 0.7: return route_fallback(query) if intent 知识问答: return route_knowledge_qa(query) elif intent 闲聊寒暄: return route_chitchat(query) elif intent 文本处理: return route_text_processing(query) elif intent 翻译润色: return route_translation(query) elif intent 总结摘要: return route_summary(query) elif intent 任务指令: return route_task_execution(query) else: return route_rejection(query)代码本身没什么技术含量但每个分支对应的后续处理逻辑才是重点。比如route_knowledge_qa里面才真正去调向量检索和RAG生成route_chitchat可能只返回一个预设话术route_rejection会根据拒答话术模板返回提示。我见过一个项目把意图分类模型训练得很好结果后面每个分支里的处理逻辑还是走同一条RAG链路等于白分类了。路由的价值必须体现在分支差异化上。5.3 多级路由与降级策略单层路由能解决大部分问题但复杂业务场景下我更推荐做成两级路由。第一级做粗粒度分流比如区分“知识问答”和“非知识问答”然后在“知识问答”分支内部再做第二级细分比如按业务域分成“产品库”“法规库”“流程库”每个子库都有独立的检索配置和知识集。两级路由的好处是让每一层的分类任务都足够简单需要的样本量也更可控。直接做一个细粒度分类器类别太多反而容易混淆。比如某业务系统需要区分几十个知识域强行让一个模型学几十个类别训练数据得准备好几万条训练难度和线上稳定性都不理想。改成两级之后第一级5类第二级每类下面最多10个子类数据需求大幅降低每个子分类器可以单独迭代。降级策略也有讲究。通常设置一个全局fallback分支当置信度低于阈值时走默认知识检索路由同时返回一句澄清文案比如“我理解您想了解产品相关问题以下是常见解答”。另外还可以在路由层引入用户反馈回路当分类结果被用户投诉或回答被点“踩”之后把该条query标记为可疑样本进入后续人工复核。5.4 线下到线上的推理优化意图分类模型体量小推理开销不大但线上部署时还是有几个优化点值得做。模型转成ONNX格式后推理速度能提升不少尤其是在CPU环境。量化方面int8量化对分类任务的精度影响很小我用下来基本感觉不到准确率下降但推理延迟可以降低一半以上。量化后的模型加载到内存单条推理在CPU上实测可以稳定保持在10毫秒以内这个量级放在RAG入口几乎感知不到。需要提前注意的一点是tokenizer和模型的版本要统一保存。很多线上事故就出在训练时用了一份tokenizer、部署时又换了一份导致同样的文本切出来的token完全不同。我在发布流程里会强制检查tokenizer的vocab size和特殊token是否一致不一致直接阻断发版。6. 踩坑记录与排查速查表6.1 高频问题与原因分析先讲几个我实际操作中踩过的坑每个都很典型。第一个坑是类别标签设计不互斥。我在某个项目里同时设置了“咨询”和“售后”结果大量请求两个类都沾边标注员硬着头皮标模型学出来的边界一团浆糊。后来把两个类合并成“业务咨询”准确率立刻上来了。第二个坑是训练数据模板化太严重。当时为了快速凑样本用模板生成了一大批闲聊数据结果线上用户用方言问“吃了没得”模型完全认不出来因为模板里全是普通话规范表达。后来我在模板基础上了加了大量真实口语和近义替换情况才好转。第三个坑是长文本截断策略不对。用户粘贴了一份合同过来问“帮我看看第几条有问题”硬截掉后面内容后模型判断成普通文本处理直接走了错误分支。后来遇到超长文本我会先对文本做分段处理提取跟指令相关的核心片段再做意图判断。6.2 排查速查表整理成一张表方便自查问题现象排查手段常见原因建议方案某类准确率特别低查看该类样本量和召回率样本量不足或边界模糊补充该类数据增加难负样本整体准确率高但线上一堆错抽样看线上失败case线上分布跟训练分布不一致用线上日志回流样本定期迭代模型总把新话术判成“其他”查看低置信度样本训练集覆盖不足补充扩展模板和真实语料推理延迟突然飙升查看路由层日志耗时模型未量化、batch处理不当转ONNX、做量化、启用batch推理训练Loss下降但验证Loss上升看验证曲线过拟合降低epoch、增加数据、加正则分类结果来回跳检查随机种子和模型状态误用了生成式模型或没有固定种子使用确定性的分类模型固定seed6.3 我的几个实操建议最后分享几个只有实际做过才会注意到的细节。第一个建议是永远保留原始query。训练环节要保留线上日志要保留路由分发之后下游模块拿到的也应该是原始输入而不是经过清洗或者改写后的版本。很多意图信息藏在细节里原始query是样本迭代最重要的素材。第二个建议是把置信度分数透出到监控面板。光看路由判了什么类别不够还要看它对每个类别的把握有多大。如果连续出现大比例低置信度请求说明线上出现了训练时没有覆盖的新意图需要及时介入处理。第三个建议是灰度切换路由。不要一上来就把100%流量切到新模型。我会先切10%对比一下路由前后相同请求的回答满意度确认没有问题再逐步扩大。这里说的满意度可以是点击反馈、点赞点踩也可以是第二轮追问率。另外还有一个边界情况要留意同一轮对话里的意图可能发生变化。用户先问知识问题紧接着追问一句“那有没有别的办法”这句单独怎么看都像闲聊但在对话上下文里其实是知识问题的延续。处理办法是把前几轮的历史意图也作为特征输入模型或者在前置路由前加一个轻量的上下文状态管理模块。这部分属于更复杂的对话路由设计初期可以先用简化方案携带最近一轮的意图作为先验新问题单独预测再用规则合并。我自己做这类前置路由时最深的体会是不要指望模型一步到位分类模型的价值是在迭代中积累的。第一批样本难免粗糙但只要线上日志回流机制跑起来每个星期换一批新数据重训一次分类效果会越用越准。刚开始可能只有知识问答这一类比较可靠跑一个月后闲聊、翻译、拒答这些边缘类别也能慢慢支撑起来。RAG的入口就像房间的门厅你愿意花心思把门厅收拾利索进到屋里之后每一步都会顺畅很多。