1. 从一次生硬的对话聊起
去年我做了一个智能客服项目,客户提的第一句话是“我要查一下上个季度的库存报表”。系统回答:“请问您要查哪个季度的?”。客户当场觉得这不是智能,这是智障。问题就出在自然语言理解这一环。
我相信很多做对话系统的人都经历过这种尴尬:规则写得越多,越发现自己根本写不完;模板做得再细,也架不住用户花式开口。后来我把任务式对话系统整个链路拆开复盘,发现瓶颈几乎都集中在最前端——NLU。它没做好,后面对话管理、自然语言生成做得再好,用户感知到的仍然是个“调不对的菜单”。
这篇总结想拉通聊一聊任务式对话系统里自然语言理解这件事。它会覆盖NLU在整条链路中的位置、意图识别与槽位抽取的核心方法、数据标注与样本构造的套路、以及我在实际项目中踩过的坑和排查经验。适合正在做智能客服、语音助手、交互式查询类系统的开发者或产品同学参考,哪怕你只是刚入门,我也会把那些基础知识连带讲清楚。
在整套系统里,NLU承担的任务可以浓缩成三件事:用户说了什么、用户想干什么、用户给出的关键信息有哪些。听起来简单,做起来却是整套系统返工率最高的地方。
2. NLU在任务式对话系统中的定位拆解
2.1 三个模块的分工边界
任务式对话系统通常被切成四块:自然语言理解、对话状态管理、对话策略、自然语言生成。很多人喜欢把它们串成一条流水线,但实际工程中我更愿意把它们理解成“翻译—记录—决策—表达”的分工。
NLU是翻译官,它的输入是用户的一句话,输出是结构化的语义表示。比如“帮我把闹钟调到明早七点”,NLU要输出意图为“设置闹钟”,槽位为“时间=明早七点”。注意,这里不需要理解“明早”在日历上具体是哪一天,那是对话管理的事。这个边界很重要,如果不划清楚,NLU会变成什么都想干但什么都干不好的杂货铺。
对话管理是记录员和调度员,它接收NLU输出的结构化结果,结合当前多轮对话的上下文,维护状态,比如“用户刚才已经提供了日期,但还没有提供时间,所以需要追问时间”。决策模块决定下一步是继续问槽位、做确认,还是调用后端服务。自然语言生成则是把决策结果变成人话。
这四者之间也会出现一些灰色地带。比如“帮我看看明天杭州的天气”到底是意图“查天气”还是意图“查询城市天气一体完成”?有些系统会把这些拆成多个意图,有些系统则用槽位填充来覆盖。这里没有标准答案,取决于后端服务接口的粒度,但原则很简单:NLU输出的是语义承载单位,不是业务执行动作。意图应当对应“用户想达成的目标类型”,槽位则是完成目标所需的参数。
2.2 为什么说NLU是整个系统的天花板
我一直跟团队强调,对话系统的下限由规则和兜底决定,上限由NLU决定。如果NLU把意图判断错了,后面做再多澄清机制,体验都会打折。用户第一次说错也许能理解,但如果每次都要反复确认,用户就直接放弃了。
更重要的是,NLU的错误会向后传导并放大。意图识别错会导致槽位抽取的对象错,对话管理基于错误语义制定的追问策略也对不上,自然语言生成出来的句子再流畅也没用。这种错误不像网络超时那样容易通过重试解决,它是语义层面的污染。
我见过一个医疗问诊类系统,它的NLU在“头痛”“头晕”这种症状词的实体抽取上做得很好,但用户在长句里说了多个症状时,系统只取了第一个命中实体,导致后续推荐科室全部偏离。这个问题的根源恰恰在某些“历史悠久”的词典匹配代码上,跟模型无关。
所以做NLU时,我建议先想清楚两件事:第一,系统会不会因为一次误判导致不可逆的糟糕体验?第二,你是否有足够的语料支撑你想采用的模型方案?这两件事没想清楚就动手,后期会非常痛苦。接下来我们把NLU的两个核心任务逐一拆开。
3. 意图识别:让机器知道“用户想干什么”
3.1 从分类问题到意图体系设计
意图识别本质上是个文本分类问题。给定一个句子,从预定义意图集合中选出一个最合适的标签。工程上常见做法有三种:规则/词典匹配、传统机器学习分类、深度模型分类。这三者不是互斥的,实际系统里通常是层级套用:先用规则做快速拦截,再用模型处理模糊情况。
我强烈建议先设计好意图体系再选技术路线。意图体系设计不是把后端接口列一遍就完事,它要做抽象。比如你有个“用户修改收货地址”和“用户新增收货地址”的需求,后台是两个接口,但对话系统里合并成一个“维护收货地址”意图更合理,因为用户常常说“把地址改一下,顺便加个门牌号”,一句话里两个动作都涉及。当你把意图划分太细,模型会崩溃,标注员也会崩溃。
我在某电商项目中总结过一个意图划分的简易准则:如果两条候选意图在常见表述下存在超过20%的重叠样本,就应该考虑合并为同一意图,通过槽位区分动作对象。别为了“看起来功能丰富”设计出几十个意图,受限于样本量,意图越多,单意图样本越少,模型效果越差。
3.2 分阶段落地的四个档位
按可控性从高到低,我习惯把意图识别方案分成四档。
第一档是词典规则。适合用来处理无歧义、高频、高可信的表述。比如用户说了“我要退钱”“给我退款”“把钱退回来”,直接映射到“退款”意图。规则的好处是可控、可解释、上线快,缺点是覆盖有限、维护成本随规则数量指数增长。
第二档是传统机器学习,比如朴素贝叶斯、逻辑回归、线性支持向量机,配TF-IDF特征。这类方案适合样本量从几百到几千的小规模场景,训练快,模型小,CPU上就能跑。我早期做过一个语音助手项目,只有五个意图,人工标了一千多句话,用逻辑回归跑出来的F1值已经很能打。小规模场景真没必要上来就上大模型。
第三档是深度文本分类模型,典型是TextCNN、BERT及其轻量变体。适合样本量大、表述变化丰富的场景。TextCNN的优点是轻量且训练稳定,适合作为基线。BERT系模型效果通常更好,但推理延迟较高,在实时对话场景需要考虑性能,通常在GPU上做批量服务或者用蒸馏后的轻量模型。
第四档是Few-shot/LLM增强方案。近年来,越来越多的项目开始用大模型做小样本意图识别,比如构造几个示例输入到prompt中,模型就能直接输出意图标签。但这存在两个问题:第一个是成本与延迟问题,对话场景每轮都调用大模型,服务成本会明显上升;第二个是结果稳定性问题,大模型对标签理解会漂移,尤其是在相近意图边界模糊时。我的建议是,如果团队有较强的推理基础设施且意图数量在几百个以上,可以考虑用大模型做候选集剪枝,再配合小模型精排,而不是让大模型直接做最终决策。
3.3 混淆意图的处理实践
在真实业务里,最容易翻车的往往是那些语义有交叠的意图。我遇到过“查询订单”和“修改订单”两个意图,用户说“我的订单怎么还没到”属于查询,“我想改一下订单里的地址”属于修改,但如果用户说“帮我看看我的订单还能改吗”,两个意图都沾边。
处理这种混淆,常规做法是定义优先级规则:出现“改”“换”“取消”等强动作词,优先走修改类意图。但这个做法有风险,因为用户可能说“我能改明天那个订单吗”,这句话带着“改”,但核心是确认“能不能改”,本质上还是查询类。更合理的做法是增加一个“澄清反问”的兜底路径:当两个候选意图得分接近且超过阈值时,系统不直接选边站,而是反问“您是想修改订单还是只想查看一下进度?”这样虽然多了一轮对话,但避免了执行错误动作造成的不可逆影响。
另外要警惕一个常见坑:测试集和真实线上分布不一致。规则和模型在测试集上准确率都很高,上线后却总出问题,很可能是因为测试集里“典型表述”太多,真实用户却总说“非典型表述”。我现在的习惯是,不管线上样本多脏多怪,都要持续回流,按周做一次badcase分析,再决定是加规则还是补样本,这个操作比调参重要得多。
4. 槽位抽取:把关键信息从话里抠出来
4.1 槽位与实体:先分清概念再说技术
很多初学者把槽位和实体混为一谈,但它们其实是两个层次的东西。实体是文本中的具体片段,比如“明天”“早上七点”“杭州”都是实体。槽位是对话系统完成用户目标所缺的参数,比如“设置闹钟”需要“时间”这个槽位。槽位的值由实体来填充,但一个槽位可能对应多个不同的实体类型,一个实体也可能在不同上下文中填充不同槽位。
举例来说,“杭州明天会下雨吗”里,“明天”是时间实体,它填充的是“日期”槽位;“杭州”是城市实体,填充“地点”槽位。而“帮我定一张明天去杭州的火车票”里,“明天”还是时间实体,但对应的是“出发日期”槽位,“杭州”则是“到达城市”槽位。实体识别是通用的信息的抽取,槽位填充则依赖于当前意图下的语义框架。
工程上常见的槽位抽取方案有三种。第一种基于词典和规则,用正则、枚举词典或句法模板抽取预定义实体。优点是可控、无需标注,适合手机号、日期、金额这类格式化的实体;缺点是难以处理同义词、缩写和上下文歧义。第二种基于序列标注模型,如BiLSTM-CRF、BERT-TokenClassification等,将每个token标注为BIO标签。这是现在落地最多的方案,适合抽取开放域实体,如句子中的自由文本地名、菜品名、人名。第三种是近年来常见的基于大模型的抽取方法,通过指令式设计让模型输出JSON格式的槽位结果,在快速原型阶段很有用。
4.2 BIO标注与模型训练的那些细节
序列标注模型把槽位抽取建模为token级别的分类任务。B表示实体的开始,I表示实体的中间或结尾,O表示不是任何实体的一部分。比如“帮我把闹钟调到明早七点”,假设“明早七点”是时间实体,标注结果是“明(B-时间) 早(I-时间) 七(I-时间) 点(I-时间)”,其余token都是O。
训练时坑很多,我挑几个典型的说。第一个坑是标签不平衡。一句话里大部分token都是O,模型很容易学成“全都预测成O”就能获得极高准确率,但实际槽位一个都抽不出来。应对措施是在损失函数中加入类别权重或者在训练时做负采样,另外从指标上不要只盯token级准确率,要看实体级别的F1,也就是只有当整个实体所有token都被正确标注才算抽对了。
第二个坑是实体边界。比如“北京烤鸭店”到底是一个地点实体还是“北京(地点)+烤鸭店(店铺名)”两个实体,标注标准不统一会让模型学出混乱边界。我现在的做法是提前编写详细标注规范,针对多义词、嵌套实体、模糊边界给出明确示例,并在标注前做试标考核。标注规范的价值再怎么强调都不为过。
第三个坑是槽位共指。用户第一轮说“帮我定个酒店”,系统问“去哪家”,用户回“就上次那家”。这里“那家”并没有包含槽位实体,但对话管理需要结合历史状态解析指代。严格说这属于对话状态管理和指代消解的范畴,但NLU模块最好能把“是否出现了指代”这个信号输出给后期,方便决策层处理,别白白把这种句子标成垃圾样本。
4.3 长句与多槽位场景的处理策略
任务式对话里最考验槽位抽取能力的场景是用户一句话里带多个槽位。比如“帮我查一下明天早上从上海到北京的高铁,要一等座”。这句话里意图是“查火车票”,需要抽出的槽位包括日期“明天”、时段“早上”、出发城市“上海”、到达城市“北京”、座位等级“一等座”。
处理这种多槽位句子,关键点是意图和槽位的耦合关系。不同意图下相同的文本可能对应不同槽位名称,比如“上海到北京”在“查火车票”里是出发和到达,但在“查物流”里可能是发货地和收货地。因此生产环境中,大多数系统会采用基于意图的槽位模板或联合模型,也就是先预测意图,再在意图限定下做槽位抽取。如果意图定错了,槽位抽取大概率跟着错。
对于BERT类的序列标注模型,处理长句还需要注意长度限制。经典的BERT模型输入上限是512个token,很多线上话术不可能那么长,但偶尔会有异常输入。处理方式是超长输入截断,但我建议截断前先判断末尾是否是完整句子边界,否则可能把末尾的关键实体切掉一半。我有个习惯,在服务层加一道“前置文本卫生处理”,做了错别字纠正、标点标准化、长度截断,这能直接减少后面模型的无效计算。
多槽位场景里的另一个经验是不要指望模型一次把所有槽位全部抽准,尤其是低频槽位。系统设计上可以考虑先抽置信度高的核心槽位,低置信度的槽位通过澄清策略让用户补说,这比强抽但抽错的结果好得多。
5. 数据标注:决定NLU上限的隐形环节
5.1 标注体系怎么搭才不容易崩
NLU模型效果的好坏,百分之六十以上由数据决定,模型结构和调参只能给数据锦上添花。我见过不少团队花大力气调BERT,却不愿意在数据上下功夫,结果始终跑不过一个老牌的CRF加精心标注的数据。这句话被很多人当鸡汤听,但做过几个项目后你会信。
搭建标注体系第一步是定义标注标准。意图标签怎么定、槽位边界怎么划、多义词怎么处理、否定句怎么标,都要写成白纸黑字的规范。规范要伴随具体例子,例子的来源应该是真实用户语料,不要自己坐在办公室里脑补,脑补出来的规范大概率跟线上偏差较大。
第二步是选标注工具。小团队别贸然上复杂平台,一张Excel表哪怕再简陋,只要字段清晰、维度固定,也能开工。数据量大起来再切换到专用标注平台,注意平台导出的格式要兼容你后续的训练脚本,否则数据迁移也够折腾的。
第三步是质量控制。我这里的经验是多重一致性校验:同一份样本给两个人独立标注,算标注一致性指标,低于某个阈值就说明规范定义不清,需要打回重修规范。这个指标在低一致性样本上反复迭代,标注质量提上来后,模型效果通常会有明显改善。
5.2 样本构造:从几百条到几万条
冷启动时往往没有真实用户数据,样本全靠人工写。写样本有一条经验法则:与其写一千条精雕细琢的仿真句子,不如写一万条包含口语噪音的糙句子。用户真实表达里带着口头语、简短省略、语法错误,模型训练时需要的是这种“脏”数据,而不是完美的书面语。
从几百条人工种子样本出发,可以借助同义词替换、句式改写、随机插入/删除来扩充样本。注意区分同义改写和随机加噪:同义改写保留语义扩充表达,随机加噪模拟真实口语的错乱和省略,二者要搭配使用,别把数据扩得跟真实分布严重偏离。
更进阶的做法是通过半监督方式,把线上无标注的真实日志收集起来,先用高置信度模型打伪标签,再经过人工抽检后汇入训练集。这个循环可以不断逼近真实分布。我在某个金融问答场景里,用这种办法把槽位抽取的F1值在两个月里提升了约11个百分点。这里有一个前提,你的模型初始效果不能太差,否则打出来的伪标签质量过低,会把模型喂坏。
5.3 样本量估算与类目平衡
新手最喜欢问一个问题:每个意图到底要多少样本才够?这个没有定值,但可以给一个粗略的感觉:规则能覆盖的简单意图,三五百条样本足够;表述多变、边界模糊的意图,两千起步;如果涉及多个槽位组合,每个常见组合最好有几十条覆盖,长尾组合也要保证至少三五条,否则模型的槽位组合泛化能力会严重缺失。
类目平衡也很关键。某些高频意图样本很多,某些低频意图样本很少,模型天然偏向高频类。常见做法是采样时设置权重,或者对低频类做过采样和同义改写补充。但补充的时候不要简单复制同一句话,样本量和样本多样性是两回事。
另外我强烈建议把测试集单独留出,且测试集要与训练集分布保持一致,不要在标注过程中混在一起。线上效果评估如果缺少稳定的测试集,你就无法区分模型迭代带来的真实涨点还是数据分布漂移造成的偶然变化。
6. 实操过程:搭一个可用的NLU模块
6.1 端到端技术选型与架构参考
以一个中等规模的智能客服系统为例,我给出一个可以落地参考的选型组合。意图识别使用TextCNN小模型作为主模型,同时叠加一套关键词规则前置,用于高频意图的快速命中;槽位抽取使用BERT-BiLSTM-CRF联合模型,意图识别和槽位抽取分别跑两条服务,通过接口串联,这样任一侧迭代时不会互相影响。
可能有同学会问,为什么意图识别不也用BERT?原因很实际:意图识别类别通常较多,但句子较短、上下文信息有限,TextCNN在这种任务上精度已经接近BERT,但推理成本和资源占用明显低。所以线上意图识别先用轻量模型顶着,如果效果不够再换成蒸馏版BERT。槽位抽取对边界感知要求更高,BERT类模型的上下文编码能力更强,投入产出比更高。
服务部署上,我习惯用Python的PaddleNLP或transformers加载模型,通过Flask/FastAPI提供服务。BERT模型做一次推理大约耗时20到80毫秒,取决于文本长度和硬件,对话场景单轮交互时延必须控制在500毫秒以内,NLU如果超过200毫秒就要告警了。
6.2 准备数据与训练一个意图模型
第一步先把标注数据整理成标准格式。假设有三列样本:文本、意图标签、槽位序列。意图识别训练可以直接用文本和意图标签两列。以逻辑回归为基线先跑一把,如果你的任务简单,可能基线就已经够用了,这一步不能跳过。
第二步特征工程。对中文来说,先把句子做分词。常用分词工具在不同领域效果差异很大,专有名词、品牌名、生造词容易切错。我建议在通用分词基础上,叠加自定义词典,把业务强相关的词汇先绑住。然后用TF-IDF特征训练逻辑回归,这也是我团队每次项目启动必做的基线,基线结果作为后续所有模型效果的参照系。
第三步搭TextCNN模型。网络结构不复杂:输入层将每个词映射成向量,经过三个不同尺寸的卷积核提取邻近词特征,再做池化拼接到全连接层,最后用softmax输出概率分布。关键超参数是卷积核大小,常用组合是2、3、4,即分别关注双词、三词、四词的局部模式。
第四步训练与评估。训练集、验证集、测试集按8比1比1划分。关注准确率的同时必须看每个类别的精确率、召回率和F1值,以及混淆矩阵。如果两个意图频繁互混,回到意图定义层面去考虑是否该合并。
6.3 训练槽位抽取模型的心得
槽位抽取需要先确定标注策略。通常按BIO格式标注。你可以自己写标注脚本,也可以借助标注平台导出。训练数据大概需要数千到上万条,常见槽位种类可以先从5到10种起步。
我用BERT做序列标注时,经验上学习率设为2e-5到5e-5之间,batch size则视显存而定,句子长度上限按数据分位数取而非直接取512。在训练后处理上,我通常会把模型的预测结果和规则结果做融合,规则优先处理格式明确的实体。这个融合说起来简单,实现时需要仔细斟酌冲突消解规则,否则两套结果打架会加剧错误。
模型上线后必须加一个“可信度过滤”:当模型预测某个槽位的置信度低于阈值时,不输出该槽位,交给对话管理去澄清。这个机制让整条对话链路的体验提升非常明显。有人担心阈值设高了会漏掉很多正确槽位,我用的是动态阈值,与历史对话轮数和关键槽位优先级联动。
6.4 联调:从单模块到全链路的贯通
单模块跑通不算完,联调阶段才能暴露出最真实的坑。意图识别和槽位抽取接口是分开的还是串行的,会直接影响延迟和纠错策略。我建议串行但可配置:某些简单意图可以直接跳过槽位模型,减少一次推理调用。
联调的第一个检查点是意图与槽位的组合合法性。比如“查天气”意图下出现“航班号”槽位,明显非法,横跨接口的校验逻辑能拦截这类组合。第二个检查点是空槽位和缺槽位的处理策略,当用户给的信息不足时,对话管理必须拿到准确“缺哪些槽位”的信息,而不能只拿到一句“未识别出槽位”的模糊消息。第三个检查点是多轮交互后的语义刷新,例如用户修改上一轮提供的城市时,槽位覆盖逻辑是否正确。
我记得有一次联调,用户说“帮我定个明天北京的酒店”,系统先确认了日期和城市,但用户反问“我明天不去北京了,改成上海”,意图其实还是“订酒店”,但日期槽位没有发生变化,城市槽位要从北京改成上海,如果槽位更新逻辑写成了“仅在新增时追加”,城市就永远是北京了。这种问题不在NLU模型本身,而在上下游对接逻辑,联调时一定要重点走查。
7. 常见问题与排查技巧实录
7.1 老模型上线效果还行,半年后断崖式下跌
这类问题十有八九是数据分布漂移,不是模型坏了。用户新增了新的表达习惯、新的产品叫法、新的网络热词,老模型没见过,自然识别不准。排查方式很简单:把最近的badcase收集起来,聚类看共性。如果集中在某几个词或某类句式上,就给对应意图加规则或补标注样本。
这里要注意,模型重训不是一劳永逸的,对话系统的NLU是持续性运营模块。我的建议是建立线上日志回流通道,每周产出一份badcase清单,每月做一次轻量重训,每季度做一次全量重训。这个节奏在多数业务场景中是可行的。
7.2 小样本下模型过拟合,怎么救
有些业务意图的天然数据就很少,强行训练大模型容易出现过拟合。缓解手段有几种:一是利用预训练模型做特征抽取而非端到端训练,减少模型参数更新范围;二是加正则化和早停;三是从类似意图迁移少量样本辅助训练;四是使用小样本学习手段,构造支持集和查询集来训练模型区分新意图的能力。
在小样本场景下,也可以优先考虑纯规则方案。我曾经做一个功能点查询类系统,意图数量少、句式高度模板化,我直接用正则加词典就覆盖了九成以上的线上请求,模型都还没用上。先把规则方案用透,再考虑加模型,这个顺序在资源有限时特别重要。
7.3 槽位值解析正确但对话管理用不上
这类问题常出现在时间、日期、金额表达上。模型可能从文本里正确抽出了“明天上午”,但对话管理期望的槽位值是标准化格式“2025-06-20 09:00:00”,中间缺一个语义解析层,也就是把自然语言时间表达转成结构化时间。这个工作通常放在NLU后处理环节,需要引入时间解析库或自研解析规则。
金额表达也很典型。“大概两百块左右”“最多不超过三百”这类模糊表述,槽位抽取阶段先抽取原文字段,解析阶段再做判断。更麻烦的是比较级和否定表达,“不要超过三百”“两百到三百之间”,如果解析层不做特殊处理,槽位值就会提取出一个你不想要的数值。这块我建议单独制定解析规范,别混在模型训练任务里。
7.4 线上日志里的“识别失败”到底意味着什么
排查识别失败前,先要定义“失败”是什么。有些人认为是“置信度低于阈值”,有些人是“意图是兜底类”,有些人则是指“槽位为空”。三种定义对应完全不同的处理逻辑,排查方向也不同。置信度低说明样本分布或模型校准有问题;兜底类命中说明语料覆盖不够;槽位为空可能是用户真没给,也可能是抽取能力不足。
我习惯在日志里给每个识别结果打上多个标记:意图得分、槽位得分、是否属于规则命中、是否落入兜底类。这样排查时可以快速分流。如果一晚上几千次请求的失败原因全是兜底类,说明训练语料和真实语料有较大偏差,需要从数据回流开始改正。如果失败集中在某一个意图,则优先检查该意图样本量和混淆程度。
7.5 意图识别正确,但槽位全填空的诡异情况
我在某项目中碰到过一种情况:意图识别模型输出正确,但槽位抽取模型对所有句子都输出空序列。先怀疑模型权重损毁或加载错误,排查后发现是线上服务在预处理时把文本的标点全部去掉了,导致实体边界信息丢失。这类问题上线前要仔细检查预处理链路的一致性,训练时和预测时的预处理必须完全一致,哪怕一个标点符号的处理逻辑不同,结果都可能从天上掉到地上。
另一个类似坑是分词器版本不一致。训练用的分词器版本和线上加载的不一致,会导致词id映射错位,模型性能大打折扣。这里建议把分词器文件和模型权重一起固化到服务镜像中,版本锁定,避免“某天某人悄悄升级了依赖库导致线上翻车”。
8. 我的经验总结
做了这么久NLU,我最大的感受是:别把自然语言理解当深度学习问题来崇拜,它本质上是个系统工程问题。模型能做很多事,但模型不能告诉你用户某个表达在业务上意味着什么,也不能替你定义意图边界,更不能替你处理脏乱差的线上数据。这些事,必须靠人和流程去解决。
在项目启动早期,我会花很多时间跟用户、客服、产品去聊,把他们的真实表达记下来,去整理意图体系,我会反复追问“这个意图和那个意图能不能合并,什么时候要拆开”。这个阶段看起来很无聊,但实际上这是整个项目投资回报率最高的阶段。后置到开发和标注阶段再改意图体系,代价会成倍放大。
技术选型上我推荐一套组合拳思路,规则保底,模型提效,大模型备用。如果你有三五百条标注数据,先上逻辑回归或者规则;如果你的意图数量超过二十个,样本量超过五千条,再考虑深度模型。一定要让模型复杂度匹配数据规模,否则就是拿大炮打蚊子,打不到不说还费钱。
还有一个容易忽略的点:NLU模块的接口设计要预留扩展位。意图标签、槽位名称、置信度阈值这些都要做成可配置项,不要硬编码在代码里。你永远预料不到三个月后业务方会提出什么样的新需求,一个灵活配置的NLU服务会给你省下大量不必要的改版和联调时间。
最后说一个小技巧。每次模型上线前,我会准备一组“魔鬼样本”,专门挑那些最容易被混淆的句子放进测试集,比如意图边界上有争议的话、槽位嵌套复杂的句子、带口语省略的长句。只要这些样本能稳定通过,线上大概率差不到哪里去。反过来说,如果这些样本没过,就坚决不上线,别指望线上用户比你好说话。
自然语言理解这条路没有终点,你永远会收到新的话术、新的产品名词、新的用户绕法。工程的意义就在于把每一次“看不懂”变成看得懂并快速处理掉,不断提高对话系统那层玻璃天花板。希望这篇总结能给你一些真实可用的参考,少踩几个我当年踩过的坑。