
意图识别是自然语言处理里最核心、最落地的任务之一,不管是智能客服、智能助手,还是语音交互、智能设备控制,都离不开它。之前我们已经系统讲过意图识别的评估指标、基本原理和整体评测流程,也用语义向量模型(all-MiniLM-L6-v2)和轻量级大模型Qwen1.5-1.8B-Chat做过一版实测。但在实际跑通代码、对比结果后我们发现:Qwen1.5-1.8B 受限于参数量,在复杂句式、混合意图、模糊表达上的识别能力明显偏弱,稳定性也不够理想。于是我们进一步引入ChatGLM3-6B做联合评测,用更大体量、更强语义理解能力的模型做对照。
还真是个有意思的事情,这一轮完整对比下来,我们能非常直观地看到:模型体量不同,意图识别的效果差异真的非常大。今天我们就基于这三类真实模型,从准确率、召回率、F1 值、响应时间等维度,完整拆解意图识别系统的评测逻辑,做个有趣的实验,同时让大家直观的理解小模型和大模型在实际业务里的差距。

意图识别是指让机器从用户的自然语言输入中,精准判断用户的核心诉求,即意图。比如:
在大模型出现之前,传统意图识别系统多基于规则、机器学习(如SVM、逻辑回归)实现;而大模型凭借强大的语义理解能力,能处理更复杂、更口语化的输入,但其效果依然需要科学的指标来衡量,没有评估的系统优化,就像 闭着眼睛开车,无法判断方向是否正确。
评估指标的核心价值体现,将模棱两可的表述量化显示,达到更直观理解的目的;
在讲解指标前,先明确几个基础概念,这是理解准确率、召回率的关键:
混淆矩阵是展示分类结果的核心工具,对于意图识别二分类场景,比如“是否为查询机票意图”,混淆矩阵包含 4 个核心维度:
举个具体例子:
假设我们有100条用户输入,其中实际“查询机票”的有 30 条,“非查询机票”的有 70 条。系统识别结果如下:
对应的混淆矩阵如下:
预测:查询机票 | 预测:非查询机票 | |
|---|---|---|
实际:查询机票 | 25(TP) | 5(FN) |
实际:非查询机票 | 5(FP) | 65(TN) |
对于多意图场景,比如系统需要识别“查询机票”、“取消订单”、“咨询退货”3种意图,混淆矩阵会变成 3×3 的形式,核心逻辑不变,对角线为正确识别的数量,非对角线为错误识别的数量。
为了让你更清晰地理解不同指标的适用场景,整理如下:
指标 | 核心关注 | 适用场景 | 缺点 |
|---|---|---|---|
准确率 | 减少误判 | 误判代价高(如投诉意图识别) | 忽略漏判,样本不均衡时参考价值低 |
召回率 | 减少漏判 | 漏判代价高(如紧急救援意图识别) | 忽略误判,可能导致大量错误识别 |
F1 值 | 平衡误判和漏判 | 大多数通用场景(如智能客服意图识别) | 无法反映响应速度等工程指标 |
响应时间 | 系统效率 | 高并发、实时交互场景(如语音助手) | 仅反映速度,不反映识别准确性 |
评估的基础实际是从样本到指标的推导,意图识别系统的评估本质是“用标注好的测试集,验证系统输出与真实标签的匹配程度”;
测试集是评估的基础,必须满足以下要求:
与传统模型相比,大模型的评估流程多了“模型部署/调用”环节,完整流程如下:

2.1.1 测试集准备的细节
2.1.2 大模型部署的细节
2.1.3 推理执行的细节
2.1.4 指标分析的细节
import os
import torch
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sentence_transformers import SentenceTransformer
from transformers import AutoTokenizer, AutoModelForCausalLM
from modelscope import snapshot_download
import torch.nn.functional as F
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
# 全局配置
CACHE_DIR = "D:\\modelscope\\hub"
os.environ["MODELSCOPE_CACHE"] = CACHE_DIR
# 设置设备(优先GPU)
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
print(f"使用设备:{DEVICE}")
def build_annotated_test_set():
"""
构建标注好的意图识别测试集(真实场景样本)
:return: DataFrame,包含text、true_intent列
"""
# 标注好的测试样本(覆盖不同表达形式)
test_data = [
# 问候意图
{"text": "你好呀,今天过得怎么样", "true_intent": "问候"},
{"text": "嗨,好久不见", "true_intent": "问候"},
{"text": "早上好,麻烦问个问题", "true_intent": "问候"},
{"text": "大家好,我是新来的", "true_intent": "问候"},
{"text": "哈喽,在吗", "true_intent": "问候"},
# 天气查询意图
{"text": "今天外面会不会下雨", "true_intent": "天气查询"},
{"text": "明天的气温大概是多少度", "true_intent": "天气查询"},
{"text": "出门需要带伞吗", "true_intent": "天气查询"},
{"text": "未来三天的天气预报怎么样", "true_intent": "天气查询"},
{"text": "现在外面冷不冷", "true_intent": "天气查询"},
# 计算意图
{"text": "帮我算一下5乘以8等于多少", "true_intent": "计算"},
{"text": "100减去25是多少", "true_intent": "计算"},
{"text": "9除以3的结果是啥", "true_intent": "计算"},
{"text": "12加18等于几", "true_intent": "计算"},
{"text": "算一下平方:5的平方是多少", "true_intent": "计算"},
# 翻译意图
{"text": "把“我爱中国”翻译成英文", "true_intent": "翻译"},
{"text": "“Hello World”的中文意思是什么", "true_intent": "翻译"},
{"text": "帮我翻译一句日语:こんにちは", "true_intent": "翻译"},
{"text": "“谢谢”的英文怎么说", "true_intent": "翻译"},
{"text": "翻译“明天见”成法语", "true_intent": "翻译"},
# 告别意图
{"text": "再见,下次再聊", "true_intent": "告别"},
{"text": "我先撤了,拜拜", "true_intent": "告别"},
{"text": "晚安,早点休息", "true_intent": "告别"},
{"text": "就到这里吧,再见", "true_intent": "告别"},
{"text": "回见,祝你愉快", "true_intent": "告别"},
# 混合意图(测试边界)
{"text": "你好,帮我算一下10加20", "true_intent": "计算"},
{"text": "再见,顺便问下明天天气", "true_intent": "天气查询"},
{"text": "帮我翻译“谢谢”,早上好", "true_intent": "翻译"},
]
df = pd.DataFrame(test_data)
# 重置索引,方便后续处理
df = df.reset_index(drop=True)
return df
# 构建测试集
test_df = build_annotated_test_set()
print("标注测试集构建完成,共{}条样本".format(len(test_df)))
print("测试集前5条:")
print(test_df.head())输出结果:
标注测试集构建完成,共28条样本 测试集前5条: text true_intent 0 你好呀,今天过得怎么样 问候 1 嗨,好久不见 问候 2 早上好,麻烦问个问题 问候 3 大家好,我是新来的 问候 4 哈喽,在吗 问候
# 大模型的意图列表
INTENT_LIST = ["问候", "天气查询", "计算", "翻译", "告别"]
def plot_evaluation_results(semantic_metrics, qwen_metrics, eval_df):
"""
可视化评估结果(真实模型版)
"""
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 混淆矩阵对比图
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# 语义模型混淆矩阵
sns.heatmap(
semantic_metrics["confusion_matrix"],
annot=True,
fmt="d",
cmap="Blues",
xticklabels=INTENT_LIST,
yticklabels=INTENT_LIST,
ax=ax1
)
ax1.set_title("Qwen1.5-1.8B 语义向量模型混淆矩阵")
ax1.set_xlabel("预测意图")
ax1.set_ylabel("真实意图")
# Qwen模型混淆矩阵
sns.heatmap(
qwen_metrics["confusion_matrix"],
annot=True,
fmt="d",
cmap="Greens",
xticklabels=INTENT_LIST,
yticklabels=INTENT_LIST,
ax=ax2
)
ax2.set_title("Qwen1.5-1.8B 混淆矩阵")
ax2.set_xlabel("预测意图")
ax2.set_ylabel("真实意图")
plt.tight_layout()
plt.savefig("Qwen1.5-1.8B 混淆矩阵 confusion_matrix_comparison.png", dpi=300, bbox_inches='tight')
plt.show()输出图示:

def calculate_intent_metrics(eval_df, true_col, pred_col, intent_list):
"""
计算意图识别的核心指标(多分类场景)
:param eval_df: 评估结果DataFrame
:param true_col: 真实意图列名
:param pred_col: 预测意图列名
:param intent_list: 意图列表
:return: 详细指标字典
"""
# 1. 构建混淆矩阵
cm = confusion_matrix(
eval_df[true_col],
eval_df[pred_col],
labels=intent_list
)
# 2. 计算每个意图的精准率、召回率、F1值
per_intent_metrics = {}
for i, intent in enumerate(intent_list):
# 提取该意图的TP、FP、FN
TP = cm[i, i]
FP = cm[:, i].sum() - TP # 所有行的第i列之和 - TP
FN = cm[i, :].sum() - TP # 第i行的所有列之和 - TP
TN = cm.sum() - TP - FP - FN
# 计算指标(避免除以0)
precision = TP / (TP + FP) if (TP + FP) > 0 else 0.0
recall = TP / (TP + FN) if (TP + FN) > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
per_intent_metrics[intent] = {
"TP": TP,
"FP": FP,
"FN": FN,
"TN": TN,
"Precision": round(precision, 4),
"Recall": round(recall, 4),
"F1": round(f1, 4)
}
# 3. 计算宏平均和微平均
# 宏平均:先算每个意图的指标,再平均
macro_precision = np.mean([m["Precision"] for m in per_intent_metrics.values()])
macro_recall = np.mean([m["Recall"] for m in per_intent_metrics.values()])
macro_f1 = np.mean([m["F1"] for m in per_intent_metrics.values()])
# 微平均:先汇总所有TP/FP/FN,再计算
total_TP = sum([m["TP"] for m in per_intent_metrics.values()])
total_FP = sum([m["FP"] for m in per_intent_metrics.values()])
total_FN = sum([m["FN"] for m in per_intent_metrics.values()])
micro_precision = total_TP / (total_TP + total_FP) if (total_TP + total_FP) > 0 else 0.0
micro_recall = total_TP / (total_TP + total_FN) if (total_TP + total_FN) > 0 else 0.0
micro_f1 = 2 * micro_precision * micro_recall / (micro_precision + micro_recall) if (micro_precision + micro_recall) > 0 else 0.0
# 4. 计算整体准确率
accuracy = (eval_df[true_col] == eval_df[pred_col]).sum() / len(eval_df)
# 5. 计算响应时间指标(如果包含)
rt_col = pred_col.replace("_intent", "_response_time_ms")
if rt_col in eval_df.columns:
rt_metrics = {
"avg_response_time_ms": round(eval_df[rt_col].mean(), 2),
"p95_response_time_ms": round(np.percentile(eval_df[rt_col], 95), 2),
"max_response_time_ms": round(eval_df[rt_col].max(), 2),
"min_response_time_ms": round(eval_df[rt_col].min(), 2)
}
else:
rt_metrics = {}
return {
"confusion_matrix": cm,
"per_intent": per_intent_metrics,
"macro": {
"Precision": round(macro_precision, 4),
"Recall": round(macro_recall, 4),
"F1": round(macro_f1, 4)
},
"micro": {
"Precision": round(micro_precision, 4),
"Recall": round(micro_recall, 4),
"F1": round(micro_f1, 4)
},
"accuracy": round(accuracy, 4),
"response_time": rt_metrics
}
# 计算语义向量模型的指标
semantic_metrics = calculate_intent_metrics(
eval_df,
true_col="true_intent",
pred_col="semantic_intent",
intent_list=INTENT_LIST
)
# 计算Qwen大模型的指标
qwen_metrics = calculate_intent_metrics(
eval_df,
true_col="true_intent",
pred_col="qwen_intent",
intent_list=INTENT_LIST
)
# 打印指标结果
print("\n===== 语义向量模型评估指标 =====")
print(f"整体准确率:{semantic_metrics['accuracy']:.2%}")
print(f"宏平均F1值:{semantic_metrics['macro']['F1']:.2%}")
print(f"微平均F1值:{semantic_metrics['micro']['F1']:.2%}")
print(f"平均响应时间:{semantic_metrics['response_time']['avg_response_time_ms']} ms")
print(f"95分位响应时间:{semantic_metrics['response_time']['p95_response_time_ms']} ms")
print("\n===== Qwen大模型评估指标 =====")
print(f"整体准确率:{qwen_metrics['accuracy']:.2%}")
print(f"宏平均F1值:{qwen_metrics['macro']['F1']:.2%}")
print(f"微平均F1值:{qwen_metrics['micro']['F1']:.2%}")
print(f"平均响应时间:{qwen_metrics['response_time']['avg_response_time_ms']} ms")
print(f"95分位响应时间:{qwen_metrics['response_time']['p95_response_time_ms']} ms")
# 使用之前计算的语义和Qwen指标(semantic_metrics和qwen_metrics已在前面计算)
print("\n" + "="*50)
print(" 对比结果摘要")
print("="*50)
print(f"{'模型':<15} | {'准确率':<8} | {'Macro F1':<8} | {'平均耗时(ms)':<12}")
print("-" * 55)
print(f"{'语义向量':<15} | {semantic_metrics['accuracy']:.2%} | {semantic_metrics['macro']['F1']:.2f} | {semantic_metrics['response_time']['avg_response_time_ms']:.2f}")
print(f"{'Qwen1.5-1.8B':<15} | {qwen_metrics['accuracy']:.2%} | {qwen_metrics['macro']['F1']:.2f} | {qwen_metrics['response_time']['avg_response_time_ms']:.2f}")
print("="*50)
# 打印各意图详细指标
print("\n各意图详细指标(语义向量模型):")
print(f"{'意图':<12} | {'精准率':<10} | {'召回率':<10} | {'F1值':<10} | {'TP':<8} | {'FP':<8}| {'FN':<8}")
print("-" * 50)
for intent, metrics in semantic_metrics["per_intent"].items():
print(f"{intent:<12} | {metrics['Precision']:.2%} | {metrics['Recall']:.2%} | {metrics['F1']:.2%} | {metrics['TP']:<8} | {metrics['FP']:<8} | {metrics['FN']:<8}")
print("\n各意图详细指标(Qwen1.5-1.8B):")
print(f"{'意图':<12} | {'精准率':<10} | {'召回率':<10} | {'F1值':<10} | {'TP':<8} | {'FP':<8}| {'FN':<8}")
print("-" * 50)
for intent, metrics in qwen_metrics["per_intent"].items():
print(f"{intent:<12} | {metrics['Precision']:.2%} | {metrics['Recall']:.2%} | {metrics['F1']:.2%} | {metrics['TP']:<8} | {metrics['FP']:<8} | {metrics['FN']:<8}")输出结果:
===== 语义向量模型评估指标 ===== 整体准确率:75.00% 宏平均F1值:73.80% 微平均F1值:75.00% 平均响应时间:197.78 ms 95分位响应时间:221.68 ms ===== Qwen大模型评估指标 ===== 整体准确率:21.43% 宏平均F1值:7.27% 微平均F1值:22.22% 平均响应时间:9722.06 ms 95分位响应时间:10861.83 ms ================================================== 对比结果摘要 ================================================== 模型 | 准确率 | Macro F1 | 平均耗时(ms) ------------------------------------------------------------------- 语义向量 | 75.00% | 0.74 | 197.78 Qwen1.5-1.8B | 21.43% | 0.07 | 9722.06 ================================================== 各意图详细指标(语义向量模型): 意图 | 精准率 | 召回率 | F1值 | TP | FP | FN ------------------------------------------------------------------------------------------- 问候 | 80.00% | 80.00% | 80.00% | 4 | 1 | 1 天气查询 | 55.56% | 83.33% | 66.67% | 5 | 4 | 1 计算 | 85.71% | 100.00% | 92.31% | 6 | 1 | 0 翻译 | 100.00% | 66.67% | 80.00% | 4 | 0 | 2 告别 | 66.67% | 40.00% | 50.00% | 2 | 1 | 3 各意图详细指标(Qwen1.5-1.8B): 意图 | 精准率 | 召回率 | F1值 | TP | FP | FN -------------------------------------------------------------------------------------------- 问候 | 0.00% | 0.00% | 0.00% | 0 | 0 | 5 天气查询 | 22.22% | 100.00% | 36.36% | 6 | 21 | 0 计算 | 0.00% | 0.00% | 0.00% | 0 | 0 | 5 翻译 | 0.00% | 0.00% | 0.00% | 0 | 0 | 6 告别 | 0.00% | 0.00% | 0.00% | 0 | 0 | 5
# 5. 计算响应时间指标(如果包含)
rt_col = pred_col.replace("_intent", "_response_time_ms")
if rt_col in eval_df.columns:
rt_metrics = {
"avg_response_time_ms": round(eval_df[rt_col].mean(), 2),
"p95_response_time_ms": round(np.percentile(eval_df[rt_col], 95), 2),
"max_response_time_ms": round(eval_df[rt_col].max(), 2),
"min_response_time_ms": round(eval_df[rt_col].min(), 2)
}
else:
rt_metrics = {}
# 2. 响应时间对比图
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# 响应时间分布直方图
ax1.hist(eval_df["semantic_response_time_ms"], bins=10, alpha=0.7, label="语义向量模型", color="blue")
ax1.hist(eval_df["qwen_response_time_ms"], bins=10, alpha=0.7, label="Qwen大模型", color="green")
ax1.axvline(eval_df["semantic_response_time_ms"].mean(), color="blue", linestyle="--", label=f"语义模型均值:{eval_df['semantic_response_time_ms'].mean():.2f}ms")
ax1.axvline(eval_df["qwen_response_time_ms"].mean(), color="green", linestyle="--", label=f"Qwen模型均值:{eval_df['qwen_response_time_ms'].mean():.2f}ms")
ax1.set_xlabel("响应时间(ms)")
ax1.set_ylabel("样本数")
ax1.set_title("Qwen1.5-1.8B 响应时间分布对比")
ax1.legend()
ax1.grid(alpha=0.3)
# 各意图平均响应时间对比
intent_rt_semantic = eval_df.groupby("true_intent")["semantic_response_time_ms"].mean()
intent_rt_qwen = eval_df.groupby("true_intent")["qwen_response_time_ms"].mean()
x = np.arange(len(INTENT_LIST))
width = 0.35
ax2.bar(x - width/2, intent_rt_semantic[INTENT_LIST], width, label="语义向量模型", color="blue")
ax2.bar(x + width/2, intent_rt_qwen[INTENT_LIST], width, label="Qwen大模型", color="green")
ax2.set_xlabel("意图类型")
ax2.set_ylabel("平均响应时间(ms)")
ax2.set_title("Qwen1.5-1.8B 各意图平均响应时间对比")
ax2.set_xticks(x)
ax2.set_xticklabels(INTENT_LIST)
ax2.legend()
ax2.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("Qwen1.5-1.8B 各意图平均响应时间对比 response_time_comparison.png", dpi=300, bbox_inches='tight')
plt.show()输出图示:

# 3. F1值对比图
fig, ax = plt.subplots(figsize=(12, 6))
# 提取各意图的F1值
semantic_f1 = [semantic_metrics["per_intent"][intent]["F1"] for intent in INTENT_LIST]
qwen_f1 = [qwen_metrics["per_intent"][intent]["F1"] for intent in INTENT_LIST]
x = np.arange(len(INTENT_LIST))
width = 0.35
ax.bar(x - width/2, semantic_f1, width, label="语义向量模型", color="blue")
ax.bar(x + width/2, qwen_f1, width, label="Qwen大模型", color="green")
# 添加数值标签
for i, v in enumerate(semantic_f1):
ax.text(i - width/2, v + 0.02, f"{v:.2f}", ha="center", fontsize=9)
for i, v in enumerate(qwen_f1):
ax.text(i + width/2, v + 0.02, f"{v:.2f}", ha="center", fontsize=9)
ax.set_xlabel("意图类型")
ax.set_ylabel("F1值")
ax.set_title("Qwen1.5-1.8B 各意图F1值对比")
ax.set_xticks(x)
ax.set_xticklabels(INTENT_LIST)
ax.set_ylim(0, 1.1)
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("Qwen1.5-1.8B 各意图F1值对比 f1_score_comparison.png", dpi=300, bbox_inches='tight')
plt.show()输出图示:

1.6.1 Qwen1.5-1.8B 逐样本预测结果对比(绿色=正确,红色=错误)

1.6.2 Qwen1.5-1.8B 逐样本详细对比表

综合以上我们发现,Qwen1.5-1.8B 受限于参数量,在复杂句式、混合意图、模糊表达上的识别能力明显偏弱,稳定性也不够理想。于是我们进一步引入ChatGLM3-6B做联合评测,用更大体量、更强语义理解能力的模型做对照。

===== 语义向量模型评估指标 ===== 整体准确率:67.86% 宏平均F1值:64.31% 微平均F1值:67.86% 平均响应时间:77.34 ms 95分位响应时间:68.25 ms ===== ChatGLM3大模型评估指标 ===== 整体准确率:96.43% 宏平均F1值:100.00% 微平均F1值:100.00% 平均响应时间:5278.32 ms 95分位响应时间:5961.11 ms ================================================== 对比结果摘要 ================================================== 模型 | 准确率 | Macro F1 | 平均耗时(ms) -------------------------------------------------------------------------------------- 语义向量 | 67.86% | 0.64 | 77.34 ChatGLM3-6B | 96.43% | 1.00 | 5278.32 ================================================== 各意图详细指标(语义向量模型): 意图 | 精准率 | 召回率 | F1值 | TP | FP | FN --------------------------------------------------------------------------------------- 问候 | 60.00% | 60.00% | 60.00% | 300.00% | 200.00% | 200.00% 天气查询 | 50.00% | 83.33% | 62.50% | 500.00% | 500.00% | 100.00% 计算 | 75.00% | 100.00% | 85.71% | 600.00% | 200.00% | 0.00% 翻译 | 100.00% | 66.67% | 80.00% | 400.00% | 0.00% | 200.00% 告别 | 100.00% | 20.00% | 33.33% | 100.00% | 0.00% | 400.00% 各意图详细指标(ChatGLM3-6B): 意图 | 精准率 | 召回率 | F1值 | TP | FP | FN -------------------------------------------------------------------------------------------------------------- 问候 | 100.00% | 100.00% | 100.00% | 500.00% | 0.00% | 0.00% 天气查询 | 100.00% | 100.00% | 100.00% | 600.00% | 0.00% | 0.00% 计算 | 100.00% | 100.00% | 100.00% | 600.00% | 0.00% | 0.00% 翻译 | 100.00% | 100.00% | 100.00% | 500.00% | 0.00% | 0.00% 告别 | 100.00% | 100.00% | 100.00% | 500.00% | 0.00% | 0.00%


2.5.1 ChatGLM3-6B 逐样本预测结果对比(绿色=正确,红色=错误)

2.5.2 ChatGLM3-6B 逐样本详细对比表

这次测试还是挺有戏剧性的,本来小任务计划启用轻量级特种兵,没想到结果反倒是出乎意料,这说明模型太小有时候连指令都听不懂,反观 ChatGLM3-6B,虽然个头大点,但脑子清楚,干活利索,反而比那个小模型快了一倍。抛开模型本身的能力问题,可能Prompt也是很大的原因,但总的来说,结合向量模型一起,混合架构才最具优势:最完美的方案其实是“向量模型兜底 + 大模型攻坚”,先用向量模型快速过一遍,置信度高的直接返回,置信度低的、复杂的、向量模型搞不定的,扔给 ChatGLM3-6B 处理,这样既有了速度,又有了精度。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。