
做RAG应用最容易被低估的问题往往不是检索器也不是生成模型而是入口处的判断——用户的问题到底该走哪条路。我在这类项目里反复被问到为什么不直接在Prompt里让大模型判断一下意图为什么非要单独训一个模型这篇文章就围绕我在实际项目里做的一个“前置路由”组件展开——用微调后的意图分类模型接管了客服问答场景里的入口判断最终把整体答非所问率降了近一半。整个过程从数据准备写到模型评估和工程接入适合对RAG已经有基础、但还没认真做过意图路由的读者也适合准备把路由作为独立模块接入现有系统的团队参考。1. 为什么要在RAG前面加一道“前置路由”不这么做会吃哪些亏1.1 没有路由的RAG会变成“有问必答”的复读机先还原一个真实场景。客服知识库接入了RAG之后用户问“你们发货用哪家快递”系统检索知识库、拼上下文、生成回答效果不错。但紧接着用户来一句“今天心情不好”系统同样走了一次检索——知识库里当然没有关于心情的文档于是它从最相近的“售后安抚话术”里捞了一段生成了“非常理解您的心情请问您遇到了什么问题”之类的回应。看着没毛病实际等于把闲聊问题硬塞进了业务管道消耗了两次大模型调用还污染了会话状态。更麻烦的是那些“需要查订单但用户没说订单号”的情况。没有路由时系统把问题直接送进RAG检索器满库找“订单号”找到一堆无关条款生成模型再把这些内容揉进回答里。用户听了一堆“请您提供订单号以便核实”的变体。你会发现检索越努力答案越离谱——因为问题本身就不适合走RAG这条路。没有前置路由还有一个隐性成本你很难统计用户到底在问什么。所有请求都混在同一个管道里你不知道有多少比例是闲聊、多少比例是售后、多少比例是报价咨询。等到想优化知识库时翻日志全是一锅粥。加了路由之后每一类请求有了明确归属后面无论是优化检索、调整话术还是做运营分析都有数据支撑。1.2 三种常见做法规则、Prompt判断、微调模型既然路由是必须的接下来就是怎么实现的问题。我最先看到的是三种方案各有各的适用场景。第一种是纯规则匹配用关键词列表把问题分到几个预设桶里。这个方案在小规模演示场景很够用“运费”开头的进物流意图“退款”开头进售后意图。问题是自然语言变体太多同一个“怎么退货”可以有几十种说法。规则表会越写越长最后变成一个没人敢动的怪物。更麻烦的是规则之间会冲突用户一句“退款怎么还没到账”同时命中了“退款”和“到账”两类规则你都不知道该让哪条规则赢。第二种是让RAG链路里的大模型直接判断意图在Prompt里加一句“请先判断用户意图再决定是否检索”。这个方案实现起来几乎零成本但有两个绕不开的问题。一是延迟和成本多一次大模型调用而且这调用还发生在每次用户请求的最前面等于把所有流量都先过了一遍大模型。二是稳定性大模型在Prompt里判断意图经常受到上下文干扰用户问“你这个东西怎么用”它有时候判断成“产品咨询”有时候判断成“售后问题”同一句话在不同会话里的路由结果可能不一样。第三种就是这篇文章真正要讲的微调一个小模型让它专门做意图分类。它的模型体积比RAG链路里的大模型小一两个数量级推理一次成本可以忽略延迟在几十毫秒这个量级。而且它是针对你自己的业务数据微调出来的不会像通用大模型那样“见多识广”反而在细分类目上摇摆。这个方案在工程上多了一步训练和部署但换来的收益是稳定、便宜、可控。1.3 前置路由到底是什么它决定的不只是“要不要检索”把“前置路由”叫路由容易让人觉得它就是个开关。实际操作下来我建议把它理解成一个独立的意图分类服务它的输出不是简单的是/否而是一个结构化的路由结果。举个例子。一个客服问答RAG系统前置路由的输出可能是这样{ intent: after_sales, confidence: 0.92, action: retrieve, query_type: order_status, slots: { need_order_id: true } }这个结果里至少有四层信息。intent决定了后续走哪条业务链路action告诉RAG这段请求要不要触发检索query_type可以用来选择检索策略比如查订单知识就走订单库查产品信息就走产品手册库slots则给下游提供了一个提示比如“这个请求缺订单号”那么生成答案之前可以先追问一句而不是盲目检索。所以我更愿意把它描述为一个“决策前置”的组件分类只是它的第一步它真正影响的是RAG系统对每个请求的处理策略。这个认知非常重要因为它决定了你后面做数据、做训练、做评估时是以“分类准确率”为标准还是以“整条RAG链路的效果”为标准。我的答案是两者都要看但端到端效果更重要。2. 手搓训练数据意图分类的成败有八成在这里2.1 意图体系该怎么设计不是越细越好3到6个是黄金区间我见过很多团队在第一步就翻车意图分类做得像一棵决策树一级意图十来个二级意图几十个每个意图下面还分场景。训练一个分类模型之前先问自己一句话下游RAG系统真的需要区分这么多分支吗如果两个意图在后续处理策略上完全一样只是说法不同那它们就应该合并。以客服问答为例一个合理的意图体系大概是这样的意图含义下游动作product_info产品参数、功能、使用方法咨询检索产品知识库返回结构化产品说明after_sales退换货、维修、退款进度检索售后政策库同时提示用户补充订单信息logistics物流、发货、配送时间检索物流知识库按单号状态查询order_status订单状态、支付状态查询触发订单系统查询不依赖向量检索chitchat闲聊、情绪表达、无关话题不触发RAG走固定话术或转人工out_of_scope业务范围之外的请求比如投诉、合作、招聘转人工或返回兜底提示这套体系只有6个意图但已经覆盖了绝大多数客服场景。分类粒度到了这个程度下游每个分支都有明确的处理策略再细的意图反而是给标注和训练添乱。设计意图体系的核心原则是意图的粒度等于下游处理策略的粒度下游怎么分叉意图就怎么分类多一分都浪费。2.2 正样本、难负样本和OOS样本训练数据的三个零件意图分类的训练数据不是随便收集一堆问题就完事它由三部分构成缺一块模型都会“瘸腿”。第一部分是正样本每个意图各几百条真实提问。这部分主要来源于线上日志我强烈建议优先挖历史客服对话记录哪怕只有几百条原始记录也比网上找的公开数据强十倍。真实问题长什么样用户不会说“我要查询产品信息”而是说“你们这个能不能支持无线充电”或者“带不带的动4K显示器”。这种自然表达里的口语、省略、错别字是公开数据集训练不出来的。第二部分是难负样本。所谓难负样本是那种“看起来像A意图但其实是B意图”的句子。比如“这个退了之后还能买吗”——前半句像售后后半句是新品咨询实际意图是product_info。再比如“运费多少钱”这可能是logistics也可能是product_info如果用户在问某件商品的运费政策。难负样本的价值在于逼着模型去抓真正的判别特征而不是靠几个关键词猜。第三部分是OOS样本即out-of-scope也就是那些不属于任何一个预设意图的输入。“你们公司加班多吗”“怎么投简历”“你这个客服是不是机器人”这类问题多到超乎想象。OOS样本的目的不是教会模型认识业务而是教会模型“这里没有你要的东西别硬答”。模型如果没见过OOS样本上线后会把一切输入强行归到6个意图里准确率反而很难看。这三类样本的比例我推荐正样本:难负样本:OOS样本大致按6:3:3来配。不要迷信绝对数量更关键的是每一类意图的正样本分布要尽量均匀别让某个意图占了全部数据的一半。2.3 数据扩写与清洗用LLM生成变体但必须人工抽检真实数据永远不够尤其是冷启动阶段。这时候用一个大模型来做数据扩写是性价比很高的做法——注意这里说的是扩写不是凭空生成。我会从已有的真实样本里挑一批作为种子让大模型生成“不同的人会怎么表达同一个意思”的变体比如把“你们发什么快递”扩写成“常用哪家物流”“发货走什么渠道”“一般用什么快递送”把“怎么申请退款”扩写成“退货流程是什么”“我想退掉怎么操作”“申请售后的入口在哪里”扩写提示词里我会明确要求保持意图不变但改变说法、替换口语词、增减语气词。生成的候选数据要人工抽检抽检比例至少20%我遇到过扩写模型把“怎么退款”扩写成了“退款到不了账”这就从售后意图悄悄变成了投诉属典型的标签污染。清洗环节有几个容易忽略的坑。第一个是去重尤其用LLM扩写出来的数据表面句子不同但实际结构高度雷同模型学到的是模板而不是语义得用文本相似度做一遍粗筛。第二个是长度过滤超过100个字的“问题”在真实场景里大多是用户在描述背景直接截断或丢弃都可以不要让模型在长尾句子上耗费学习能力。第三个是标签核对建议至少两个人各自抽一遍对不一致的样本重新讨论这个环节虽然费时但能避免整个训练集带上系统性偏差。3. 基座模型与微调方案选型为什么我不选大参数量模型3.1 意图分类是判别任务不是生成任务很多第一次做这个事的朋友上来就问要不要用最新的大模型微调我的答案通常是如果你只需要类别标签就用一个小而专的判别式模型或者一个小参数量的生成式模型加LoRA不要用几十B的大模型。原因是意图分类本质上是一个判别任务给定一句话输出它属于哪个类别。判别任务不需要背下全世界的知识它需要的是抓住这句话里的“区分性特征”。一个几十B的大模型在微调时当然能做得更好但它的优势主要体现在语义理解很复杂、类别边界很模糊的场景。如果你的意图分类只需要区分七八个业务类目一个轻量模型就完全够用而且推理快、部署省、成本低。我试过的方案里有两条路都走得通。一条是传统的BERT类模型做分类头微调另一条是用小的生成式模型加LoRA做序列分类。前者训练快、推理极快模式非常稳定适合意图类别固定、结构简单的场景后者对复杂语义的容忍度更高即使输入里混着繁体、英文、错别字也不容易崩适合意图边界模糊、输入噪声大的场景。两条路各有拥趸我自己的经验是先从BERT类模型起步跑通之后再考虑要不要换生成式模型千万别一上来就上重武器。3.2 轻量模型选哪个给一个对比参考这一类模型的选择其实没有标准答案不同团队环境和中文语料的适配度也不同。我列一个横向对比给没有头绪的读者一个起点方案训练成本推理延迟中文鲁棒性适用场景BERT类中文底座约1亿参数低单卡几十分钟低10ms级中对口语变体偏弱类别稳定、输入规范轻量生成式底座约1-3B参数LoRA中单卡数小时中低几十ms级高对口语/错别字容忍度高类别模糊、输入噪声大大参数底座10B以上LoRA高多卡或大显存高百ms级高但成本收益低极少使用除非类别极端复杂关键是要理解参数量的提升带来的收益不是线性的你先要明确自己的瓶颈是语义理解还是标注质量。我做过一个对比实验同一套训练数据BERT类模型和轻量生成式模型的准确率都在90%左右差距不超过2个点。那2个点的差距到底值不值额外的推理延迟我的结论是不值。在意图分类这个任务里大部分错误来自标注噪声和OOS样本覆盖不足而不是模型容量不够。3.3 LoRA不是唯一方案但它是最省事的路径微调一个轻量模型实际上有三种路子全量微调、LoRA、蒸馏。全量微调在数据量小的情况下很容易过拟合而且每次调整都要保存一份完整权重工程上很笨重。蒸馏则需要一个更强大的教师模型做先导成本前置不适合快速迭代。LoRA是这批方案里最平衡的训练时只更新低秩适配矩阵显存占用小训练速度快推理时可以把LoRA权重合并进底座模型不影响线上延迟换业务场景的时候只需要训练不同的LoRA分支底座模型可以复用。有人担心LoRA会损失效果我实测下来的结论是在数据量不超过几万条的意图分类场景里LoRA和全量微调的差距非常小甚至没有明显差距。它最让我舒服的一点是模型文件管理一次业务微调产出一个几十MB的适配器文件换业务就等于换文件回滚也只需要切回旧的适配器不用重训整个模型。所以后面的实操部分我会默认用LoRA这个方案来讲如果你确实有资源做全量微调思路完全一样只是训练脚本里少一步冻结参数而已。4. 从数据到权重LoRA微调完整实操过程4.1 准备环境一本账算清楚需要什么微调一套意图分类模型需要的资源比想象中低很多。我用BERT类模型做例子一张消费级显卡就能完成训练如果用轻量生成式模型加LoRA一张24G显存级别的显卡也足够应付十几万条数据的训练。下面是要装的核心依赖transformers4.30.0 datasets2.12.0 peft0.5.0 accelerate0.22.0 torch2.0.0这里要说明一下peft负责LoRA注入datasets负责数据加载和预处理accelerate配合单机多卡或混合精度训练。整个环境用一个Python虚拟环境装就行不要让这些依赖和线上服务混在同一套环境里线上环境保持干净训练环境随便折腾。4.2 数据格式与转换从JSONL到模型输入训练数据标准格式是一份JSONL文件每一行一个样本字段就两个text和label。label可以直接用意图字符串也可以用ID我建议在原始文件里保留字符串可读性好后面做人工检查方便。{text: 你们这个能支持无线充电吗, label: product_info} {text: 退款什么时候才能到账, label: after_sales} {text: 发的什么快递, label: logistics} {text: 你们招人吗, label: out_of_scope}训练之前要把这份JSONL读进内存转成模型能处理的输入格式。BERT类模型的关键步骤是tokenizer分词、padding、截断。截断长度这里有个小讲究意图分类的问题句子普遍不超过64个字我直接设成64太长的问题本来就少见强行保留反而会让模型学到某些无关的长尾特征。生成式模型的准备方式略有不同它的输入格式是你是一个意图分类助手请判断下面问题的意图{text}标签是意图名称这属于IE式构造但原理一样都是把文本映射到标签空间。4.3 超参数与训练脚本抄作业级别的参数组合训练脚本的核心逻辑分四步加载底座模型、注入LoRA配置、定义训练参数、启动训练。我直接给一段核心代码按你的底座模型调整路径和类别数就能跑from transformers import AutoTokenizer, AutoModelForSequenceClassification from peft import LoraConfig, get_peft_model from transformers import TrainingArguments, Trainer from datasets import load_dataset model_path your-base-model-path num_labels 6 tokenizer AutoTokenizer.from_pretrained(model_path) model AutoModelForSequenceClassification.from_pretrained( model_path, num_labelsnum_labels ) lora_config LoraConfig( r8, lora_alpha16, target_modules[query, value], lora_dropout0.05, biasnone, task_typeSEQ_CLS ) model get_peft_model(model, lora_config) training_args TrainingArguments( output_dir./intent_router, per_device_train_batch_size32, learning_rate2e-4, num_train_epochs5, logging_steps50, save_steps200, evaluation_strategyepoch, fp16True, remove_unused_columnsFalse, ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, tokenizertokenizer, ) trainer.train() model.save_pretrained(./intent_router_lora) tokenizer.save_pretrained(./intent_router_lora)几个超参数我展开说一下。learning_rate2e-4在LoRA微调里是个安全的起点比全量微调常用的5e-5多了近一个数量级原因在于LoRA训练的参数很少学习率太小收敛太慢。r8是常见的低秩维度lora_alpha16是缩放因子两者保持2:1的比例关系通常表现稳定。num_train_epochs5针对几千条的意图分类数据基本够用如果你的数据量超过五万条可以适当减到3轮。训练过程中主要盯两个东西训练loss曲线的下降斜率以及eval集合的准确率是否不再上升。如果loss在首个epoch之后就急剧下降但eval准确率反而退化说明学习率偏大可以回退到1e-4再试。如果loss下降平缓但eval准确率不错那就按当前配置跑完不必强求loss追求到极小——注意意图分类的loss降到非常低往往意味着模型开始死记训练集了。4.4 训练完成之后合并权重与快速验证训练结束后得到一个LoRA适配器目录里面通常是adapter_config.json和adapter_model.bin。上线之前有两个动作要做。第一个动作是把LoRA权重合并回底座模型。不合并也能用但需要在加载时额外显式调用PeftModel.from_pretrained多一层依赖而且线上框架不一定支持。合并的好处是产出一个标准的、自包含的模型目录部署时当成普通模型加载就行。合并代码就一行from peft import PeftModel model PeftModel.from_pretrained(base_model, ./intent_router_lora) merged_model model.merge_and_unload() merged_model.save_pretrained(./intent_router_merged)第二个动作是拿一批线上真实问题做冒烟测试这个测试集必须是训练时从未见过的。我习惯准备20条当前线上日志里出现频率最高的问法加上10条明显不该命中任何意图的闲聊看模型的输出分布是否合理。这一步能发现的最经典问题是所有样本都涌向同一个意图那就是数据分布出了大问题回去看训练集八成是那个意图的样本量过多或者OOS样本过少需要重新平衡数据再做一轮。5. 不是只看准确率意图分类模型的线上评估方法5.1 离线评估分意图看指标别被总准确率骗了很多团队评估意图分类模型只看一个“总准确率”我强烈建议把这个指标拆开看。总准确率很容易被“高频意图”带节奏假设6个意图里product_info占了50%的样本模型把其他意图全分错只要有product_info分对总准确率也有50%看了等于没看。正确的姿势是分意图计算精确率Precision、召回率Recall和F1用macro平均而不是micro平均。我放一个小小的评估样例意图PrecisionRecallF1product_info0.940.910.92after_sales0.890.850.87logistics0.900.930.91order_status0.870.820.84chitchat0.780.800.79out_of_scope0.750.690.72看这个表格总准确率可能是85%看起来很光鲜但out_of_scope的召回率只有69%意味着约三分之一的闲聊和无关问题没有被拦下会被错误地送进RAG。这类问题往往比某个业务意图分错影响更大。所以我每次评估的时候除了算平均指标还会单独给out_of_scope设一个最低阈值召回率低于80%就不允许上线。除此之外要专门检查“混淆矩阵”里对角线之外的密集区。如果product_info经常被分进after_sales说明这两个意图的边界本身定义不清晰或者标注人员自己都分不清那就不是模型的问题是意图体系或者标注规范的问题。这种情况要回到数据层面去修正不要指望模型自己顿悟。5.2 端到端评估路由模型最终的KPI要看整条RAG链路离线指标只是中间关卡。模型放在RAG前面最终要看的是尼玛整条链路的业务效果。我在项目里主要用三个指标评估路由改造前后的差异答非所问率、首轮解决率、检索调用量。答非所问率是核心指标。统计方式是给一批用户问题打标注用规则判断回答里是否包含了用户要的信息。RAG链路加了前置路由后明显受益的场景有两个一是闲聊类请求不再被硬塞进检索管道系统返回固定话术答非所问率直接降二是订单状态类请求被路由到订单查询分支不再走容易在知识库里“近义替换”的向量检索答非所问率也降。首轮解决率关注的是用户在第一轮交互里有没有拿到想要的答案。路由对它的影响比较微妙路由决策正确把不同问题分到对应知识域首轮解决率会上升路由决策错误把用户问题分到一个不相干的分支首轮解决率会下降。这个指标是路由效果的真实投影因为它是纯粹的端到端结果。检索调用量这个指标可能被忽视。没有路由前每个请求都触发检索有路由之后只有真的需要检索的请求才会去查向量库。这一个指标直接转换成延迟和成本如果chitchat和out_of_scope类请求的占比超过20%整个链路的检索调用量会下降20%以上省下来的就是真金白银。5.3 一个始终要提醒自己的原则路由的保守偏差比激进偏差更安全意图分类模型不可避免地会出错关键是你希望它往哪个方向错。我在实验里对比过两种错误模式激进路由模型倾向于把模糊问题分为某个业务意图结果是闲聊被当成业务问题处理RAG强行回答答非所问率高。保守路由模型倾向于把模糊问题分为chitchat或out_of_scope结果是部分真实业务问题被转人工或兜底回答用户多一次点击但不会得到完全错误的信息。从业务体验看保守路由的代价远小于激进路由。一个真实业务问题被拦到人工用户会损失一点便利但客服接手后问题能解决一个闲聊问题被塞进RAG用户收获的是一段莫名其妙的回答信任损失更严重。要让模型偏向保守两个常用手段一是在数据层面把chitchat和out_of_scope类样本的多样性做足让模型见过更多“边界地带”的表达二是在推理层面给每个意图设置信度阈值低于阈值的统一落到“低置信度”分支不走RAG走通用话术或转人工。后者实施起来更简单可以快速止损。6. 把模型接进RAG服务工程改造与降级策略6.1 部署方式单独开一个微服务比什么都省心意图分类模型体积小推理快部署方式有很多种。简单做法是把它和RAG主服务放在同一个进程里加载模型进内存函数内直接调用。问题在于耦合模型加载占用显存RAG主服务可能本身就部署在大模型集群里一旦模型更新或出问题整个RAG服务的发布和回滚都被牵制。我更偏向的做法是单独开一个“意图路由微服务”模型只在这个服务里加载对外暴露一个HTTP接口RAG主服务通过内部网络调用它。这样有几个好处路由服务可以独立扩缩容请求量大了单独给它加副本模型更新不影响RAG主服务发布路由服务挂了可以通过降级开关跳过不会拖垮整个链路。这套架构牺牲了一点点服务间调用的延迟但在稳定性和运维便捷性上赢回来太多。接口设计上我建议不要只返回一个意图标签而是像前面提到的返回一个结构化的路由决策。接口大致长这样POST /v1/route Body: {text: 你们发什么快递} Response: {intent: logistics, confidence: 0.91, action: retrieve}字段不需要太多但confidence绝对不能省。这个值在降级策略和运营分析里都要用它是“模型知道自己在说什么”的程度比单纯一个标签信息量大得多。6.2 超时、缓存与降级路由服务挂了不能让RAG跟着挂路由服务虽然是轻量模型但任何服务都可能出问题。我见过几个系统在这里踩坑路由调用超时设得特别短5毫秒没回就直接报错RAG主服务没做任何降级硬是把这个错误暴露给了用户。工程上一定要记住一个原则路由服务是RAG链路的增强组件不是RAG链路的依赖项。降级策略可以这样设计当路由请求超时或返回非200状态码RAG主服务直接走默认策略——按普通RAG流程处理。因为绝大多数业务请求本来就需要检索默认走RAG的损失很小只是少了分类优化而已。这比“用户请求直接报错”强十倍也符合前面说的保守原则。缓存也是提升路由效率的关键手段。真实用户问题高度重复同一个“怎么退款”能在一个月里出现几百次。在路由服务前面加一层按文本hash的本地缓存命中率通常在20%以上。对缓存命中率影响最大的是计算hash前统一对文本做归一化去掉首尾空格、全角转半角、繁体转简体这些简单处理能让缓存命中率再高一截。6.3 监控与数据回流路由上线只是开始路由服务上线之后要建立一套针对它的监控面板。核心指标有四个路由请求量、各意图占比、平均延迟、低置信度比例。其中各意图占比特别值得关注它能直接反映用户需求的变化趋势。如果out_of_scope占比在某天突然上升很可能是有异常流量打进来了如果某个业务意图占比骤降可能是知识库或产品层面有调整也可能是模型出现了偏差。更重要的数据回流机制每天把路由服务判错的样本抽出来人工复核后进入下一轮训练集。这个闭环是整个系统持续变好的核心。我在项目里设了一个每天自动任务从线上日志中随机抽取三类样本——低置信度样本、被人工客服改判的样本、以及RAG链路A/B测试反馈为差评的样本——定时汇入训练数据仓库。两周做一次增量微调每次只需要跑几个epoch路由准确率就能肉眼可见地稳步提升。7. 踩过的坑与我的建议7.1 公开数据训练的模型上线后水土不服我最早一版训练数据主要来自开源意图数据集当时图省事觉得大规模、多领域模型泛化能力肯定强。结果上线不到一周就被用户教育了公开数据集里“怎么退货”是标准书面语真实用户写的是“退不了货怎么办”“七天无理由咋退”模型对这类口语变体的识别率低得吓人。后来我花了一个下午手动从客服历史会话里挖了三百条真实问法加进训练集效果立刻回升。这个教训让我后来定了一个规矩训练数据必须以真实线上日志为底座公开数据集只能做冷启动的补充。原因是公开数据集覆盖的是“通用表达”而你的业务有自己特有的词汇、缩写和常见错别字这些是公开数据永远覆盖不到的。7.2 OOS样本不够闲聊全被塞进业务意图有一版训练数据里out_of_scope样本只占总量的5%直觉是“闲聊不多少放一点没关系”。结果上线后chitchat和out_of_scope的召回率双双崩到60%以下。原因很好理解模型没见过足够多的“其他东西”它会倾向于把所有输入都强行塞进它见过的几个业务桶里。修复方式不是简单加几条闲聊样本而是把OOS样本的多样性做足。我从公开的多轮闲聊数据集里抽了一批又从真实日志里抽了一批“看起来像业务但实际不是”的样本把这部分数据的比例拉到总体的25%左右模型对无关输入的拒识能力才明显改善。OOS样本是路由模型最容易忽略、但对线上体验影响最大的部分分配训练数据时请务必给足占比。7.3 对prompt模板和截断长度过于敏感换成生成式模型做意图分类后我也踩过一个坑。同一个样本我把prompt模板从“判断下面句子的意图”改成“请对下面这句话进行分类”模型输出的类别居然完全变了。这让我意识到小模型的输出对prompt模板的敏感度比大模型高得多模板里的标点、语气、甚至空格都可能影响结果。对策是把prompt模板当作超参数的一部分来对待先固定一版模板做完一轮数据验证再考虑要不要微调模板措辞不要频繁改动。截断长度同理训练时用128字截断上线推理时也一定要用128字我曾经因为训练和推理截断长度不一致导致分类结果在线上出现诡异的偏移排查了好久才意识到是这个参数造成的。7.4 意图体系不是一锤子买卖最后一个有点反直觉的经验意图体系本身也要迭代。我最初的意图清单是产品、售后、物流、其他四个上线后发现“查订单”类请求量很大而且这类请求完全不需要检索知识库才拆出了order_status这个独立意图。后来“投诉”类请求比重上升又把它从out_of_scope里单独剥离了出来。但迭代要克制每次新增意图之前都先问这个意图在下游有没有对应策略如果没有宁可继续让它在其他意图里混着。意图体系是一个工程组件不是学术分类学它服务于系统对用户请求的差异化处理。这套思路在后续几次微调迭代里被反复验证也让我在项目复盘时确认了一件事前置路由最难的部分从来不是训练脚本而是持续的数据维护和对业务意图边界的理解。