评论区一条“多少钱”和一条“你们怎么收费”,在业务上可能指向完全不同的人——一个在比价,一个真有需求。意图识别要做的事情,就是在这类模糊表达里找到真正的意向客户。
意图识别是 AI 获客系统的核心模块,它的任务是把自然语言评论分类到预定义的意图槽位(购买意向、咨询意向、无效闲聊、同行探价等),并输出一个可执行的意向等级。本文记录一套从规则匹配到行业微调落地的完整方案。
整体方案从规则匹配演进到行业微调,大致经历了三个阶段:
#mermaid-svg-1bupvx1hB7UTDzNH{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1bupvx1hB7UTDzNH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1bupvx1hB7UTDzNH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1bupvx1hB7UTDzNH .error-icon{fill:#552222;}#mermaid-svg-1bupvx1hB7UTDzNH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1bupvx1hB7UTDzNH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1bupvx1hB7UTDzNH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1bupvx1hB7UTDzNH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1bupvx1hB7UTDzNH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1bupvx1hB7UTDzNH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1bupvx1hB7UTDzNH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1bupvx1hB7UTDzNH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1bupvx1hB7UTDzNH .marker.cross{stroke:#333333;}#mermaid-svg-1bupvx1hB7UTDzNH svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1bupvx1hB7UTDzNH p{margin:0;}#mermaid-svg-1bupvx1hB7UTDzNH .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1bupvx1hB7UTDzNH .cluster-label text{fill:#333;}#mermaid-svg-1bupvx1hB7UTDzNH .cluster-label span{color:#333;}#mermaid-svg-1bupvx1hB7UTDzNH .cluster-label span p{background-color:transparent;}#mermaid-svg-1bupvx1hB7UTDzNH .label text,#mermaid-svg-1bupvx1hB7UTDzNH span{fill:#333;color:#333;}#mermaid-svg-1bupvx1hB7UTDzNH .node rect,#mermaid-svg-1bupvx1hB7UTDzNH .node circle,#mermaid-svg-1bupvx1hB7UTDzNH .node ellipse,#mermaid-svg-1bupvx1hB7UTDzNH .node polygon,#mermaid-svg-1bupvx1hB7UTDzNH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1bupvx1hB7UTDzNH .rough-node .label text,#mermaid-svg-1bupvx1hB7UTDzNH .node .label text,#mermaid-svg-1bupvx1hB7UTDzNH .image-shape .label,#mermaid-svg-1bupvx1hB7UTDzNH .icon-shape .label{text-anchor:middle;}#mermaid-svg-1bupvx1hB7UTDzNH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1bupvx1hB7UTDzNH .rough-node .label,#mermaid-svg-1bupvx1hB7UTDzNH .node .label,#mermaid-svg-1bupvx1hB7UTDzNH .image-shape .label,#mermaid-svg-1bupvx1hB7UTDzNH .icon-shape .label{text-align:center;}#mermaid-svg-1bupvx1hB7UTDzNH .node.clickable{cursor:pointer;}#mermaid-svg-1bupvx1hB7UTDzNH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1bupvx1hB7UTDzNH .arrowheadPath{fill:#333333;}#mermaid-svg-1bupvx1hB7UTDzNH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1bupvx1hB7UTDzNH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1bupvx1hB7UTDzNH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1bupvx1hB7UTDzNH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1bupvx1hB7UTDzNH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1bupvx1hB7UTDzNH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1bupvx1hB7UTDzNH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1bupvx1hB7UTDzNH .cluster text{fill:#333;}#mermaid-svg-1bupvx1hB7UTDzNH .cluster span{color:#333;}#mermaid-svg-1bupvx1hB7UTDzNH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1bupvx1hB7UTDzNH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1bupvx1hB7UTDzNH rect.text{fill:none;stroke-width:0;}#mermaid-svg-1bupvx1hB7UTDzNH .icon-shape,#mermaid-svg-1bupvx1hB7UTDzNH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1bupvx1hB7UTDzNH .icon-shape p,#mermaid-svg-1bupvx1hB7UTDzNH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1bupvx1hB7UTDzNH .icon-shape .label rect,#mermaid-svg-1bupvx1hB7UTDzNH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1bupvx1hB7UTDzNH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1bupvx1hB7UTDzNH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1bupvx1hB7UTDzNH :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
命中
未命中
原始评论输入
规则匹配粗筛
是否命中关键词
初判高意向
长尾表达漏召回
人工复核与标注
行业数据生产
LoRA 垂直行业微调
意图分类推理
意向等级输出
效果评估与迭代
01 规则匹配解决不了长尾表达
第一版意图识别用的是关键词规则:命中“多少钱”“价格”“怎么收费”就判为高意向。上线一周就发现两个问题。
召回率严重不足。用户表达同一意图的方式太多——“你们家做不做”“能帮我看看吗”"这个怎么弄"都不带价格词,规则完全命中不了。某次抽样统计中,300 条人工标注为高意向的评论,规则版只召回41%。
规则列表维护成本失控。每个行业要维护一套关键词表,装修行业和律师行业的表达完全不同,规则数量膨胀到 2000 多条后,互相冲突的情况开始出现。
规则匹配适合做第一层粗筛(过滤明显无效内容),但不适合做意图判定本身。
两版方案在关键指标上的对比如下:
| 高意向召回率 | 41%(300 条人工标注样本抽样) | 0.88 ~ 0.91(装修 0.90 / 法律 0.88 / 教培 0.91) |
| 高意向精确率 | 关键词命中即判高意向,误判率高,未单独统计 | 0.90 ~ 0.93(装修 0.92 / 法律 0.90 / 教培 0.93) |
| 宏平均 F1 | 未达可用水平,长尾表达大量漏召回 | 0.87 ~ 0.90(装修 0.89 / 法律 0.87 / 教培 0.90) |
| 规则/适配器维护成本 | 每行业维护一套关键词表,规则膨胀至 2000 多条后互相冲突 | 每行业一个几十 MB 的 LoRA 适配器,切换行业只需加载对应适配器 |
02 行业微调的核心是数据而非模型
第二版改为模型方案:基座模型选通用大模型,在其上做垂直行业微调。
一个关键认知:微调的效果瓶颈在数据,不在模型。测试中发现,把训练数据从 2000 条加到 8000 条,意图分类 F1 提升约 11 个百分点;而把基座模型从 7B 换到 13B,F1 只提升约 2 个百分点。数据质量和数量是第一优先级。
数据生产流程分四步:
上述四步流程可以概括为下面这张图:
#mermaid-svg-lwTXmpoQricKVVO7{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lwTXmpoQricKVVO7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lwTXmpoQricKVVO7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lwTXmpoQricKVVO7 .error-icon{fill:#552222;}#mermaid-svg-lwTXmpoQricKVVO7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lwTXmpoQricKVVO7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lwTXmpoQricKVVO7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lwTXmpoQricKVVO7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lwTXmpoQricKVVO7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lwTXmpoQricKVVO7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lwTXmpoQricKVVO7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lwTXmpoQricKVVO7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lwTXmpoQricKVVO7 .marker.cross{stroke:#333333;}#mermaid-svg-lwTXmpoQricKVVO7 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lwTXmpoQricKVVO7 p{margin:0;}#mermaid-svg-lwTXmpoQricKVVO7 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-lwTXmpoQricKVVO7 .cluster-label text{fill:#333;}#mermaid-svg-lwTXmpoQricKVVO7 .cluster-label span{color:#333;}#mermaid-svg-lwTXmpoQricKVVO7 .cluster-label span p{background-color:transparent;}#mermaid-svg-lwTXmpoQricKVVO7 .label text,#mermaid-svg-lwTXmpoQricKVVO7 span{fill:#333;color:#333;}#mermaid-svg-lwTXmpoQricKVVO7 .node rect,#mermaid-svg-lwTXmpoQricKVVO7 .node circle,#mermaid-svg-lwTXmpoQricKVVO7 .node ellipse,#mermaid-svg-lwTXmpoQricKVVO7 .node polygon,#mermaid-svg-lwTXmpoQricKVVO7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lwTXmpoQricKVVO7 .rough-node .label text,#mermaid-svg-lwTXmpoQricKVVO7 .node .label text,#mermaid-svg-lwTXmpoQricKVVO7 .image-shape .label,#mermaid-svg-lwTXmpoQricKVVO7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-lwTXmpoQricKVVO7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lwTXmpoQricKVVO7 .rough-node .label,#mermaid-svg-lwTXmpoQricKVVO7 .node .label,#mermaid-svg-lwTXmpoQricKVVO7 .image-shape .label,#mermaid-svg-lwTXmpoQricKVVO7 .icon-shape .label{text-align:center;}#mermaid-svg-lwTXmpoQricKVVO7 .node.clickable{cursor:pointer;}#mermaid-svg-lwTXmpoQricKVVO7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lwTXmpoQricKVVO7 .arrowheadPath{fill:#333333;}#mermaid-svg-lwTXmpoQricKVVO7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lwTXmpoQricKVVO7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lwTXmpoQricKVVO7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lwTXmpoQricKVVO7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lwTXmpoQricKVVO7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lwTXmpoQricKVVO7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lwTXmpoQricKVVO7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lwTXmpoQricKVVO7 .cluster text{fill:#333;}#mermaid-svg-lwTXmpoQricKVVO7 .cluster span{color:#333;}#mermaid-svg-lwTXmpoQricKVVO7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lwTXmpoQricKVVO7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lwTXmpoQricKVVO7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-lwTXmpoQricKVVO7 .icon-shape,#mermaid-svg-lwTXmpoQricKVVO7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lwTXmpoQricKVVO7 .icon-shape p,#mermaid-svg-lwTXmpoQricKVVO7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lwTXmpoQricKVVO7 .icon-shape .label rect,#mermaid-svg-lwTXmpoQricKVVO7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lwTXmpoQricKVVO7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lwTXmpoQricKVVO7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lwTXmpoQricKVVO7 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
一致
不一致
真实评论抽样
人工意图标注
标注一致性校验
进入训练集
仲裁复核
构造反例集
合并为微调数据集
反例集是微调质量的胜负手。没有反例的模型会把所有带“多少钱”的评论都判成高意向,误判率会高到业务无法接受。
03 微调方案选型与参数记录
对比了全参微调和 LoRA 两种路线。全参微调效果略好,但每个行业都要存一份完整权重,存储和推理成本高;LoRA 只训练低秩适配矩阵,单行业适配器只有几十 MB,切换行业只需加载不同的适配器。
最终方案采用 LoRA,关键参数:
from peft import LoraConfig, get_peft_model
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type="SEQ_CLS"
)
训练阶段固定 base model,只更新适配器权重。学习率用 1e-4,batch size 32,3 个 epoch 后验证集指标开始收敛,第 5 个 epoch 出现轻微过拟合(训练 loss 继续下降、验证 loss 回升),最终取第 4 个 epoch 的 checkpoint。
04 线上推理与效果评估
线上推理服务把基座模型与行业 LoRA 适配器合并加载,对单条评论做意图分类并映射为意向等级,核心代码如下:
import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from peft import PeftModel
BASE_MODEL = "your-base-model-path"
ADAPTER_DIR = "./lora-adapter-zhuangxiu" # 每个行业一个适配器目录
# 意图标签与意向等级的映射关系
INTENT_LABELS = ["购买意向", "咨询意向", "无效闲聊", "同行探价", "其他"]
INTENT_LEVEL = {
"购买意向": "A(高意向)",
"咨询意向": "B(中意向)",
"同行探价": "C(观察)",
"无效闲聊": "D(无意向)",
"其他": "D(无意向)",
}
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
base_model = AutoModelForSequenceClassification.from_pretrained(
BASE_MODEL, num_labels=len(INTENT_LABELS)
)
# 加载 LoRA 适配器并与基座模型合并,合并后推理不再有额外延迟
model = PeftModel.from_pretrained(base_model, ADAPTER_DIR).merge_and_unload()
model.eval()
@torch.no_grad()
def classify_comment(text: str, max_length: int = 128):
inputs = tokenizer(
text, truncation=True, padding=True,
max_length=max_length, return_tensors="pt"
)
logits = model(**inputs).logits
probs = torch.softmax(logits, dim=–1)[0]
idx = int(torch.argmax(probs))
intent = INTENT_LABELS[idx]
return {
"text": text,
"intent": intent,
"confidence": round(float(probs[idx]), 4),
"level": INTENT_LEVEL[intent],
}
if __name__ == "__main__":
print(classify_comment("你们家装修大概多少钱一平?"))
关键步骤说明:
- 加载基座模型:用与训练阶段一致的基座模型和标签数量初始化分类头,保证权重结构对齐。
- 加载并合并适配器:PeftModel.from_pretrained 读取行业适配器,merge_and_unload() 把低秩矩阵合并进基座权重,推理时无需额外计算分支。
- 切换行业:只需替换 ADAPTER_DIR 指向对应行业的适配器目录,无需重新加载基座模型。
- 推理与后处理:model.eval() 关闭 dropout,torch.no_grad() 省去梯度计算;softmax 取最大概率类别作为意图,再通过映射表输出意向等级与置信度。
线上推理时,LoRA 适配器与基座模型合并加载,推理延迟与未微调模型基本持平,单条评论分类耗时稳定在 300ms 以内。
评估用五折交叉验证,指标取宏平均 F1。三个行业(装修、法律、教培)的测试结果:
| 装修 | 5 | 0.89 | 0.92 | 0.90 |
| 法律 | 6 | 0.87 | 0.90 | 0.88 |
| 教培 | 5 | 0.90 | 0.93 | 0.91 |