嵌入向量给用户问题做意图分类路由实操
先说结论:一个入口要应付好几类问题,别再堆 if-else 关键词了。把用户那句话先转成嵌入向量,跟你预设的几个意图样本算余弦相似度,谁近就路由到谁的处理链——退款走退款链,查物流走物流链,闲聊扔给兜底。我上个月给一个内部客服机器人这么改完,误分类肉眼可见地少了一截。
我踩到的那个问题
背景是这样。我们组维护一个给运营同学用的小工具,入口就一个输入框,啥都能问。一开始我图省事,用关键词匹配做分流:句子里有"退"就当退款,有"到哪了"就当查物流。
跑了两周,运营群里炸了。
有人问"这单为啥退不了"——里面有"退",路由到退款链,结果人家是想查物流状态卡在哪。还有人打错字把"物流"写成"无流",直接掉进兜底,回了句"没听懂"。关键词这玩意儿对语义一点感知都没有,换个说法就抓瞎。我盯着日志看了半天,决定换嵌入。
分类路由怎么做,分四步
思路不复杂,核心就是把"匹配字面"换成"匹配语义"。
第一步,给每个意图准备几条样本句。别只写一条。比如"查物流"我写了"我的包裹到哪了""快递怎么还没动""单号查一下到哪"这么三四条,覆盖不同说法。
第二步,把这些样本句全部过一遍嵌入模型,存成向量。这步离线做一次就行,结果缓存起来。我直接存内存里了,几十条向量而已,没必要上向量库。
第三步,用户问题进来,实时转成向量,跟每个意图的样本向量算余弦相似度。取每个意图里最高的那条当该意图得分。
第四步,谁分高走谁,但要卡个阈值。最高分低于 0.75 我就判定"没匹配上",扔兜底链让大模型自由发挥,别硬塞。
一段能跑的核心代码
import numpy as np # 意图样本(离线先转成向量缓存好) INTENTS = { "refund": ["我要退款", "这单想退掉", "钱怎么退回来"], "logistics":["包裹到哪了", "快递还没动", "查下物流到哪"], } def cos(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) def route(query_vec, intent_vecs, threshold=0.75): best_intent, best_score = None, -1 for name, vecs in intent_vecs.items(): score = max(cos(query_vec, v) for v in vecs) # 取该意图最近的一条 if score > best_score: best_intent, best_score = name, score return best_intent if best_score >= threshold else "fallback" # route(embed("我这单为啥退不了"), intent_vecs) -> "refund"embed()就是调嵌入接口拿向量,剩下全是 numpy。真没多少行。
一个真把我坑住的地方
阈值这事我栽过。
一开始我把 threshold 设成 0.6,想着"宽松点别漏"。结果闲聊和正经问题全被归到最近的业务意图去了——有人发了句"在吗",居然被判成查物流,因为跟某条样本沾了点边。后来调到 0.78 又太严,正常的问句被踢进兜底。
来回试了大半天,最后落在 0.75,并且发现一个细节:阈值跟你用的嵌入模型强相关,换个模型这数得重调。不同模型出来的向量分布不一样,余弦值的"高低"刻度根本不通用。所以别抄别人的阈值,自己拿一两百条真实日志跑一遍分布,看错分都卡在哪个值,再定。
另外多嘴一句脏细节:样本句别写得太书面。我最早样本全是"请问我的订单物流信息如何查询"这种端着的句子,结果用户大白话"东西到哪了"反而匹配不上。后来把样本改成跟用户一样的口气,准确率立马上去了。
写在后面
整套搭下来其实最花时间的不是代码,是攒样本句和调阈值。代码两小时,调参两天。
搭这个分类器的时候我没自己撸服务,是在一个零代码就能配智能体的平台上拖出来的——意图分类、知识库、几条处理链拉一拉就连上了,省了我搭框架的功夫。当然它也就帮你把杂活串起来,真正的业务逻辑和样本还是得自己抠,指望它一键生产可用机器人是不现实的,第一版出来照样干巴巴。
你们做多意图路由是怎么定阈值的?还在用关键词的评论区聊聊,我挺好奇有没有更省事的招。
(嵌入和大模型我走的讯飞星辰 MaaS,现成 API 调,没自己部署算力。)
