通用意图标签在自家业务话术上总会差一口气,原因往往不在模型规模,而在标签边界没有用自家语料重新划一遍。拿业务日志再训一轮 Xing4.0-29B-Intent 是可行的方向,但真正决定结果的是三件事:日志能不能被整理成「用户原话 + 确定意图」的成对样本、歧义与跨类重复有没有被清掉、验证集是否用同一批测试样本和微调前做过逐类对比。参数、学习率和轮数都是在这三件事成立之后才值得调的东西。
意图边界收不住,通常不是模型不够大,而是训练样本里混了歧义、跨类重复和未脱敏的噪声。建议先从业务日志抽出「用户原话 + 人工确认意图」的成对样本,按统一规则去重脱敏,清掉一人可判两类的样本,再按意图分层划验证集,每类留出足够样本做逐类对比。判断是否变好以同一批测试样本的混淆矩阵为准,训练损失下降不算依据;微调前后没有逐类改善,就回退到原模型与既有规则。
把业务日志整理成意图与文本成对的样本
原始日志一般不能直接训练:它记录的是会话过程,不是标注结果。抽取时要先明确「哪一行才算一条确定标签」——通常只保留人工坐席改判过的、二次确认后未被推翻的、以及用户明确选择过业务入口的记录;模型自己给出且无人复核的低置信度记录,建议直接排除,否则等于把模型的旧判断又教回去一遍。
字段抽取建议按下面的口径固定下来,写进抽取脚本而不是每次手工挑:
- text:用户单轮原始问句,多轮会话只取被标注意图所对应的那一轮,必要时把上文关键槽位拼成一句,不要拼整段对话。
- intent:最终生效的意图标识,来自人工确认或业务系统回执,不取模型预测值。
- source / labeled_by / ts:渠道、标注来源、时间,用来事后回溯某一批数据是否被污染。
脱敏在抽取阶段一次做完,不要留到训练前:手机号、身份证、订单号、地址、邮箱统一替换成占位符,保留占位符形态反而有助于模型学到「这里是单号」的结构。去重先做文本归一化(去首尾空白、全半角统一、连续标点折叠、英文小写),归一化后完全相同的只留一条;再做近重复合并,相似度阈值需要结合你自己的语料抽样确认,不要照搬别人的数。
{
"id": "biz-000123",
"text": "我上周买的那单怎么还没到,单号<ORDER_ID>",
"intent": "logistics_delay",
"source": "app_chat",
"labeled_by": "human_agent",
"ts": "<ISO8601>"
}
清掉歧义样本和跨类重复样本
标注噪声一旦进了训练集,模型学到的是「遇到这类话就随机猜」,表现出来就是边界模糊、相似意图之间来回跳。清理阶段建议按三条标准判定歧义:
- 换一个人来标会得到不同意图,且两种标法都能在业务上自圆其说(例如「东西坏了」既可归质量投诉也可归售后换货)。
- 文本信息不足,缺少能区分意图的槽位,只有「怎么办」「不行」这类没有指向的片段。
- 一条文本同时命中两个意图的关键词,而这两个意图对应的业务处理动作不同(退款与退货、催发货与查物流)。
第 1、2 类优先尝试补槽位或按业务动作重新定义标签;补不出来就丢弃,或统一归入 unknown 并从训练集中排除,不要硬塞进最接近的类。跨类重复的识别分两步:归一化后文本完全相同但 intent 不同,属于硬冲突,必须人工裁决留一个;文本高度相似而类别不同,按模板键(去掉单号、时间、地名后的剩余骨架)分组抽检,模板批量生成、只替换变量的样本尤其要按模板键去重,否则某一类在训练集里权重会虚高。
清理前后的样本量对照表建议每次重跑数据都填一遍,数字以你自己脚本的输出为准,不引用外部结论:
python - <<'PY'
import json, collections
cnt = collections.Counter(json.loads(l)["intent"] for l in open("clean.jsonl"))
for k, v in cnt.most_common():
print(k, v)
print("total", sum(cnt.values()))
PY
表里至少列:原始条数、去重后条数、去歧义后条数、可用条数、丢弃原因分布。如果某一类清理后剩余样本低于你在下一节设定的下限,先考虑回补该类的真实日志,或把它并入相邻的上位意图,而不是放宽容忍度把噪声留下。
划出训练集与验证集并保证每类都有样本
划分要按意图分层抽样,不能整体随机切,否则小类可能整类落进训练集,验证时这一类等于没测。常用比例是训练集与验证集 8:2 或 9:1,随机种子固定并写进配置,保证同一次实验可复现。每类样本下限建议:训练集每类不低于几十条量级、验证集每类不低于二十条左右,这两个数是经验起点,需要结合你的类别总数和业务容错来定;类别多而样本薄的时候,宁可先合并意图,也不要用个位数样本去评估。
import json, random, collections
random.seed(42)
by_intent = collections.defaultdict(list)
for line in open("clean.jsonl"):
r = json.loads(line)
by_intent[r["intent"]].append(r)
train, dev = [], []
for intent, rows in by_intent.items():
random.shuffle(rows)
n_dev = max(1, int(len(rows) * 0.2)) # 比例按需改
dev.extend(rows[:n_dev])
train.extend(rows[n_dev:])
for name, rows in (("train", train), ("dev", dev)):
with open(f"data/{name}.jsonl", "w") as f:
f.writelines(json.dumps(r, ensure_ascii=False) + "\n" for r in rows)
某一类总量极少时,处理办法按优先级排:合并到上位意图(可评估、可上线)→ 只保留在训练集不单列验证指标、改由规则兜底 → 从本轮微调中剔除,等下个数据周期补样本。不建议为了凑验证集把同一模板的样本复制扩充,那样得到的指标没有意义。
写一份通用微调脚本骨架
下面的骨架只固定结构与可复现项,学习率、轮数、batch 全部用占位符,需要按你的显存和实际框架自行调。Xing4.0-29B-Intent 这个量级通常不适合全参微调,建议先确认可用的并行方式和参数高效微调接口,再决定训练配置;接口细节按你所使用训练框架的文档填写,这里不假设具体 SDK。
# train_intent.py —— 通用骨架,按实际框架替换模型加载与训练器部分
import json, random, torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
SEED = 42
random.seed(SEED); torch.manual_seed(SEED)
CFG = {
"base_model": "<本地权重路径>", # 替换为 Xing4.0-29B-Intent 的实际路径
"train_file": "data/train.jsonl",
"dev_file": "data/dev.jsonl",
"max_len": 256,
"lr": "<待调,例如 1e-5 量级需自行扫>",
"epochs": "<待调>",
"batch_size": "<受显存限制,待定>",
"output_dir": "runs/intent-v1",
"save_steps": 200,
"logging_dir": "runs/intent-v1/logs",
}
# 1) 读数据,固定 intent -> id 的映射并落盘,避免两次训练标签错位
# 2) tokenizer 编码,truncation=True, max_length=CFG["max_len"]
# 3) 训练器参数里显式写入 seed、logging_dir、save_steps、report_to=[]
# 4) 训练结束后保存 label_map.json、CFG 快照与 git commit(若有)
启动时把标准输出同时落盘,断点续跑依赖 save_steps 产出的 checkpoint 目录:
python train_intent.py `--config` cfg.yaml 2>&1 | tee runs/intent-v1/train.log
# 中断后从最近的 checkpoint 续跑(参数名按框架填)
python train_intent.py `--config` cfg.yaml `--resume` runs/intent-v1/checkpoint-600
数据文件建议用内容哈希命名或在目录里留一份 version 文件,否则一两个月后很难说清某个指标对应的是哪一版样本。
用验证集混淆矩阵判断是否真的变好
训练损失下降只说明模型拟合了训练集,不能说明业务判定变准。真正的判断要落在验证集混淆矩阵上:行是真实意图,列是预测意图,对角线是判对的样本。读的时候先看每一行的召回,也就是该类真实样本有多少被判了出来;再看每一列的精确率,也就是被判成该类的样本里有多少真属于它。然后重点盯两件事——把 A 判成 B 的具体类别对,以及大量样本落进 unknown 或其它类的汇聚列。前者对应业务动作会走错的场景,后者说明模型还没学会分,而不是「判得保守」。
对比必须用同一批测试样本,微调前后各跑一次,输出逐类结果再逐类比:
python eval.py `--model` <微调前权重路径> `--input` data/dev.jsonl `--out` eval/base.csv
python eval.py `--model` runs/intent-v1 `--input` data/dev.jsonl `--out` eval/ft.csv
# 逐类对比 recall / precision 与混淆对 top-N,关注目标类是否升、其它类是否明显掉
判定无效的回退动作要事先写清楚:整体或关键类没有改善,或改善目标类的同时让原本稳定的类明显变差,就回退到上一个模型版本与既有规则,而不是继续加轮数去压验证集。回退同时保留这次的数据集版本和错例清单,把验证集里新增的错例当作下一轮清洗的输入。如果连续两轮清洗后验证集指标仍在原地,问题多半在标签定义本身,应该回到意图体系去合并或拆分类别,而不是继续调整微调参数。