去年我们做客服消息意图识别的时候,最开始的想法特别简单:大模型那么强,直接把用户消息丢给LLM分类不就完了吗?结果一上线就发现不是那么回事。线上每天的会话消息量在十万级,每条都走LLM,成本直接爆表,而且线程一多延迟就开始抖动,客服那边又催得急。后来我把方案改成了规则加小模型加LLM的三层混合架构,单条消息的平均处理成本降了一个数量级,准确率和稳定性反而提上去了。这篇文章就把这套架构的思路、每一层的具体实现,以及层与层之间的调度细节完整拆开讲一遍。
这套架构适合谁?如果你正在做电商客服机器人、工单自动分类、或者客服会话路由,又不想让每一句话都烧大模型的token,那这篇文章应该能给你一套可以直接落地的参考。我尽量把代码示例、阈值设置、踩坑记录都写出来,是那种可以照着抄作业的级别。
1. 为什么客服意图识别不适合“无脑上大模型”
1.1 电商客服场景的三个硬约束
很多人一提意图识别就想到大模型,是因为Demo阶段效果确实惊艳。但电商客服的真实场景有三个约束,是Demo里看不出来的。
第一是请求量大。一个中大型店铺每天的客服消息量轻松过十万条,双十一这种大促能到百万级别。每一轮用户消息都要判断意图,这就是实打实的推理请求量。
第二是实时性要求高。用户发一句“我东西怎么还没到”,系统需要在几百毫秒内给出意图判断,好让机器人立即响应。超过两三秒,用户就会开始狂发问号。LLM的推理延迟在非流式场景下通常要一秒以上,量大时还不稳定。
第三是成本敏感。客服消息不像搜索广告那种高价值请求,单条消息对应的商业价值就那么点,花几厘钱去处理还可以接受,但如果每条都要花几分钱甚至更贵,业务账就完全算不过来了。
这三条约束放到一起,决定了我们不能拿LLM当默认选项去处理所有流量。它应该是“最后出场的专家”,而不是“前台接待”。
1.2 三层各管一块,核心目标不是准确率而是“性价比”
这套三层混合架构的出发点很简单:让便宜的模块处理大部分常见问题,只让贵重的模块处理真正复杂的问题。
第一层是规则层,用正则和词典把那些表达高度固定的请求直接拦下来。比如“查一下快递到哪了”“转人工”“我要投诉”,这些话说来说去就那么几种写法,没必要让任何模型出场。
第二层是小模型层,处理的是规则层没有覆盖的、有一定表达变化但整体不复杂的请求。我们用的是TextCNN,参数量很小,在CPU上单条推理只要几毫秒,几乎不占成本。
第三层才是LLM层,专门处理小模型拿不准的、语义绕的、有多重意图的请求。用户说得再莫名其妙,LLM凭借它的常识理解能力,能给出一个基本靠谱的判断。
准确率当然重要,但这套架构真正优化的指标是“单位成本下的准确率”。我的目标是:LLM处理的流量占比控制在5%以内,但整条链路的意图识别准确率不低于95%。这个目标靠任何单一层都做不到,合在一起就能做到。
1.3 我踩过的“纯大模型方案”的坑
说一个我们自己的反面案例。最早我们用纯LLM方案的时候,一开始测试集准确率确实很好看,96%以上。但线上跑了一周就发现问题了。
首先是账单。纯LLM方案每天十万级请求,日成本直接冲到几千块,而且这还是在模型输出很短的情况下。其次是延迟波动。早高峰时段,模型服务的排队时间拉到三四秒,客服机器人出现大量超时未响应,用户满意度掉得厉害。最离谱的是稳定性:同一个意思换几种表达,比如“东西什么时候到”和“货发出来了吗”,有时判断成查物流,有时判断成催发货,行为不可控。
后来我们才意识到,意图识别这种场景最怕的不是某一条判错了,而是“同类型请求处理逻辑不一致”。规则和小模型的输出是完全确定的,同样的输入永远给同样的输出,这本身就是一种价值。
2. 第一层规则层:正则和词典先把六成流量拦住
2.1 先定意图标签,再写规则词典
动手写规则之前,第一件事是把意图标签体系定下来。这一步必须跟业务方对齐,因为后续所有层、所有模块都围绕这套标签工作。
我们目前的电商客服场景常用标签如下:
| 意图标签 | 含义 | 后续动作示例 |
|---|---|---|
| track_order | 查物流/催发货 | 查订单物流接口 |
| modify_address | 修改收货地址 | 触发地址修改流程 |
| refund | 申请退款 | 进入退款引导 |
| after_sale | 退换货/维修 | 进入售后流程 |
| invoice | 发票问题 | 推送开票入口 |
| product_inquiry | 商品咨询(规格/库存) | 返回商品信息 |
| price_discount | 价格/优惠券咨询 | 返回优惠信息 |
| complaint | 投诉/强烈不满 | 转人工优先处理 |
| human_service | 要求转人工 | 直接转接 |
| chitchat | 闲聊/无关内容 | 闲聊应答或不处理 |
| other | 其他/不确定 | 转人工兜底 |
标签不是越多越好。粒度过细,会导致数据稀疏、小模型学不动;粒度过粗,业务侧又不好用。比如“查物流”和“催发货”在表达上很接近,我们一开始合并成一个track_order,但运营希望区分“只在问状态”和“已经表达不满”的用户,后来才分拆。建议你根据业务方的“后续动作”来定标签,动作不同,才拆成不同意图。
2.2 正则怎么写才不容易误伤
写规则层的正则,最核心的教训是:宁可漏,不要错。漏了还有下一层小模型和LLM兜底,错了就直接走到错误业务流程,用户体感非常差。
以track_order为例。我们一开始写了一个很粗糙的正则:
import re pattern_track_order = re.compile(r"快递|物流|发货|到哪|没到")结果一堆误判。例如“你们发货地址是在哪”被命中成track_order,实际是product_inquiry。再比如“这个物流方式支持顺丰吗”,也被误判。
后来我们把规则拆成“强规则”和“弱规则”两类。强规则要求同时包含动作词和对象词,比如:
# 强规则:动作 + 对象 pattern_track_order_strong = re.compile( r"(查|看|问).{0,6}(快递|物流|单号|到哪)|" r"(快递|物流).{0,6}(到哪|发货|发出)|" r"(怎么).{0,4}(还没到|没收到|不发货)" )弱规则则只作为候选,不能单独下结论,要交给小模型复核。比如只出现“发货”这个词,但没有明确动作,就属于弱命中。
正则的另一个关键点是排除规则。比如“改地址”和“发货地址”虽然有“地址”和“发货”两个词,但真实意图是modify_address或product_inquiry。所以规则层要有一套负向词典,把这些常见干扰项排除掉。
2.3 规则层的入口拦截清单与优先级
规则层不是平均用力,而是按优先级排序。我们实际用的优先级大致是:
- 转人工:只要出现“人工/客服/真人/专员”这类词,直接命中human_service,且不再做任何后续判断。这个意图风险最低、回报最高。
- 投诉:出现“投诉/差评/曝光/315”等词,或者语气词组合“太过分了”“气死我了”,直接命中complaint。投诉用户不能按常规流程处理,必须优先介入。
- 强指令类:改地址、退款、开发票,这些有明确动作词,且动作对象唯一。比如“把地址改成”“申请退款”“开票”。
- 查询类:查物流、查商品、查价格等,这类即使误判,造成的损失也相对小,优先级放后面。
每一层命中后,规则模块会返回一个置信度。规则命中时置信度直接给1.0,但如果是弱规则,就给0.7左右,并标记需要小模型复核。
规则层的目标覆盖率,我们定在“拦下40%到60%的线上请求”。如果你发现线上请求里大量是千奇百怪的表达,说明你的规则还不到位;但如果你发现规则越写越复杂、越来越多的正则互相打架,说明这部分流量该交给小模型处理了,规则层的边际收益到头了。
2.4 规则层为什么能养出小模型的训练集
这一条我觉得是三层架构里最妙的地方:规则层不仅在生产环境干活,它还是小模型训练集的主要来源。
冷启动阶段没有标注数据怎么办?我们把线上历史消息过一遍规则层,凡是强规则命中的,直接当作该意图的伪标注样本。比如命中了“申请退款”强规则的句子,就自动标记为refund。这样不花一分钱标注费,就能拿到几万条高质量训练样本。
当然伪标注会有噪声。我们的做法是:规则命中后抽样人工复核,确认精度达到95%以上,才把这些样本放入训练集。后续每次规则更新,都会自动沉淀一批新样本,标注成本几乎为零。
这个思路其实很朴素:规则是最确定的先验知识,用小模型把规则的泛化能力扩展开,比人肉写几万条正则靠谱得多。
3. 第二层小模型层:TextCNN在CPU上做意图分类
3.1 为什么不直接选BERT系列
小模型层的选型,我们在FastText、TextCNN、BERT小模型之间纠结过。最终选了TextCNN,主要考虑三个因素。
第一是推理速度。线上环境我们用的是普通CPU服务器,没有给这个小模型单独上GPU。TextCNN在这种环境下单条推理只需要2到5毫秒,FastText更快,但在语义理解上稍弱。
第二是训练和维护成本。BERT系列即使是小模型,也需要GPU训练、需要更复杂的微调和蒸馏流程,对小团队来说太重了。TextCNN在一张普通的T4上训练半小时就能收敛,迭代节奏可以快到一天一版。
第三是场景适配。电商客服消息普遍比较短,大部分在20个字以内,这类短文本恰恰是CNN的舒适区。它通过卷积核提取局部n-gram特征,能捕捉到“不退款”和“退款不”这类关键语序差异,应付客服意图分类足够了。
FastText不是不能用,只是它对词序不敏感,遇到“我不想要了要退款”和“退款我不想要了”这种语义相近但词序不同的句子,FastText容易学不到。TextCNN的局部卷积对这个问题有明显改善。
3.2 TextCNN的训练数据怎么来
训练集由三部分构成,按比例混合:
- 规则层伪标注样本,占比约60%。这是基础盘,量最大。
- 人工标注样本,占比约20%。主要覆盖规则层没有覆盖到的长尾表达,每周从线上抽样让运营同学标注几百条。
- 线上bad case回流样本,占比约20%。后面会详细讲回流机制。
每条样本格式很简单:用户的一条消息文本加一个意图标签。需要注意,一条消息如果有多个意图,我们默认取“主意图”作为标签。多意图问题单独走LLM层,不在小模型这里处理。
数据预处理上,我们做了几个动作。一是清理无意义符号,但保留表情符号周边的文本,比如“老板在吗😭”里的表情对判断情绪有用。二是对口语化表达做词典归一,比如“木有”归一成“没有”,“咋还不发”归一成“怎么还不发”。三是切词,中文用jieba,但字级别特征也会保留一部分,原因是电商口语里很多新词和错别字,字级别能兜底。
3.3 模型结构与训练要点
我们的TextCNN结构非常经典,不搞花活。核心参数如下:
import torch import torch.nn as nn class TextCNN(nn.Module): def __init__(self, vocab_size, embedding_dim=128, filter_sizes=(2, 3, 4), num_filters=256, num_classes=11): super().__init__() self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0) self.convs = nn.ModuleList([ nn.Conv1d(embedding_dim, num_filters, kernel_size=size) for size in filter_sizes ]) self.dropout = nn.Dropout(0.5) self.fc = nn.Linear(len(filter_sizes) * num_filters, num_classes) def forward(self, x): x = self.embedding(x) # [batch, seq_len, emb] x = x.transpose(1, 2) # [batch, emb, seq_len] pooled = [] for conv in self.convs: c = torch.relu(conv(x)) # [batch, filters, seq_len - k + 1] p = torch.max_pool1d(c, c.size(2)).squeeze(2) pooled.append(p) out = torch.cat(pooled, dim=1) out = self.dropout(out) return self.fc(out)词表大小5万左右,pad到固定长度32,因为客服消息超过32个字的比例不高。embedding用随机初始化,没有用预训练向量,因为数据量足够大之后,随机初始化训练出来的效果不差,还省去加载预训练模型的部署体积。
训练时用带权重的交叉熵损失,因为refund和track_order这类请求占比高,complaint这类请求占比低,不处理类别不均衡的话,模型会对高频类过拟合,把投诉误判成查物流。类别的权重可以按出现频率的倒数来设置。
训练5个epoch,batch size 64,学习率1e-3,用Adam优化器。验证集准确率一般能到93%到95%。之后导出ONNX格式部署,Python端用onnxruntime加载,兼容性很好。
3.4 置信度阈值和OOD样本放行
小模型层输出的不是意图本身,而是一个概率分布。我们不会直接取argmax作为最终结果,而是看最高置信度落在哪个区间。
我们的阈值设定如下:
- 最高概率大于等于0.85:直接采用该意图。
- 最高概率在0.55到0.85之间:进入“犹豫带”,不直接下结论,而是把top2候选意图交给LLM层,让LLM从两个候选中选一个。这样能显著降低LLM的自由发挥空间,准确率更高。
- 最高概率小于0.55:模型对输入完全没有把握,大概率是OOD(out-of-distribution)样本,也就是训练分布之外的新表达。此时也交给LLM层,但LLM面对的是全量意图列表,而不是top2候选。
这里还有一个细节:小模型判断为“other”时,我们不会直接返回other,而是先检查概率。如果other的概率也不高,说明是其他类别的边缘样本,同样优先给LLM。只有小模型高置信度地判断为other,我们才相信它不是任何已知意图。
4. 第三层LLM层:只在长尾语义里出场
4.1 哪些请求注定要落到LLM
LLM层的定位是兜底,但“兜底”二字的边界需要明确。我总结了三类必须落到LLM层的请求。
第一类是语义含蓄的表达。比如“你们这个也太慢了吧,我都能走到你们仓库去拿了”,这句话没有出现“快递”“发货”等关键词,但真实意图是催发货还带点不满。规则层和小模型很难理解这种反讽,LLM可以。
第二类是双重甚至多重意图。比如“我上周买的那个数据线到现在还在揽收,客服能帮我改一下地址吗”,这里既有查物流又有改地址,且明显改地址才是主意图。文本分类模型默认是单标签输出,处理多意图容易漏。LLM能识别出主次意图。
第三类是带上下文依赖的请求。比如用户上一句问了商品参数,这一句说“那这个呢”,单看当前消息完全无法判断。规则层和小模型只看单条文本,而LLM层可以把最近两轮对话拼进prompt里做理解。
这三类请求占比可能只有5%,但恰恰是最影响用户体验的5%。前面的层判断错了,用户会觉得这个机器人很蠢;LLM把这部分接住,整体体验就上了一个档次。
4.2 Prompt设计与输出约束
LLM层用的系统提示词,我迭代了很多版,目前看起来比较稳的版本大概是这样的:
你是电商客服意图识别器。请判断用户最新一条消息最可能的意图。 可选意图: - track_order: 查物流、催发货 - modify_address: 修改收货地址 - refund: 申请退款 - after_sale: 退换货、维修 - invoice: 发票问题 - product_inquiry: 商品咨询、规格、库存 - price_discount: 价格、优惠券、活动 - complaint: 投诉、强烈不满 - human_service: 要求转人工 - chitchat: 闲聊、与购物无关 - other: 以上都不匹配 规则: 1. 只输出JSON,不要输出任何解释。 2. 格式:{"intent": "...", "confidence": 0.0, "reason": "一句话判断理由"} 3. 如果用户明确要求转人工,intent必须为human_service。 4. 如果消息包含多个意图,选择用户最想让客服执行的意图。 5. confidence低于0.6时intent设为other。 示例: 用户:麻烦看看我的快递到哪了 输出:{"intent": "track_order", "confidence": 0.98, "reason": "用户明确询问快递位置"} 用户:这个衣服我要退货 输出:{"intent": "after_sale", "confidence": 0.95, "reason": "用户表示退货意愿"} 用户最新消息:{user_message}开发者需要设置response_format={"type": "json_object"}之类的参数,前提是你用的模型服务支持结构化输出。如果不支持,就在规则里强调只输出JSON,然后在代码里做强解析。
4.3 模型选型、成本与优化的取舍
LLM层的模型选型,核心是在效果、成本和延迟之间找平衡。我们试过好几家主流模型,最终长期用的是中型规模的API模型,而不是最大最强的版本。
原因很简单:意图识别这个任务对模型的“理解深度”要求没有那么极端,反而对输出格式稳定、延迟低、价格可控有更高要求。中等规模的模型在精心设计的prompt下,准确率和强模型差距不大,但单次调用成本可能只有强模型的五分之一。
成本控制可以算一笔账。假设每天10万条请求,目标是LLM层只承担5%,也就是5000条。每条消息的输入输出token平均约800个,模型单价按当时的市场价格粗算,每天LLM成本大约在十元左右量级。如果全部请求都走大模型,每天成本要高出两个数量级。这就是混合架构的意义。
延迟方面,我们给LLM调用设置了2秒超时和异步降级。如果2秒内没返回,系统不会继续傻等,而是直接转人工兜底。客服场景宁可让真人来接,也不要让用户干等。
4.4 LLM幻觉怎么防:枚举校验与失败兜底
LLM在这类任务上有一个特别常见的毛病:不按枚举输出。比如我们在prompt里给定了11个意图,它有可能会自作主张输出一个“refund_status”之类的新标签,这在语义上似乎合理,但我们下游业务流程根本没有这个分支,一旦冒出来就会挂掉。
我们的解决办法是三层校验。
第一层是枚举校验:代码解析LLM输出的JSON后,检查intent字段是否在候选意图集合里。不在就直接视为无效输出,触发后续兜底逻辑。
第二层是置信度校验:LLM输出的confidence如果低于0.6,我们同样不采用,就算它选了一个看起来很合理的意图,也要转人工。
第三层是业务规则校验:比如LLM判断为refund,但用户消息里完全没有退款、退货、不想要这类的语义,那就算环境概率再高也会被拦住。这一步相当于业务知识的最后一道保险。
兜底链路是:LLM输出无效,降级返回小模型置信度最高的候选;如果小模型也不靠谱,再降级进人工队列。总之,宁可多花人工成本,也不能给出一个错误意图让下游流程执行离谱操作。
5. 调度与数据流:三个层怎么握手才不出乱子
5.1 调度主线:优先级加置信度的握手流程
三层架构不是简单的“规则→小模型→LLM”顺序执行,那样的话每一层都要跑一遍,延迟和成本都是浪费。我们的调度主线是按优先级加置信度来做“短路”判断的。
伪代码大概是这样的:
def recognize_intent(user_message, session_context=None): # 1. 规则层先命中 rule_result = rule_engine.match(user_message) if rule_result.is_high_priority: # 转人工、投诉这类高风险直接短路 return rule_result.intent, "rule", 1.0 # 2. 规则弱命中,规则给了候选,但需要小模型复核 if rule_result.is_weak_match: small_prob = small_model.predict_proba(user_message) # 如果小模型和规则意见一致,直接返回;不一致则进入犹豫带 if small_prob.argmax() == rule_result.intent and small_prob.max() >= 0.6: return rule_result.intent, "rule+small", small_prob.max() # 否则走犹豫带逻辑 # 3. 小模型正常预测 small_prob = small_model.predict_proba(user_message) max_prob = small_prob.max() if max_prob >= 0.85: return small_prob.argmax(), "small", max_prob if 0.55 <= max_prob < 0.85: top2 = small_prob.argsort()[-2:][::-1] llm_intent = llm_choose_from_candidates( user_message, candidates=top2, context=session_context ) return llm_intent, "llm_select", max_prob if max_prob < 0.55: llm_intent = llm_classify_full( user_message, context=session_context ) return llm_intent, "llm_classify", max_prob你可能会问,为什么规则层已经弱命中了,还要小模型复核?因为弱正则的误伤率不低,如果只用规则判断,某些语句会错误进入业务流程。让小模型复核一次,成本几乎可以忽略,但准确率能拉回来不少。
5.2 双重意图与上下文问题怎么处理
前面提到双重意图最好交给LLM,在调度上要有对应逻辑。
小模型输出的是单标签分布,它无法告诉你“用户可能同时想查物流和改地址”。所以我们做了一个轻量检测:当规则层同时命中两个以上不同意图的强规则时,直接跳过小模型层,进入LLM层。比如一条消息同时包含“快递到哪了”和“改地址”,规则层会同时命中track_order和modify_address,我们就知道这是多意图请求,让小模型去硬选一个没有意义,直接交给LLM做主次判断。
上下文问题也是类似。我们的调度器会维护一个会话状态,当用户当前消息很短,比如“那这个呢”“还能改吗”,就会携带最近两轮对话一起拼进LLM的prompt。小模型只处理当前消息,不处理上下文。
这里有一个成本控制策略:并不是所有消息都带上下文,只有当小模型置信度低、或者消息本身非常短时,才把上下文拼接给LLM。因为多传一轮对话,token数量可能翻倍,成本也要翻倍。能用当前消息判断的,就不要浪费token。
5.3 降级策略:LLM超时或限流时怎么办
线上跑久了你会发现,LLM API不可能永远稳定。白天高峰期经常有超时、限流,甚至偶发5xx错误。调度模块必须考虑降级。
我们的降级逻辑是这样的:
| LLM状态 | 降级策略 |
|---|---|
| 正常返回且校验通过 | 使用LLM结果 |
| 超时(超过2秒) | 返回小模型置信度最高的意图,不再等待 |
| 返回格式无法解析 | 尝试重新解析一次,仍失败则用小模型结果 |
| 枚举校验不过 | 直接使用小模型结果 |
| API限流 | 当前请求走小模型结果,同时触发限流保护,后续几分钟减少进入LLM的比例 |
降级的核心原则是:不阻塞、不无限重试。LLM只是三层中的一层,它挂了不能影响整个系统。在我们看来,小模型的答案虽然可能不如LLM聪明,但至少是稳定的、可预期的,比超时无响应强得多。
5.4 线上效果要盯哪些指标
架构上线后,除了关注整体准确率,我还会盯一组分层指标,这样才能知道每一层干得怎么样。
- 规则层覆盖率:规则层直接命中的流量占比,目标40%到60%。如果低于这个数,说明规则词典需要补充。
- 小模型层直接采用率:小模型高置信度直接返回的流量占比,目标30%到40%。这个比例能反映训练数据有没有覆盖住主流表达。
- LLM层调用率:目标5%以内。如果超过10%,要么是前两层没优化好,要么是线上出现了大量新表达,要警惕成本。
- 人工介入率:最终转人工的占比。这个指标比准确率更有业务意义,因为它直接关系到客服团队的工作量。
- 分层bad case数量:每周抽检每层的结果,记录误判数量和类型。
这些指标我们通过日志系统实时统计,每天出一份报表。看到LLM调用率异常上升,多半是线上有大促活动,用户表达开始出现新变化,这时候就需要快速补充规则和训练数据。
6. 线上迭代闭环:每一层的bad case都有自己的修法
6.1 三个层各自的bad case长什么样
三层架构有个好处:任何一个bad case出现后,你能很清楚地知道是谁的锅,然后对症下药。
规则层的典型bad case是过度匹配。比如“你们的发货地址是在上海吗”被track_order的弱规则命中,实际上用户在问仓库位置。修法很直接:给这条规则加排除词“地址”“在哪”,或者把这条规则从强规则降为弱规则,让小模型去复核。
小模型层的典型bad case是语义混淆。比如“这个订单我不想要了”和“这个订单我没收到”,前者是refund,后者是track_order,如果文本太短、上下文不足,小模型很容易混淆。修法是把这类bad case加入训练集,让模型在下一次训练时记住这个教训。
LLM层的典型bad case是过度解读。比如用户说“你可真行啊”,本来可能是吐槽也可能反讽,LLM经常会把这种话判成complaint,但实际上很多用户只是口头禅。修法是调整prompt,在示例中补充类似表达,并强调“不要过度推断情绪”。
6.2 小模型数据回流:别把LLM的错误蒸馏给小模型
每周我们会从线上日志里抽取一批LLM层高置信度的结果,把它们作为小模型的训练样本回流。这个操作本质上是在“蒸馏”LLM的能力,但一定要小心:LLM也会犯错。
我们的回流流程有一个人工确认步骤。运营同学会对抽样的500条LLM结果做快速标注确认,只把确认正确的样本放进训练集。如果这个步骤省掉,LLM的系统性错误会被小模型学走,而且小模型会把错误泛化得更远,最后三层一起错,问题会被放大。
另外,回流的样本不是简单追加到训练集就完事,还要做去重和分布控制。如果连续两周线上大量出现“仅退款”相关请求,而这些样本在训练集中占比过高,模型就会偏向refund,导致其他意图误判率上升。我们一般把每周新增回流样本控制在总训练集增量的20%以内,避免数据分布被带偏。
6.3 一周一次的快节奏迭代
这套架构上线后,我们的迭代节奏是每周一次。
周一到周二:运营同学抽检线上bad case,标记出典型问题。 周三:算法更新规则层词典和正则,同时把确认过的bad case加入小模型训练集,重新训练和评估。 周四:如果有必要,更新LLM层的prompt,用一组固定测试集做回归验证,确保改动没有让其他意图变差。 周五:灰度发布,先放5%流量观察指标,稳定后全量。
这个节奏比纯LLM方案的prompt迭代要快很多,因为大部分bad case都能在规则层或小模型层快速修复,不需要每次都在prompt上反复试探。而规则层的修改是可以精确控制影响范围的,不像改prompt那样可能牵一发动全身。
6.4 一个容易被忽略的细节:版本回溯
线上跑久了,你一定会遇到“这次优化把原本对的搞错了”的情况。所以每一层的规则、模型、prompt都要版本化管理。
模型文件用版本号命名,规则词典放配置中心,prompt存Git。每次发布都在日志里记录当时的版本组合。这样一旦线上指标异常,可以快速对比是哪个版本的变化导致的,一键回滚到上一个稳定版本。
我之前有一次更新了LLM的prompt,加了几个示例后,整体准确率提升了,但“投诉”意图的召回率明显下降。如果没有版本回溯机制,光靠肉眼很难定位是prompt改动导致的。后来我们建立了标准的回归测试集,每次改版都跑一遍所有意图的准确率和召回率,这种问题就能在发布前发现。
三层混合架构相比纯大模型方案,真正的优势不在于某一层有多强,而在于每一层都有清晰的边界和修复手段。规则层错了改规则,小模型错了加数据,LLM错了调prompt,问题永远能在最合适的位置解决,而不是每次都要拿大模型从头试到尾。如果你现在也在做类似的客服意图识别系统,我建议你从这套思路出发做一下小规模验证,先用规则层加一个小模型跑起来,再逐步加入LLM兜底。它可能不是最炫的方案,但一定是能长期稳定跑下去、还让运营成本可控的方案。