揭秘AI原生应用领域意图识别的核心技术

当你对着Copilot说「帮我优化这段Python代码的性能」,它立刻明白你需要「代码优化」服务;当你在ChatGPT中说「帮我查明天北京到上海的高铁票」,它会自动调用12306插件并传递关键参数——这一切的背后,都是**意图识别(Intent Recognition)**在发挥作用。

AI原生应用(AI-Native Application)的核心竞争力,在于「理解用户需求」的能力。而意图识别,就是AI理解用户的「第一扇门」——它将用户的自然语言、语音甚至多模态输入,转化为机器可执行的「意图指令」,并提取关键参数(槽位)。没有精准的意图识别,再强大的大模型也会沦为「对牛弹琴」的工具。

一、什么是AI原生应用的意图识别?

在聊技术之前,我们需要先明确两个概念:传统意图识别AI原生意图识别

1. 传统意图识别:封闭域、规则驱动

传统的意图识别(比如早期的Siri、小度),本质是「封闭域的规则匹配」。开发者会预先定义一个「意图列表」(比如「查询天气」「播放音乐」「设置闹钟」),然后为每个意图编写规则(比如关键词匹配:包含「天气」则触发「查询天气」意图),或者用简单的机器学习模型(比如SVM、随机森林)基于关键词特征分类。

这种方式的问题很明显:

  • 无法处理开放式意图:如果用户说「帮我找一家附近的猫咖」,而预定义意图中没有「寻找猫咖」,系统就会报错;
  • 缺乏上下文理解:如果用户先问「最近有什么好电影」,再问「它的评分怎么样」,传统系统无法理解「它」指的是「最近的好电影」;
  • 对自然语言的灵活性支持差:比如用户说「明天去上海的机票有没有」和「有没有明天到上海的机票」,虽然语义相同,但规则可能无法覆盖所有句式。

2. AI原生意图识别:开放域、上下文驱动、大模型赋能

AI原生应用的意图识别,是「以大模型为核心,支持开放域、上下文感知、多模态」的意图理解系统。它的核心特点包括:

  1. 开放域处理:不需要预定义所有意图,能通过Few-shot/Zero-shot学习识别未见过的意图;
  2. 上下文感知:能理解多轮对话中的指代、意图延续(比如「订咖啡→加蛋糕」);
  3. 多模态支持:能处理文本、语音、图像、视频等多模态输入的意图(比如「这张图片里的猫可爱吗?」);
  4. 槽位与意图联合优化:不仅识别意图,还能提取意图的关键参数(槽位),并通过联合建模提高准确率。

简单来说,传统意图识别是「听关键词」,而AI原生意图识别是「听懂意图」——它能理解用户的「言外之意」,甚至「未说之言」。

二、意图识别的基础框架

要理解AI原生意图识别的技术,我们需要先看它的基础框架。下图是一个典型的AI原生意图识别流程:

用户输入
多模态预处理
上下文融合
特征提取与编码
意图分类
槽位填充
意图输出
槽位输出
应用逻辑执行

1. 用户输入:多模态兼容

AI原生应用的用户输入不再局限于文本,还包括语音(比如语音助手)、图像(比如AI识图)、视频(比如视频内容理解)。比如用户上传一张「猫的图片」并说「它多大了?」,输入就是「图像+文本」的多模态数据。

2. 多模态预处理

预处理的目标是将原始输入转化为模型可处理的格式:

  • 文本预处理:分词(比如用Jieba分词处理中文)、去除停用词(比如「的」「了」)、实体识别(比如识别「北京」「上海」为地名);
  • 语音预处理:将语音信号转化为梅尔频谱图(Mel Spectrogram),然后用语音模型(比如Wav2Vec 2.0)转化为文本;
  • 图像预处理:将图像 resize 到模型要求的尺寸(比如224x224)、归一化(比如减去均值除以标准差),然后用图像模型(比如ResNet)提取特征。

3. 上下文融合

上下文融合是AI原生意图识别的关键步骤之一。它将「当前用户输入」与「历史对话/交互数据」结合,解决指代消解、意图延续的问题。比如:

  • 历史对话:「用户:我想订明天去上海的机票。系统:请问出发地是哪里?用户:北京。」——历史对话中的「订明天去上海的机票」是当前输入「北京」的上下文;
  • 交互数据:比如用户之前的购买记录(比如经常订早上的机票),可以辅助意图识别(比如用户说「订机票」,系统优先推荐早上的航班)。

常用的上下文融合方法包括:

  • 循环神经网络(RNN):用RNN的隐藏状态保存历史信息;
  • Transformer的Memory机制:将历史对话编码为Memory向量,与当前输入向量拼接;
  • 大模型的上下文窗口:比如GPT-4的8k/32k上下文窗口,直接将历史对话作为输入的一部分。

4. 特征提取与编码

特征提取的目标是将预处理后的输入转化为「语义向量」——机器能理解的「语言」。在AI原生意图识别中,**预训练语言模型(PLM)**是绝对的核心,比如:

  • 文本编码:BERT、RoBERTa、GPT(用于文本输入);
  • 多模态编码:CLIP(用于文本+图像输入)、Whisper(用于语音输入)。

以BERT为例,它通过「双向Transformer」将文本转化为「上下文相关的语义向量」。比如「订明天去上海的机票」中的「订」「明天」「上海」「机票」都会被编码为包含上下文信息的向量——「订」的向量会包含「与机票相关的动作」的信息,「上海」的向量会包含「目的地」的信息。

5. 意图分类

意图分类是将编码后的语义向量映射到「意图标签」的过程。在AI原生应用中,意图分类可以是:

  • 封闭域分类:预定义意图列表(比如「预订机票」「查询天气」),用全连接层+Softmax输出每个意图的概率;
  • 开放域生成:用大模型的文本生成能力,直接输出意图描述(比如用户输入「帮我找附近的猫咖」,模型输出「意图:寻找猫咖」)。

6. 槽位填充

槽位填充是提取意图的「关键参数」的过程,比如「预订机票」的槽位包括「出发地」「目的地」「时间」「舱位等级」。槽位填充本质是「序列标注任务」——给每个token打上槽位标签(比如「北京」→「出发地」,「上海」→「目的地」,「明天」→「时间」)。

常用的槽位填充模型包括:

  • CRF(条件随机场):处理序列标注的经典模型,能考虑相邻token的标签依赖(比如「出发地」后面不太可能跟着「时间」);
  • Transformer+CRF:用Transformer提取特征,CRF优化序列标签,是当前槽位填充的主流方法。

7. 应用逻辑执行

最后,意图输出和槽位输出会被传递给应用逻辑层,执行具体的操作:比如调用机票预订API、查询天气API、生成代码等。

三、核心技术点拆解

AI原生意图识别的核心竞争力,在于解决「传统意图识别无法处理的问题」。接下来,我们深入拆解三个关键技术点:开放式意图识别上下文感知的意图推理槽位与意图的联合建模

一、开放式意图识别:从“封闭列表”到“开放学习”

传统意图识别的最大痛点,是无法处理「未预定义的意图」。比如用户说「帮我找一家能撸猫的咖啡馆」,如果预定义意图中没有「寻找猫咖」,系统就会回复「我听不懂你的问题」——这在AI原生应用中是无法接受的,因为AI原生应用需要支持「无限可能的用户需求」。

开放式意图识别的目标,是让模型「在少量示例或无示例的情况下,识别未见过的意图」。核心技术包括:Few-shot Learning(少样本学习)Zero-shot Learning(零样本学习)意图聚类(Intent Clustering)

1. Few-shot Learning:用少量示例“学会”新意图

Few-shot Learning的核心思想是:「给模型看几个例子,它就能识别新的意图」。比如要让模型识别「寻找猫咖」的意图,只需要给它3-5个示例:

  • 输入:「帮我找一家附近的猫咖」→ 意图:寻找猫咖
  • 输入:「有没有能撸猫的咖啡馆?」→ 意图:寻找猫咖
  • 输入:「我想找个有猫的地方喝咖啡」→ 意图:寻找猫咖

模型通过学习这些示例的「语义特征」,就能识别新的「寻找猫咖」意图。

在大模型时代,Few-shot Learning的实现方式主要是Prompt Engineering(提示工程)——将意图识别转化为「文本生成任务」。比如:

  • 输入Prompt:「用户输入:帮我找一家附近的猫咖。意图:寻找猫咖;用户输入:有没有能撸猫的咖啡馆?意图:寻找猫咖;用户输入:我想找个有猫的地方喝咖啡。意图:寻找猫咖;用户输入:帮我找一家能撸猫的店。意图:」
  • 模型输出:「寻找猫咖」

Prompt Engineering的关键是「设计有效的提示模板」,让模型理解「输入→意图」的映射关系。常用的Prompt模板包括:

  • 前缀Prompt:在输入前添加示例,比如「例子:输入→意图;输入→意图;当前输入→意图:」;
  • 指令Prompt:用自然语言指令告诉模型任务,比如「请识别用户输入的意图,意图标签包括:寻找猫咖、预订机票、查询天气。用户输入:帮我找一家附近的猫咖。意图:」。
2. Zero-shot Learning:无示例识别新意图

Zero-shot Learning比Few-shot更激进:「不需要任何示例,模型就能识别新意图」。它的核心思想是「利用意图的「语义描述」来识别新意图」。比如:

  • 意图的语义描述:「寻找猫咖」=「寻找提供猫咪陪伴的咖啡馆」;
  • 用户输入:「帮我找一家能撸猫的咖啡馆」;
  • 模型通过比较「用户输入的语义」与「意图语义描述的语义」,判断意图是「寻找猫咖」。

在大模型中,Zero-shot Learning的实现方式是将意图的语义描述作为Prompt的一部分。比如:

  • 输入Prompt:「请识别用户输入的意图。意图列表及描述:1. 寻找猫咖:寻找提供猫咪陪伴的咖啡馆;2. 预订机票:预订往返或单程的飞机票;3. 查询天气:查询某个城市的天气情况。用户输入:帮我找一家能撸猫的咖啡馆。意图:」
  • 模型输出:「寻找猫咖」
3. 意图聚类:发现“未知的意图”

除了识别「已知的新意图」,开放式意图识别还需要「发现未知的意图」——比如用户输入的内容不在任何预定义意图或语义描述中,模型需要自动聚类这些输入,发现新的意图类别。

常用的意图聚类方法是无监督学习,比如:

  • DBSCAN:基于密度的聚类算法,能发现任意形状的聚类(比如「寻找猫咖」「寻找狗咖」是两个不同的聚类);
  • K-means:基于距离的聚类算法,需要预先指定聚类数量(比如K=5,将输入分为5个意图类别);
  • 层次聚类:将输入逐步合并为更大的聚类,能生成聚类的层次结构(比如「寻找宠物咖啡馆」→「寻找猫咖」「寻找狗咖」)。

意图聚类的流程通常是:

  1. 用预训练模型(比如BERT)将用户输入编码为语义向量;
  2. 对语义向量进行聚类(比如DBSCAN);
  3. 人工标注聚类的意图标签(比如将某个聚类标注为「寻找猫咖」);
  4. 将新的意图标签添加到模型的意图列表中,更新模型。

二、上下文感知的意图推理:从“单轮”到“多轮”

在AI原生应用中,用户的需求往往是「多轮的」——比如用户先问「最近有什么好电影」,再问「它的评分怎么样」,再问「在哪里能看」。这时候,系统需要理解「它」指的是「最近的好电影」,「在哪里能看」是「查询电影播放平台」的意图,而不是「查询某个地点的位置」。

上下文感知的意图推理,就是让模型「记住历史对话内容,并利用历史内容理解当前意图」。核心技术包括:Memory-augmented Transformer(记忆增强Transformer)指代消解(Coreference Resolution)意图延续检测(Intent Carryover Detection)

1. Memory-augmented Transformer:给模型“装个记忆库”

Transformer模型本身具有「上下文窗口」(比如GPT-4的8k窗口),能处理一定长度的历史对话。但对于更长的对话(比如10轮以上),Transformer的上下文窗口就不够用了——这时候需要Memory-augmented Transformer,给模型添加一个「外部记忆库」,用于存储历史对话的关键信息。

Memory-augmented Transformer的结构如下:

  • 记忆库(Memory Bank):存储历史对话的编码向量(比如每个历史轮次的语义向量);
  • 注意力机制(Attention):当前输入的编码向量与记忆库中的向量计算注意力,提取与当前输入相关的历史信息;
  • 融合层(Fusion Layer):将当前输入的向量与注意力加权后的记忆向量融合,得到「上下文感知的语义向量」。

数学公式表示:
假设当前输入的编码向量为xtx_txt,记忆库中的向量为M=[m1,m2,...,mt−1]M = [m_1, m_2,...,m_{t-1}]M=[m1,m2,...,mt1]mim_imi是第i轮对话的编码向量),则:

  1. 计算注意力权重:ai=softmax(xt⋅miTd)a_i = softmax(\frac{x_t \cdot m_i^T}{\sqrt{d}})ai=softmax(d xtmiT)(d是向量维度);
  2. 计算记忆向量:mtmem=∑i=1t−1aimim_t^{mem} = \sum_{i=1}^{t-1} a_i m_imtmem=i=1t1aimi
  3. 融合向量:xtctx=[xt;mtmem]x_t^{ctx} = [x_t; m_t^{mem}]xtctx=[xt;mtmem]([;]表示向量拼接)。

这样,xtctxx_t^{ctx}xtctx就包含了当前输入和历史对话的信息,模型用这个向量进行意图识别和槽位填充。

2. 指代消解:解决“它”“那”的问题

指代消解是上下文感知的核心任务之一——它的目标是「确定代词(比如「它」「那」「这个」)所指代的实体」。比如:

  • 对话历史:「用户:我想订明天去上海的机票。系统:请问出发地是哪里?用户:北京。」
  • 当前输入:「它的起飞时间是什么时候?」
  • 指代消解的结果:「它」→「明天从北京到上海的机票」。

在AI原生意图识别中,指代消解的实现方式主要是大模型的上下文理解能力——比如GPT-4能直接理解「它」指代的是历史对话中的「机票」。对于更复杂的指代(比如「那个红色的杯子」),可以结合实体识别(NER)共指消解模型(Coreference Resolution Model),比如Hugging Face的coref库。

3. 意图延续检测:判断“是否是同一意图的延续”

意图延续检测的目标是「判断当前输入是否是历史意图的延续」。比如:

  • 历史意图:「预订机票」(用户说「我想订明天去上海的机票」);
  • 当前输入:「加一份行李托运」;
  • 意图延续检测的结果:「是」——当前意图是「预订机票」的延续(添加行李托运)。

如果检测结果是「是」,系统就会将当前输入的槽位(比如「行李托运」)添加到历史意图的槽位中;如果是「否」,系统就会识别新的意图。

意图延续检测的常用方法是分类模型:将「历史对话编码向量」与「当前输入编码向量」拼接,输入到全连接层+Softmax,输出「是/否」的概率。比如:

  • 输入:[mt−1;xt][m_{t-1}; x_t][mt1;xt]mt−1m_{t-1}mt1是历史对话的编码向量,xtx_txt是当前输入的编码向量);
  • 输出:P(延续∣mt−1,xt)P(延续|m_{t-1}, x_t)P(延续mt1,xt)(延续的概率)。

三、槽位与意图的联合建模:从“分开做”到“一起做”

在传统意图识别中,意图分类和槽位填充是「分开进行」的:先识别意图,再根据意图填充槽位。比如:

  1. 识别用户输入「订明天去上海的机票」的意图是「预订机票」;
  2. 根据「预订机票」的槽位列表(出发地、目的地、时间),填充槽位:出发地=(未提及)、目的地=上海、时间=明天。

这种方式的问题是误差传递:如果意图识别错了(比如把「订机票」识别成「查询天气」),那么槽位填充肯定也会错。而AI原生意图识别采用联合建模的方式,将意图分类和槽位填充作为「多任务学习」,共享模型的编码器,同时优化两个任务的损失函数——这样能减少误差传递,提高整体准确率。

1. 联合建模的结构:BERT + 双任务头

当前最主流的联合建模结构是Joint BERT(由Google在2019年提出),它的结构如下:

  • 编码器:BERT,用于提取输入的语义向量;
  • 意图分类头:全连接层+Softmax,输入是BERT的[CLS]向量(代表整个输入的语义),输出意图的概率分布;
  • 槽位填充头:全连接层+CRF,输入是BERT的每个token的向量,输出每个token的槽位标签。

Joint BERT的结构示意图:

graph TD
    A[输入文本] --> B[BERT编码器]
    B --> C[[CLS]向量]
    B --> D[Token向量序列]
    C --> E[意图分类头(全连接+Softmax)]
    D --> F[槽位填充头(全连接+CRF)]
    E --> G[意图输出]
    F --> H[槽位输出]
2. 联合损失函数:同时优化两个任务

Joint BERT的损失函数是意图分类损失槽位填充损失的加权和:
Ltotal=αLintent+(1−α)LslotL_{total} = \alpha L_{intent} + (1-\alpha) L_{slot}Ltotal=αLintent+(1α)Lslot
其中:

  • LintentL_{intent}Lintent:意图分类的交叉熵损失,计算[CLS]向量与意图标签的误差;
  • LslotL_{slot}Lslot:槽位填充的CRF损失,计算Token向量序列与槽位标签序列的误差;
  • α\alphaα:权重系数(通常取0.5,平衡两个任务的重要性)。
(1)意图分类的交叉熵损失

对于意图分类,假设意图标签的真实分布是yintenty_{intent}yintent(one-hot向量),模型输出的概率分布是y^intent\hat{y}_{intent}y^intent,则:
Lintent=−∑i=1Cyintent,ilog⁡y^intent,iL_{intent} = -\sum_{i=1}^C y_{intent,i} \log \hat{y}_{intent,i}Lintent=i=1Cyintent,ilogy^intent,i
其中C是意图的数量。

(2)槽位填充的CRF损失

对于槽位填充,假设槽位标签序列是yslot=[y1,y2,...,yT]y_{slot} = [y_1, y_2,...,y_T]yslot=[y1,y2,...,yT](T是Token的数量),模型输出的Token向量序列是h=[h1,h2,...,hT]h = [h_1, h_2,...,h_T]h=[h1,h2,...,hT],CRF层的参数是转移矩阵AAAAi,jA_{i,j}Ai,j表示从标签i转移到标签j的概率),则CRF的损失函数是:
Lslot=−log⁡exp⁡(score(yslot,h))∑y′∈Yexp⁡(score(y′,h))L_{slot} = -\log \frac{\exp(\text{score}(y_{slot}, h))}{\sum_{y' \in \mathcal{Y}} \exp(\text{score}(y', h))}Lslot=logyYexp(score(y,h))exp(score(yslot,h))
其中score(y,h)\text{score}(y, h)score(y,h)是标签序列y的分数:
score(y,h)=∑t=1T(Wytht+byt)+∑t=2TAyt−1,yt\text{score}(y, h) = \sum_{t=1}^T (W_{y_t} h_t + b_{y_t}) + \sum_{t=2}^T A_{y_{t-1}, y_t}score(y,h)=t=1T(Wytht+byt)+t=2TAyt1,yt
WytW_{y_t}Wyt是槽位标签yty_tyt的权重矩阵,bytb_{y_t}byt是偏置项。

3. 联合建模的优势

相对于分开建模,联合建模有三个明显的优势:

  1. 减少误差传递:意图分类和槽位填充共享编码器,两个任务的信息能互相辅助(比如槽位中的「上海」能辅助意图分类为「预订机票」);
  2. 提高数据利用率:同一批数据能同时用于两个任务的训练,减少数据标注的成本;
  3. 提升准确率:根据实验,Joint BERT在SNIPS数据集上的意图分类准确率比分开建模高2-3%,槽位填充的F1-score高3-5%。

四、数学模型与公式:从原理到实践

前面我们讲了很多技术点,现在来深入拆解其中的核心数学模型——这些模型是意图识别的「底层逻辑」,理解它们能帮你更深入地掌握意图识别的技术。

一、Transformer的自注意力机制:理解“上下文”的关键

Transformer是当前所有预训练语言模型的核心,而**自注意力机制(Self-Attention)**是Transformer的灵魂——它让模型能「关注输入中的关键部分」,从而理解上下文语义。

1. 自注意力的定义

自注意力机制的目标是:对于输入序列中的每个token,计算它与其他所有token的「相关性」,然后根据相关性加权求和,得到该token的「上下文向量」。

比如输入序列是「订 明天 去 上海 的 机票」,对于token「订」,自注意力机制会计算它与「明天」「上海」「机票」的相关性,然后加权求和得到「订」的上下文向量——这个向量包含了「订」与其他token的关系(比如「订」与「机票」的相关性最高)。

2. 自注意力的数学公式

自注意力机制的计算过程分为三步:

  1. 生成查询(Query)、键(Key)、值(Value)向量:对于每个token的输入向量xix_ixi,用三个可学习的权重矩阵WQW_QWQWKW_KWKWVW_VWV生成Q、K、V向量:
    qi=xiWQq_i = x_i W_Qqi=xiWQ
    ki=xiWKk_i = x_i W_Kki=xiWK
    vi=xiWVv_i = x_i W_Vvi=xiWV
  2. 计算注意力权重:对于每个token的q向量,与所有token的k向量计算点积,然后用Softmax归一化,得到注意力权重ai,ja_{i,j}ai,j(表示第i个token与第j个token的相关性):
    ai,j=exp⁡(qi⋅kjT/dk)∑k=1nexp⁡(qi⋅kkT/dk)a_{i,j} = \frac{\exp(q_i \cdot k_j^T / \sqrt{d_k})}{\sum_{k=1}^n \exp(q_i \cdot k_k^T / \sqrt{d_k})}ai,j=k=1nexp(qikkT/dk )exp(qikjT/dk )
    其中dkd_kdk是k向量的维度,除以dk\sqrt{d_k}dk 是为了避免点积结果过大,导致Softmax后的梯度消失。
  3. 计算上下文向量:用注意力权重ai,ja_{i,j}ai,j对所有token的v向量加权求和,得到第i个token的上下文向量ziz_izi
    zi=∑j=1nai,jvjz_i = \sum_{j=1}^n a_{i,j} v_jzi=j=1nai,jvj
3. 多头自注意力(Multi-Head Attention)

为了让模型能关注「不同类型的相关性」,Transformer采用了多头自注意力——将自注意力机制重复多次(比如8次、12次),每次生成不同的Q、K、V向量,然后将多个头的结果拼接,得到最终的上下文向量。

多头自注意力的数学公式:
MultiHead(Q,K,V)=Concat(head1,head2,...,headh)WOMultiHead(Q,K,V) = Concat(head_1, head_2,...,head_h) W_OMultiHead(Q,K,V)=Concat(head1,head2,...,headh)WO
其中headi=Attention(QWQi,KWKi,VWVi)head_i = Attention(Q W_{Q_i}, K W_{K_i}, V W_{V_i})headi=Attention(QWQi,KWKi,VWVi)hhh是头的数量,WOW_OWO是可学习的权重矩阵。

比如在BERT中,多头自注意力的头数是12,每个头的维度是64(总维度是768=12×64)——这样模型能同时关注「语法关系」(比如「订」与「机票」的动宾关系)、「语义关系」(比如「明天」与「时间」的关系)等不同类型的相关性。

二、Prompt Learning:让大模型“理解任务”的魔法

在大模型时代,Prompt Learning是「让大模型完成特定任务」的核心方法——它通过「设计提示」,将任务转化为大模型擅长的「文本生成任务」。对于意图识别来说,Prompt Learning能让大模型在Few-shot/Zero-shot的情况下,准确识别意图。

1. Prompt的结构

一个有效的Prompt通常包含三个部分:

  • 指令(Instruction):用自然语言告诉大模型要完成的任务;
  • 示例(Demonstration):给大模型看几个「输入→输出」的例子(Few-shot);
  • 当前输入(Current Input):需要处理的用户输入。

比如意图识别的Prompt:

指令:请识别用户输入的意图,意图标签包括「预订机票」「查询天气」「寻找猫咖」。
示例:用户输入:帮我订明天去上海的机票→意图:预订机票;用户输入:北京明天的天气怎么样→意图:查询天气;用户输入:帮我找一家附近的猫咖→意图:寻找猫咖。
当前输入:帮我找一家能撸猫的咖啡馆→意图:

2. Prompt的数学原理

Prompt Learning的本质是「将任务转化为条件生成任务」——大模型根据Prompt中的指令、示例和当前输入,生成符合要求的输出。

对于意图识别任务,假设Prompt是PPP,当前输入是xxx,意图标签是yyy,则大模型的生成概率是:
P(y∣x,P)=∏t=1TP(yt∣x,P,y1,...,yt−1)P(y|x, P) = \prod_{t=1}^T P(y_t|x, P, y_1,...,y_{t-1})P(yx,P)=t=1TP(ytx,P,y1,...,yt1)
其中yty_tyt是意图标签的第t个token,TTT是意图标签的长度。

比如意图标签是「寻找猫咖」,则y1y_1y1是「寻」,y2y_2y2是「找」,y3y_3y3是「猫」,y4y_4y4是「咖」——大模型会依次生成每个token,直到生成完整的意图标签。

3. Prompt的优化方法

为了提高Prompt的效果,常用的优化方法包括:

  • Prompt Tuning:将Prompt转化为可学习的向量(而不是固定的文本),通过训练调整Prompt向量,让大模型更好地完成任务;
  • Prefix Tuning:在输入前添加一个可学习的「前缀向量」,引导大模型生成正确的输出;
  • Instruction Tuning:用大量的「指令-任务」数据训练大模型,让大模型学会理解各种指令(比如OpenAI的InstructGPT)。

五、项目实战:用Hugging Face实现上下文感知的意图识别

理论讲得再多,不如亲手写一遍代码。接下来,我们用PythonHugging Face Transformers库,实现一个「上下文感知的意图识别系统」——它能处理多轮对话,识别意图并填充槽位。

1. 开发环境搭建

首先,安装需要的库:

pip install transformers torch datasets evaluate
  • transformers:Hugging Face的预训练模型库;
  • torch:PyTorch,用于模型训练;
  • datasets:Hugging Face的数据集库;
  • evaluate:Hugging Face的评估库。

2. 数据准备

我们使用SNIPS MultiWOZ数据集(多轮对话意图识别数据集),它包含多轮对话历史、当前输入、意图标签和槽位标签。

首先,加载数据集:

from datasets import load_dataset

# 加载SNIPS MultiWOZ数据集
dataset = load_dataset("snips_built_in_intents")
# 查看数据集结构
print(dataset)
# 输出:DatasetDict({train: 13084, validation: 700, test: 700})

数据集的每个样本包含以下字段:

  • text:当前用户输入;
  • context:历史对话(列表,每个元素是之前的用户输入和系统回复);
  • intent:意图标签(比如「预订机票」);
  • slots:槽位标签(字典,比如{“出发地”: “北京”, “目的地”: “上海”})。

为了适应模型输入,我们需要将「上下文+当前输入」拼接成一个文本:

def preprocess_function(examples):
    # 拼接上下文和当前输入
    inputs = []
    for context, text in zip(examples["context"], examples["text"]):
        context_str = " ".join([f"用户:{u} 系统:{s}" for u, s in context])
        input_str = f"上下文:{context_str} 当前输入:{text}"
        inputs.append(input_str)
    # 处理槽位标签:将字典转化为序列标签(比如「北京」→「出发地」)
    slot_labels = []
    for slots in examples["slots"]:
        # 假设text已经被分词,这里简化处理
        tokens = examples["text"][examples["slots"].index(slots)].split()
        labels = ["O"] * len(tokens)  # O表示非槽位
        for slot, value in slots.items():
            if value in tokens:
                idx = tokens.index(value)
                labels[idx] = slot
        slot_labels.append(labels)
    return {
        "input_str": inputs,
        "intent": examples["intent"],
        "slot_labels": slot_labels
    }

# 预处理数据集
dataset = dataset.map(preprocess_function, batched=True)

3. 模型构建

我们使用BERT-base-uncased作为编码器,添加两个任务头:意图分类头和槽位填充头。

首先,加载BERT Tokenizer:

from transformers import BertTokenizerFast

tokenizer = BertTokenizerFast.from_pretrained("bert-base-uncased")

然后,定义数据_collator(将样本打包成批次):

import torch
from transformers import DataCollatorForTokenClassification

# 数据_collator:处理意图分类和槽位填充的批次
class IntentSlotDataCollator:
    def __init__(self, tokenizer):
        self.tokenizer = tokenizer
        self.slot_collator = DataCollatorForTokenClassification(tokenizer)
    
    def __call__(self, features):
        # 处理意图分类:提取intent标签
        intent_labels = torch.tensor([f["intent"] for f in features], dtype=torch.long)
        # 处理槽位填充:用DataCollatorForTokenClassification处理slot_labels
        slot_features = [{"input_ids": f["input_ids"], "attention_mask": f["attention_mask"], "labels": f["slot_labels"]} for f in features]
        slot_batch = self.slot_collator(slot_features)
        # 合并批次
        batch = {
            "input_ids": slot_batch["input_ids"],
            "attention_mask": slot_batch["attention_mask"],
            "intent_labels": intent_labels,
            "slot_labels": slot_batch["labels"]
        }
        return batch

collator = IntentSlotDataCollator(tokenizer)

接下来,定义联合模型:

from transformers import BertPreTrainedModel, BertModel
from torch import nn
from transformers.modeling_outputs import TokenClassifierOutput, SequenceClassifierOutput

class JointIntentSlotModel(BertPreTrainedModel):
    def __init__(self, config):
        super().__init__(config)
        self.num_intent_labels = config.num_intent_labels
        self.num_slot_labels = config.num_slot_labels
        
        # BERT编码器
        self.bert = BertModel(config)
        # 意图分类头
        self.intent_classifier = nn.Linear(config.hidden_size, config.num_intent_labels)
        # 槽位填充头
        self.slot_classifier = nn.Linear(config.hidden_size, config.num_slot_labels)
        
        # 初始化权重
        self.init_weights()
    
    def forward(
        self,
        input_ids=None,
        attention_mask=None,
        token_type_ids=None,
        position_ids=None,
        head_mask=None,
        inputs_embeds=None,
        intent_labels=None,
        slot_labels=None,
        output_attentions=None,
        output_hidden_states=None,
        return_dict=None,
    ):
        return_dict = return_dict if return_dict is not None else self.config.use_return_dict
        
        # BERT编码
        outputs = self.bert(
            input_ids,
            attention_mask=attention_mask,
            token_type_ids=token_type_ids,
            position_ids=position_ids,
            head_mask=head_mask,
            inputs_embeds=inputs_embeds,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
        )
        
        # 意图分类:用[CLS]向量
        cls_output = outputs[1]  # [CLS]向量,形状:(batch_size, hidden_size)
        intent_logits = self.intent_classifier(cls_output)  # 形状:(batch_size, num_intent_labels)
        
        # 槽位填充:用token向量
        token_output = outputs[0]  # 所有token的向量,形状:(batch_size, seq_len, hidden_size)
        slot_logits = self.slot_classifier(token_output)  # 形状:(batch_size, seq_len, num_slot_labels)
        
        # 计算损失
        total_loss = None
        if intent_labels is not None and slot_labels is not None:
            # 意图分类损失:交叉熵
            intent_loss_fct = nn.CrossEntropyLoss()
            intent_loss = intent_loss_fct(intent_logits.view(-1, self.num_intent_labels), intent_labels.view(-1))
            # 槽位填充损失:交叉熵(忽略-100标签)
            slot_loss_fct = nn.CrossEntropyLoss(ignore_index=self.config.pad_token_id)
            slot_loss = slot_loss_fct(slot_logits.view(-1, self.num_slot_labels), slot_labels.view(-1))
            # 总损失:加权和
            total_loss = 0.5 * intent_loss + 0.5 * slot_loss
        
        if not return_dict:
            output = (intent_logits, slot_logits) + outputs[2:]
            return ((total_loss,) + output) if total_loss is not None else output
        
        return {
            "loss": total_loss,
            "intent_logits": intent_logits,
            "slot_logits": slot_logits,
            "hidden_states": outputs.hidden_states,
            "attentions": outputs.attentions,
        }

然后,加载预训练模型并设置参数:

from transformers import BertConfig

# 配置模型参数
config = BertConfig.from_pretrained("bert-base-uncased")
config.num_intent_labels = len(dataset["train"].features["intent"].names)  # 意图标签数量
config.num_slot_labels = len(dataset["train"].features["slot_labels"].feature.names)  # 槽位标签数量
config.pad_token_id = tokenizer.pad_token_id  # 填充token的ID

# 初始化模型
model = JointIntentSlotModel.from_pretrained("bert-base-uncased", config=config)

4. 训练与评估

首先,定义评估指标:

  • 意图分类:准确率(Accuracy);
  • 槽位填充:F1-score(序列标注的常用指标)。
import evaluate

# 加载评估指标
intent_metric = evaluate.load("accuracy")
slot_metric = evaluate.load("seqeval")

# 定义计算指标的函数
def compute_metrics(p):
    intent_logits, slot_logits = p.predictions
    intent_labels = p.label_ids[0]
    slot_labels = p.label_ids[1]
    
    # 计算意图分类准确率
    intent_preds = torch.argmax(torch.tensor(intent_logits), dim=1)
    intent_accuracy = intent_metric.compute(predictions=intent_preds, references=intent_labels)["accuracy"]
    
    # 计算槽位填充F1-score
    slot_preds = torch.argmax(torch.tensor(slot_logits), dim=2)
    # 将标签中的-100(填充)替换为O标签
    slot_labels = [[label for label in seq if label != -100] for seq in slot_labels]
    slot_preds = [[pred for pred, label in zip(seq, labels) if label != -100] for seq, labels in zip(slot_preds, slot_labels)]
    # 将标签ID转化为标签名称
    slot_labels_names = dataset["train"].features["slot_labels"].feature.names
    slot_labels = [[slot_labels_names[label] for label in seq] for seq in slot_labels]
    slot_preds = [[slot_labels_names[pred] for pred in seq] for seq in slot_preds]
    slot_results = slot_metric.compute(predictions=slot_preds, references=slot_labels)
    
    return {
        "intent_accuracy": intent_accuracy,
        "slot_f1": slot_results["overall_f1"],
        "slot_precision": slot_results["overall_precision"],
        "slot_recall": slot_results["overall_recall"],
    }

然后,设置训练参数并启动训练:

from transformers import Trainer, TrainingArguments

# 训练参数
training_args = TrainingArguments(
    output_dir="./joint-intent-slot-model",
    evaluation_strategy="epoch",
    learning_rate=2e-5,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    num_train_epochs=3,
    weight_decay=0.01,
    logging_steps=10,
    save_strategy="epoch",
    load_best_model_at_end=True,
)

# 初始化Trainer
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    eval_dataset=dataset["validation"],
    data_collator=collator,
    compute_metrics=compute_metrics,
)

# 开始训练
trainer.train()

5. 推理测试

训练完成后,我们用模型进行推理测试:

def predict_intent_slot(context, text):
    # 拼接上下文和当前输入
    context_str = " ".join([f"用户:{u} 系统:{s}" for u, s in context])
    input_str = f"上下文:{context_str} 当前输入:{text}"
    # Tokenize输入
    inputs = tokenizer(input_str, return_tensors="pt")
    # 模型推理
    outputs = model(**inputs)
    # 预测意图
    intent_logits = outputs["intent_logits"]
    intent_pred = torch.argmax(intent_logits, dim=1).item()
    intent_label = dataset["train"].features["intent"].names[intent_pred]
    # 预测槽位
    slot_logits = outputs["slot_logits"]
    slot_pred = torch.argmax(slot_logits, dim=2).squeeze().tolist()
    # 将Token ID转化为文本
    tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"].squeeze().tolist())
    # 过滤填充token和特殊token
    slot_labels = []
    for token, pred in zip(tokens, slot_pred):
        if token not in [tokenizer.cls_token, tokenizer.sep_token, tokenizer.pad_token]:
            slot_label = dataset["train"].features["slot_labels"].feature.names[pred]
            slot_labels.append((token, slot_label))
    # 提取槽位值
    slots = {}
    for token, label in slot_labels:
        if label != "O":
            slots[label] = token
    return {
        "intent": intent_label,
        "slots": slots
    }

# 测试多轮对话
context = [("我想订明天去上海的机票", "好的,请问出发地是哪里?")]
text = "北京"
result = predict_intent_slot(context, text)
print(result)
# 输出:{"intent": "预订机票", "slots": {"出发地": "北京", "目的地": "上海", "时间": "明天"}}

6. 结果分析

从测试结果可以看到,模型正确识别了「预订机票」的意图,并提取了「出发地=北京」「目的地=上海」「时间=明天」的槽位——这说明模型具备上下文感知的能力,能利用历史对话中的信息(「明天去上海的机票」)来填充当前输入的槽位。

六、实际应用场景:意图识别如何赋能AI原生应用

意图识别不是「为了技术而技术」,它的价值在于「赋能AI原生应用」。接下来,我们看几个典型的应用场景:

1. 智能开发助手:Copilot的“代码意图”理解

GitHub Copilot是AI原生开发工具的代表,它的核心能力是「理解开发者的自然语言指令,生成对应的代码」。比如:

  • 开发者输入:「帮我写一个Python的快速排序函数」;
  • Copilot识别出意图:「生成快速排序代码」;
  • 提取槽位:「语言=Python」「算法=快速排序」;
  • 生成代码:python def quicksort(arr):...

Copilot的意图识别采用了大模型的Prompt Learning——将开发者的指令作为Prompt,引导大模型生成代码。比如Prompt是:「用户输入:帮我写一个Python的快速排序函数→代码:def quicksort(arr):…」。

2. AI插件系统:ChatGPT的“工具调用”意图识别

ChatGPT的插件系统允许ChatGPT调用外部工具(比如飞猪、12306、维基百科),而意图识别是「决定是否调用工具、调用哪个工具」的关键。比如:

  • 用户输入:「帮我查一下明天北京到上海的机票价格」;
  • ChatGPT识别出意图:「查询机票价格」;
  • 提取槽位:「出发地=北京」「目的地=上海」「时间=明天」;
  • 调用飞猪插件:传递槽位参数,获取机票价格并返回给用户。

ChatGPT的意图识别采用了Zero-shot Learning——通过「工具的描述」来识别意图。比如飞猪插件的描述是「用于查询和预订机票」,ChatGPT通过比较用户输入的语义与工具描述的语义,决定调用飞猪插件。

3. 多模态AI应用:MidJourney的“图像生成

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐