1. 意图识别评测为什么值得认真做一遍意图识别说白了就是让机器听懂用户到底想干什么。用户说“帮我查一下明天去北京的机票”系统要能判断出这是“查询机票”用户说“我要取消刚才下的订单”系统要能判断出这是“取消订单”。这件事看起来简单但它是智能客服、语音助手、智能设备控制这些场景的第一道关卡识别错了后面全白搭。我之前用轻量级模型 Qwen1.5-1.8B-Chat 跑过一版意图识别实测结果不太理想。受限于参数量它在复杂句式、混合意图、模糊表达上的识别能力明显偏弱稳定性也不够。于是我又引入了 ChatGLM3-6B 做联合评测用更大体量、更强语义理解能力的模型做对照。这一轮完整对比下来模型体量不同带来的效果差异非常直观。这篇文章会交付一套可复制的评测配置骨架包括用 TaoToken 统一 Key 和 API 通道接入 settings.json 或 config.toml 的示例以及逐项验证动作。你可以跟着步骤复现 Qwen1.5 和 ChatGLM3 在意图识别任务上的差异结论。适合正在做模型选型、意图识别系统搭建、或者想了解不同体量模型实际表现差异的开发者。2. TaoToken 前置准备统一 Key 与 API 通道2.1 为什么评测场景需要统一通道做联合评测时最烦的事情之一是每个模型都要单独配一套 API Key 和调用地址。Qwen1.5 一套、ChatGLM3 一套切换模型时改代码改到怀疑人生。TaoToken 提供的是 OpenAI 兼容的统一 API 通道一个 Key 就能调用多个模型评测脚本里只需要改模型名参数不用动调用逻辑。TaoToken 的 API 地址是https://taotoken.net/api官网是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。你可以在官网注册后拿到 API Key然后在控制台里管理 Key 和查看用量。2.2 获取 API Key 的步骤打开官网后进入控制台找到 API Keys 管理页面创建一个新的 Key。建议给这个 Key 起个容易识别的名字比如“intent-eval”方便后续在评测项目里区分。创建完成后把 Key 复制出来后面配置里要用。如果你需要查看模型列表和详细接入文档可以在官网的文档页面找到对应说明。模型对话功能可以用来快速验证 Key 是否正常工作不用写代码就能测试。2.3 配置文件接入示例评测项目里我习惯把配置抽出来避免硬编码。下面分别给出 settings.json 和 config.toml 两种格式的示例你可以根据自己的项目习惯选一种。settings.json 示例{ taotoken: { api_key: sk-your-key-here, base_url: https://taotoken.net/api, timeout: 60, max_retries: 3 }, models: { qwen: { name: Qwen1.5-1.8B-Chat, temperature: 0.0, max_tokens: 50 }, chatglm: { name: ChatGLM3-6B, temperature: 0.0, max_tokens: 50 } }, eval: { intent_list: [问候, 天气查询, 计算, 翻译, 告别], test_set_path: ./data/intent_test.jsonl, output_dir: ./results } }config.toml 示例[taotoken] api_key sk-your-key-here base_url https://taotoken.net/api timeout 60 max_retries 3 [models.qwen] name Qwen1.5-1.8B-Chat temperature 0.0 max_tokens 50 [models.chatglm] name ChatGLM3-6B temperature 0.0 max_tokens 50 [eval] intent_list [问候, 天气查询, 计算, 翻译, 告别] test_set_path ./data/intent_test.jsonl output_dir ./results注意api_key 不要提交到代码仓库建议用环境变量覆盖或者放在 .gitignore 里排除的本地配置文件中。3. 可复制的评测配置骨架3.1 测试集构建评测的基础是标注好的测试集。每条样本要有明确的真实意图标签覆盖所有核心意图并且各意图的样本比例要接近真实场景。下面是一个覆盖 5 类意图的测试集构建代码包含口语化输入和混合意图的边界样本。import json import pandas as pd def build_annotated_test_set(): 构建标注好的意图识别测试集 覆盖问候、天气查询、计算、翻译、告别 5 类意图 包含口语化表达和混合意图边界样本 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(dropTrue) return df test_df build_annotated_test_set() print(f标注测试集构建完成共 {len(test_df)} 条样本) print(test_df.head())3.2 统一调用封装下面这个封装函数通过 TaoToken 的统一通道调用模型你只需要传入模型名和 prompt不用关心底层是哪个厂商的接口。import os import time import json import requests class TaoTokenClient: def __init__(self, api_keyNone, base_urlhttps://taotoken.net/api): self.api_key api_key or os.environ.get(TAOTOKEN_API_KEY) self.base_url base_url.rstrip(/) self.headers { Authorization: fBearer {self.api_key}, Content-Type: application/json } def chat(self, model, messages, temperature0.0, max_tokens50): 统一对话接口兼容 OpenAI 格式 返回 (content, elapsed_ms) url f{self.base_url}/v1/chat/completions payload { model: model, messages: messages, temperature: temperature, max_tokens: max_tokens } start time.time() resp requests.post(url, headersself.headers, jsonpayload, timeout60) elapsed_ms (time.time() - start) * 1000 resp.raise_for_status() data resp.json() content data[choices][0][message][content].strip() return content, elapsed_ms3.3 意图识别 Prompt 模板Prompt 设计对意图识别结果影响很大。我用的模板把意图列表和输出格式都写清楚要求模型只输出意图标签避免多余解释干扰解析。INTENT_LIST [问候, 天气查询, 计算, 翻译, 告别] INTENT_PROMPT_TEMPLATE 你是一个意图识别系统。请判断下面用户输入属于哪个意图。 可选意图{intent_list} 要求 1. 只输出一个意图标签不要输出任何其他内容 2. 如果无法判断输出未知 用户输入{user_input} 意图 def build_intent_prompt(user_input, intent_listINTENT_LIST): return INTENT_PROMPT_TEMPLATE.format( intent_list、.join(intent_list), user_inputuser_input )3.4 批量评测执行把测试集逐条送入模型记录预测意图和响应时间最后汇总成 DataFrame。def run_evaluation(client, model_name, test_df, intent_listINTENT_LIST): 对测试集逐条执行意图识别返回带预测结果的 DataFrame results [] for idx, row in test_df.iterrows(): prompt build_intent_prompt(row[text], intent_list) messages [{role: user, content: prompt}] try: pred, elapsed_ms client.chat(model_name, messages) # 清理输出只保留意图标签 pred pred.strip().split(\n)[0].strip() if pred not in intent_list: pred 未知 except Exception as e: pred 错误 elapsed_ms -1 print(f[{idx}] 调用失败{e}) results.append({ text: row[text], true_intent: row[true_intent], pred_intent: pred, response_time_ms: round(elapsed_ms, 2) }) return pd.DataFrame(results)4. 验证请求与成功结果4.1 先跑一条单样本验证在批量评测之前先用一条样本确认通道正常。这一步能快速排除 Key 配置错误、网络不通等问题。client TaoTokenClient(api_keysk-your-key-here) test_input 帮我算一下5乘以8等于多少 prompt build_intent_prompt(test_input) messages [{role: user, content: prompt}] content, elapsed client.chat(Qwen1.5-1.8B-Chat, messages) print(f输入{test_input}) print(f预测意图{content}) print(f耗时{elapsed:.2f} ms)如果配置正确你会看到类似这样的输出输入帮我算一下5乘以8等于多少 预测意图计算 耗时3200.45 ms4.2 批量评测与指标计算确认单条请求正常后跑完整测试集。下面是指标计算函数覆盖准确率、召回率、F1 值以及响应时间统计。import numpy as np from sklearn.metrics import confusion_matrix def calculate_intent_metrics(eval_df, true_col, pred_col, intent_list): 计算意图识别的核心指标多分类场景 返回混淆矩阵、各意图详细指标、宏平均、微平均、整体准确率、响应时间 cm confusion_matrix(eval_df[true_col], eval_df[pred_col], labelsintent_list) per_intent_metrics {} for i, intent in enumerate(intent_list): TP cm[i, i] FP cm[:, i].sum() - TP FN cm[i, :].sum() - TP TN cm.sum() - TP - FP - FN 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: int(TP), FP: int(FP), FN: int(FN), TN: int(TN), Precision: round(precision, 4), Recall: round(recall, 4), F1: round(f1, 4) } 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()]) 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 accuracy (eval_df[true_col] eval_df[pred_col]).sum() / len(eval_df) rt_col response_time_ms 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) } 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 }4.3 实测结果对比我用同一套测试集分别跑了 Qwen1.5-1.8B-Chat 和 ChatGLM3-6B结果差异非常明显。模型整体准确率Macro F1平均响应时间(ms)95分位响应时间(ms)Qwen1.5-1.8B-Chat21.43%0.079722.0610861.83ChatGLM3-6B96.43%1.005278.325961.11Qwen1.5-1.8B 的表现可以用“灾难”来形容。看详细指标会发现“天气查询”的召回率是 100%但精准率只有 22.22%FP 高达 21 个。这说明模型把几乎所有输入都判断成了“天气查询”其他意图的 F1 值全是 0完全失去了区分能力。ChatGLM3-6B 则展现了真正的语义理解能力。宏平均 F1 值达到 100%各意图的精准率和召回率基本都是满分。对比传统语义向量模型在“告别”意图上仅 20% 的召回率ChatGLM3 达到了 100%说明它真正读懂了语境而不是靠关键词匹配。注意Qwen1.5-1.8B 在这个任务上的失败不代表它所有场景都不行而是说明在需要精细分类的意图识别任务上1.8B 的参数量确实不够用。Prompt 设计可能也有优化空间但模型本身的能力上限是主要瓶颈。5. 本篇常见错排查5.1 调用返回 401 或 403最常见的原因是 API Key 配置错误。检查 Key 是否完整复制有没有多余空格。如果用环境变量确认变量名和代码里读取的一致。另外确认 Key 没有过期或被禁用可以在控制台里查看 Key 状态。5.2 模型名写错导致 404TaoToken 的模型名需要和文档里列出的名称完全一致。比如Qwen1.5-1.8B-Chat和qwen1.5-1.8b-chat可能不一样大小写和连字符都要对上。建议先在模型对话页面测试一下模型名是否正确。5.3 响应时间波动大大模型推理的响应时间受并发、输入长度、模型负载影响。如果发现某次请求特别慢先检查是不是网络抖动。评测时建议串行执行避免并发导致互相干扰。另外记录响应时间时要区分“模型推理时间”和“网络传输时间”如果走 API 通道网络延迟会计入总耗时。5.4 模型输出格式不符合预期Qwen1.5-1.8B 有时候会输出一长串解释而不是单个意图标签。这时候需要在解析时做后处理比如只取第一行、去掉标点、匹配意图列表。如果模型频繁输出“未知”或无关内容说明 Prompt 需要调整可以加 few-shot 示例或者更严格的格式约束。5.5 混淆矩阵标签顺序不对confusion_matrix的labels参数要显式传入意图列表否则 sklearn 会按字母序排列导致矩阵行列和你的意图列表对不上。计算 TP/FP/FN 时也要确保索引和意图列表一致。5.6 测试集样本太少导致指标波动每个意图至少要有 100 条以上样本指标才有参考价值。样本太少时一条预测错误就能让 F1 值大幅波动。如果只是快速验证流程20-30 条可以接受但正式评测要扩充测试集。6. 继续深入的方向如果你已经跑通了上面的评测流程接下来可以尝试几个方向。一是扩充测试集覆盖更多意图类别和更复杂的混合意图场景看看模型在边界样本上的表现。二是调整 Prompt 策略比如加入 few-shot 示例、改变输出格式约束观察对识别准确率的影响。三是把评测结果接入持续集成流程每次模型更新或 Prompt 调整后自动跑一遍指标避免人工回归。对于需要长期做模型评测和编码任务的场景可以了解一下 Coding Plan它提供了更稳定的调用额度和更适合批量任务的使用方式。如果你只是想快速验证某个模型在意图识别上的表现可以直接在模型对话页面输入测试样本不用写代码就能看到结果。评测这件事跑一次不难难的是持续跑、持续对比。把配置骨架搭好之后后面换模型、换 Prompt、换测试集都只是改几个参数的事。