目录
- 一、任务定义
- 二、技术路线对比
- 三、数据准备
- 四、Schema 设计
- 五、传统方法:BERT 分类 + CRF
- 六、现代方法:LLM 抽取
- 七、混合架构
- 八、Few-shot / In-context Learning
- 九、约束解码与结构化输出
- 十、模型评估
- 十一、生产部署
- 十二、在线优化
- 十三、错误分析与调优
- 十四、常见问题
- 十五、完整示例:客服 NLU 系统
一、任务定义
1.1 意图识别(Intent Classification)
判断用户输入属于哪一类(多分类或多标签):
"帮我查下明天上海到北京的高铁" → BookTrain
"我的快递到哪了" → CheckDelivery
"取消订单" → CancelOrder
1.2 命名实体识别(NER)
抽取文本中的关键实体:
"帮我查下明天上海到北京的高铁"
↓
{
"date": "明天",
"departure": "上海",
"destination": "北京",
"transport_type":"高铁"
}
1.3 联合任务(Joint NLU)
意图 + 实体一次输出:
{
"intent": "BookTrain",
"confidence": 0.96,
"entities": {
"date": "明天",
"departure": "上海",
"destination": "北京",
"transport_type": "高铁"
}
}
1.4 应用场景
- 客服机器人:路由到对应技能
- 语音助手:解析指令
- 搜索 / RAG:理解用户检索意图
- Agent 工具调用:function name + arguments 即 intent + entities
二、技术路线对比
| 路线 | 准确率 | 延迟 | 成本 | 数据需求 | 可解释 | 适用 |
|---|---|---|---|---|---|---|
| 规则 / 模板 | 中 | 极低 | 极低 | 无 | 高 | 冷启动、长尾兜底 |
| BERT 分类 + CRF | 高 | 低 | 低 | 数千-万条 | 中 | 高 QPS 稳定场景 |
| LLM Zero/Few-shot | 中-高 | 中 | 中 | 极少 | 高 | 快速上线、长尾意图 |
| LLM 微调 | 极高 | 中 | 中-高 | 数千条 | 中 | 需要兼顾准确率与灵活性 |
| Embedding 检索 + 分类 | 中-高 | 低 | 低 | 数百-千条 | 高 | 意图频繁新增 |
2.1 选择决策树
意图集合稳定且 QPS 高?
├ 是 ─→ BERT/CRF 微调
└ 否 ─→ 数据少且常变?
├ 是 ─→ LLM Few-shot 或 Embedding 检索
└ 否 ─→ LLM 微调(LoRA)
三、数据准备
3.1 数据格式
意图分类(CSV/JSONL):
{"text": "查询订单状态", "intent": "CheckOrder"}
{"text": "明天天气怎么样", "intent": "QueryWeather"}
NER(BIO/BIOES 标注):
查 O
询 O
张 B-PERSON
三 I-PERSON
的 O
订 B-ORDER
单 I-ORDER
或 span 格式:
{
"text": "查询张三的订单",
"spans": [
{"start": 2, "end": 4, "label": "PERSON"},
{"start": 5, "end": 7, "label": "ORDER"}
]
}
3.2 标注工具
| 工具 | 优势 | 适用 |
|---|---|---|
| Label Studio | 自部署、强大 | 团队标注 |
| doccano | 轻量、开源 | 小团队 |
| Prodigy | 主动学习、Spacy 集成 | 商业项目 |
| Argilla | 与 LLM 协作 | LLM 数据闭环 |
3.3 数据增强
# 1. 同义替换(基于词表/embedding)
def synonym_aug(text):
return text.replace("查询", random.choice(["查一下", "看下", "帮我查"]))
# 2. 回译(Translate → Back-translate)
en = translate_zh_en(text)
aug = translate_en_zh(en)
# 3. LLM 改写
llm("以不同口语表达保留含义改写:" + text)
# 4. 实体替换(保持标签的同时换实体)
# "明天上海到北京" → "后天广州到深圳"
3.4 数据质量
- 每个意图至少 50 条,长尾意图可补 LLM 合成
- 类别不平衡:oversampling 少数类 + 加权交叉熵
- 标注一致性:双标 + Cohen's Kappa > 0.8
- 保留真实分布的测试集:不要从训练集切
四、Schema 设计
4.1 意图层级
扁平 vs 层级:
扁平:
CheckOrder, BookOrder, CancelOrder, ReturnOrder, ...
层级:
Order
├── Check
├── Book
├── Cancel
└── Return
层级好处:
- 路由时先大类再细分,提高鲁棒性
- 长尾子意图可降级到父类
4.2 实体类型设计
| 实体类型 | 示例 | 备注 |
|---|---|---|
| 通用实体 | PERSON, LOCATION, DATE, MONEY | 复用预训练 |
| 业务实体 | ORDER_ID, PRODUCT_SKU, USER_LEVEL | 领域定制 |
| 槽位 | departure_city, return_date | 任务相关 |
4.3 槽位 vs 实体
- 实体(Entity):通用、文本本身的属性
- 槽位(Slot):意图所需的参数,可能由实体填充
实体: city = "上海"
槽位: departure_city = "上海" ← 由 city + 上下文角色填入
4.4 Schema 版本管理
# schema.yaml
version: 1.2.0
intents:
- name: BookTrain
description: 预订火车票
slots:
- { name: departure, type: city, required: true }
- { name: destination, type: city, required: true }
- { name: date, type: date, required: true }
- { name: passengers, type: integer, default: 1 }
- { name: train_type, type: enum, values: [高铁, 动车, 普快] }
五、传统方法:BERT 分类 + CRF
5.1 模型架构
意图:[CLS] → Linear → softmax
NER :每个 token → Linear → CRF → BIO tags
联合:共享 BERT encoder + 两个 head
5.2 训练(HuggingFace)
from transformers import (
AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer
)
from datasets import Dataset
import torch
MODEL = "hfl/chinese-roberta-wwm-ext"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
intents = ["BookTrain", "CheckOrder", "QueryWeather", "Other"]
i2l = {i: l for i, l in enumerate(intents)}
l2i = {l: i for i, l in i2l.items()}
def preprocess(ex):
enc = tokenizer(ex["text"], truncation=True, max_length=64)
enc["label"] = l2i[ex["intent"]]
return enc
train_ds = Dataset.from_json("train.jsonl").map(preprocess)
val_ds = Dataset.from_json("val.jsonl").map(preprocess)
model = AutoModelForSequenceClassification.from_pretrained(
MODEL, num_labels=len(intents),
id2label=i2l, label2id=l2i,
)
args = TrainingArguments(
output_dir="./intent",
learning_rate=3e-5,
per_device_train_batch_size=64,
num_train_epochs=5,
eval_strategy="epoch",
metric_for_best_model="f1",
load_best_model_at_end=True,
)
def metrics(p):
preds = p.predictions.argmax(-1)
from sklearn.metrics import f1_score
return {"f1": f1_score(p.label_ids, preds, average="macro")}
Trainer(
model=model, args=args,
train_dataset=train_ds, eval_dataset=val_ds,
compute_metrics=metrics,
).train()
5.3 NER 训练(Token Classification)
from transformers import AutoModelForTokenClassification, DataCollatorForTokenClassification
ner_labels = ["O", "B-CITY", "I-CITY", "B-DATE", "I-DATE"]
l2i = {l: i for i, l in enumerate(ner_labels)}
def tokenize_align(ex):
enc = tokenizer(ex["tokens"], is_split_into_words=True, truncation=True)
word_ids = enc.word_ids()
labels = []
prev = None
for wid in word_ids:
if wid is None:
labels.append(-100)
elif wid != prev:
labels.append(l2i[ex["ner_tags"][wid]])
else:
labels.append(l2i[ex["ner_tags"][wid]])
prev = wid
enc["labels"] = labels
return enc
model = AutoModelForTokenClassification.from_pretrained(MODEL, num_labels=len(ner_labels))
5.4 推理(带阈值与兜底)
import torch
from transformers import pipeline
clf = pipeline("text-classification", model="./intent", top_k=3)
ner = pipeline("ner", model="./ner", aggregation_strategy="simple")
def predict(text):
intents = clf(text)[0]
top = intents[0]
# 阈值兜底
if top["score"] < 0.6:
return {"intent": "Other", "score": top["score"], "entities": []}
return {
"intent": top["label"],
"score": top["score"],
"entities": ner(text),
}
5.5 加 CRF 层(可选,效果略升)
from torchcrf import CRF
class BertCRF(torch.nn.Module):
def __init__(self, bert, n_labels):
super().__init__()
self.bert = bert
self.classifier = torch.nn.Linear(bert.config.hidden_size, n_labels)
self.crf = CRF(n_labels, batch_first=True)
def forward(self, input_ids, attention_mask, labels=None):
h = self.bert(input_ids, attention_mask=attention_mask).last_hidden_state
logits = self.classifier(h)
if labels is not None:
mask = attention_mask.bool()
loss = -self.crf(logits, labels, mask=mask)
return loss
return self.crf.decode(logits, mask=attention_mask.bool())
六、现代方法:LLM 抽取
6.1 Zero-shot Prompt
from openai import OpenAI
client = OpenAI()
SYSTEM = """你是 NLU 系统,从用户输入中识别意图和实体,严格输出 JSON。
意图集合:BookTrain, CheckOrder, QueryWeather, Other
实体类型:city, date, order_id, train_type
输出格式:
{"intent": "...", "entities": {...}}
"""
def nlu(text):
resp = client.chat.completions.create(
model="claude-haiku-4-5",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": text},
],
response_format={"type": "json_object"},
)
return json.loads(resp.choices[0].message.content)
6.2 Function Calling(更可靠)
tools = [{
"type": "function",
"function": {
"name": "BookTrain",
"description": "预订火车票",
"parameters": {
"type": "object",
"properties": {
"departure": {"type": "string", "description": "出发城市"},
"destination": {"type": "string", "description": "目的城市"},
"date": {"type": "string", "description": "日期 YYYY-MM-DD 或自然语言"},
"train_type": {"type": "string", "enum": ["高铁", "动车", "普快"]},
},
"required": ["departure", "destination", "date"],
},
},
}, {
"type": "function",
"function": {
"name": "CheckOrder",
"description": "查询订单",
"parameters": {
"type": "object",
"properties": {"order_id": {"type": "string"}},
"required": ["order_id"],
},
},
}]
def nlu(text):
resp = client.chat.completions.create(
model="claude-haiku-4-5",
messages=[{"role": "user", "content": text}],
tools=tools, tool_choice="auto",
)
msg = resp.choices[0].message
if msg.tool_calls:
call = msg.tool_calls[0]
return {"intent": call.function.name,
"entities": json.loads(call.function.arguments)}
return {"intent": "Other", "entities": {}}
Function calling 比纯 prompt 更稳定,模型有专门优化。
6.3 LLM 微调(LoRA)
数据集(OpenAI 微调格式):
{"messages": [
{"role":"system","content":"NLU 系统..."},
{"role":"user","content":"明天上海到北京的高铁"},
{"role":"assistant","content":"{\"intent\":\"BookTrain\",\"entities\":{...}}"}
]}
LoRA 训练(PEFT):
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM
base = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-7B-Instruct", torch_dtype="auto")
lora_cfg = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj", "v_proj"], lora_dropout=0.05)
model = get_peft_model(base, lora_cfg)
# ... 训练 SFT
6.4 优势对比
| 传统 BERT/CRF | LLM 抽取 | |
|---|---|---|
| 训练数据需求 | 数千+ | 几条到 0 |
| 新意图扩展 | 需重训 | 改 prompt/tools |
| 模糊/口语化 | 弱 | 强 |
| 推理延迟 | < 50ms | 200-2000ms |
| 推理成本 | 低 | 高(10-100×) |
七、混合架构
实际生产常用双层架构:
┌─────────────────┐
用户输入 ────> │ Embedding 检索 │ ─→ 高置信 ─→ 直接返回
└────────┬────────┘
│ 低置信
▼
┌─────────────────┐
│ BERT 分类 │ ─→ 高置信 ─→ 返回
└────────┬────────┘
│ 低置信
▼
┌─────────────────┐
│ LLM 兜底 │ ─→ 返回
└─────────────────┘
7.1 Embedding 召回(快速 + 可解释)
import faiss, numpy as np
from sentence_transformers import SentenceTransformer
encoder = SentenceTransformer("BAAI/bge-large-zh-v1.5")
examples = load_examples() # [(text, intent)]
texts, intents = zip(*examples)
embeddings = encoder.encode(list(texts), normalize_embeddings=True)
index = faiss.IndexFlatIP(embeddings.shape[1])
index.add(embeddings.astype("float32"))
def retrieve_intent(text, k=5, threshold=0.85):
q = encoder.encode([text], normalize_embeddings=True).astype("float32")
scores, idx = index.search(q, k)
if scores[0][0] < threshold:
return None # 低置信,走下一层
# k=5 投票
votes = {}
for s, i in zip(scores[0], idx[0]):
votes[intents[i]] = votes.get(intents[i], 0) + s
return max(votes, key=votes.get)
7.2 优势
- 新意图增量加入:仅写入向量库,无需重训
- 高置信样本秒级响应
- LLM 仅处理 5-15% 的疑难 case,成本低
八、Few-shot / In-context Learning
8.1 静态示例
SYSTEM = """从用户输入识别意图与实体。
示例:
输入:明天去上海的高铁票
输出:{"intent":"BookTrain","entities":{"date":"明天","destination":"上海","train_type":"高铁"}}
输入:我的快递到哪了
输出:{"intent":"CheckDelivery","entities":{}}
输入:取消订单 12345
输出:{"intent":"CancelOrder","entities":{"order_id":"12345"}}
"""
8.2 动态示例(KNN + LLM)
def build_prompt(text):
# 用 embedding 检索 top-5 最相似的标注样本
similar = retrieve_top_k(text, k=5)
examples = "\n\n".join(
f"输入:{e['text']}\n输出:{json.dumps(e['label'], ensure_ascii=False)}"
for e in similar
)
return f"{SYSTEM}\n\n{examples}\n\n输入:{text}\n输出:"
KNN-augmented 通常比静态 few-shot 强 3-8 个百分点。
8.3 Chain-of-Thought(复杂场景)
请分两步:
1. 先分析用户的核心需求(reasoning)
2. 再给出最终 JSON(answer)
输入:帮我看看明天有没有从上海去北京的高铁,最好早上的
1. 用户在询问火车票,并指定了出发地、目的地、日期、时间偏好。
2. {"intent":"BookTrain","entities":{...}}
九、约束解码与结构化输出
9.1 JSON Schema 约束
vLLM / SGLang / OpenAI 都支持:
schema = {
"type": "object",
"properties": {
"intent": {"type": "string", "enum": ["BookTrain", "CheckOrder", "Other"]},
"entities": {"type": "object"},
},
"required": ["intent", "entities"],
}
# OpenAI
client.chat.completions.create(
model="...",
messages=[...],
response_format={"type": "json_schema",
"json_schema": {"name": "nlu", "schema": schema, "strict": True}},
)
# SGLang
import sglang as sgl
@sgl.function
def nlu(s, text):
s += SYSTEM + "\n用户:" + text + "\n输出:"
s += sgl.gen("out", json_schema=schema)
9.2 Outlines(开源约束生成)
import outlines
from pydantic import BaseModel
class NLUResult(BaseModel):
intent: Literal["BookTrain", "CheckOrder", "Other"]
entities: dict[str, str]
model = outlines.models.transformers("Qwen/Qwen2.5-7B-Instruct")
gen = outlines.generate.json(model, NLUResult)
result: NLUResult = gen("明天上海到北京的高铁")
收益:100% 合法 JSON、字段值在枚举范围内。
9.3 注意
- 约束解码会让生成更慢 5-15%
- 不能纠正语义错误:只能保证形式合法
- 强 schema + 充分示例是最稳的组合
十、模型评估
10.1 意图指标
from sklearn.metrics import classification_report, confusion_matrix
print(classification_report(y_true, y_pred, digits=4))
# precision / recall / f1-score / support
关键:macro-F1(每类平均),不是 accuracy(不平衡数据下误导)。
10.2 NER 指标(seqeval)
from seqeval.metrics import classification_report
print(classification_report(y_true, y_pred))
# 严格 span 匹配的 P/R/F1
注意:实体级别 vs token 级别得分差异很大,报告必须用 span-level。
10.3 端到端指标
NLU 系统真正关心:
- Frame Accuracy:意图 + 全部实体都正确
- Slot F1:每个槽位 P/R/F1
- 意图正确率
def frame_accuracy(preds, golds):
n = sum(1 for p, g in zip(preds, golds)
if p["intent"] == g["intent"] and p["entities"] == g["entities"])
return n / len(golds)
10.4 业务指标
- 降级率:转人工 / 兜底 Other 的比例
- 拒识率:低置信度直接拒绝的比例
- 人工标注一致率:模型预测 vs 复核员的一致率
十一、生产部署
11.1 服务架构
┌──────────────┐
请求 ─→ │ Tokenizer │
├──────────────┤
│ Embedding检索│ ←─ FAISS / Qdrant
├──────────────┤
│ BERT 分类 │ ←─ ONNX / Triton
├──────────────┤
│ NER │
├──────────────┤
│ Rule 后处理 │ ←─ 正则修补、归一化
├──────────────┤
│ LLM 兜底 │ ←─ 仅低置信触发
└──────────────┘
↓
统一 JSON 输出
11.2 ONNX 加速
from optimum.onnxruntime import ORTModelForSequenceClassification
model = ORTModelForSequenceClassification.from_pretrained(
"./intent",
export=True, provider="CUDAExecutionProvider",
)
通常 2-5× 加速。
11.3 Triton 部署
model_repository/
├── intent/
│ ├── 1/model.onnx
│ └── config.pbtxt
├── ner/
│ ├── 1/model.onnx
│ └── config.pbtxt
└── nlu_pipeline/
├── 1/model.py # Python backend 串联
└── config.pbtxt
# nlu_pipeline/1/model.py
import triton_python_backend_utils as pb_utils
class TritonPythonModel:
def execute(self, requests):
responses = []
for req in requests:
text = pb_utils.get_input_tensor_by_name(req, "TEXT")
intent = self._call_intent(text)
entities = self._call_ner(text)
responses.append(pb_utils.InferenceResponse(
output_tensors=[
pb_utils.Tensor("INTENT", intent),
pb_utils.Tensor("ENTITIES", entities),
]
))
return responses
11.4 缓存
热门 query 直接命中缓存(Redis):
import hashlib, redis, json
r = redis.Redis()
def cache_key(text):
return "nlu:" + hashlib.md5(text.encode()).hexdigest()
def predict_cached(text):
k = cache_key(text)
v = r.get(k)
if v:
return json.loads(v)
result = predict(text)
r.setex(k, 3600, json.dumps(result))
return result
短文本场景缓存命中率可达 30-50%。
11.5 灰度
按用户 ID 哈希切流量,新模型先放 1% → 10% → 100%,对比业务指标后再切。
十二、在线优化
12.1 数据闭环
线上请求 ─→ 模型预测
│
├─ 高置信 ─→ 直接使用
├─ 中置信 ─→ 抽样人工标注 ─→ 加入训练集
└─ 低置信 ─→ 全量人工标注 + 兜底
│
▼
下次迭代
12.2 主动学习
import numpy as np
def uncertainty_sampling(probs, n=100):
# 选熵最高的 n 个
entropy = -np.sum(probs * np.log(probs + 1e-12), axis=1)
return entropy.argsort()[-n:]
每周抽不确定样本人工标注,迭代成本最低。
12.3 模型监控
| 指标 | 告警条件 |
|---|---|
| 平均置信度 | 较基线下降 5%+ |
| 兜底率 | 上升 10%+ |
| 各意图占比 | 分布漂移 |
| p95 延迟 | 上升 50%+ |
| 标注样本召回的 F1 | 下降 |
数据漂移检测(PSI / KL 散度):
from scipy.stats import entropy
def psi(expected, actual, bins=10):
e = np.histogram(expected, bins)[0] / len(expected)
a = np.histogram(actual, bins)[0] / len(actual)
return np.sum((a - e) * np.log((a + 1e-9) / (e + 1e-9)))
# > 0.2 显著漂移
十三、错误分析与调优
13.1 错误归因
def analyze_errors(preds, golds):
errors = []
for p, g, t in zip(preds, golds, texts):
if p["intent"] != g["intent"]:
errors.append({"text": t, "pred": p, "gold": g, "type": "intent"})
for k, v in g["entities"].items():
if p["entities"].get(k) != v:
errors.append({"text": t, "pred": p, "gold": g, "type": f"entity:{k}"})
return errors
按 type 分桶分析占比,优先修最大头。
13.2 常见错误模式
| 模式 | 解决 |
|---|---|
| 标签噪声 | 复审训练集,剔除/修正 |
| 意图边界模糊 | 合并相近意图 / 加判别样本 |
| 长尾意图欠召回 | 数据增强 / oversampling |
| 实体边界错位 | CRF / 后处理正则 |
| 实体类型混淆 | 加更多对抗样本 |
| OOV 实体 | gazetteer 字典补充 |
13.3 字典 + 模型混合
import re
CITY_DICT = load_cities() # set of 4000+ 城市名
CITY_RE = re.compile("|".join(map(re.escape, sorted(CITY_DICT, key=len, reverse=True))))
def fix_entities(text, entities):
# 模型漏识别的城市,用字典补
found_cities = CITY_RE.findall(text)
for city in found_cities:
if city not in entities.values():
entities.setdefault("city", city)
return entities
十四、常见问题
14.1 训练 F1 高,线上效果差
- 训练/测试分布不一致:用真实流量重新切分
- 标注偏差:交叉复核
- 时间漂移:用最近 2 周数据重训
14.2 LLM 输出 JSON 不合法
- 用
response_format/ function calling - 加
strict=True(OpenAI Structured Outputs) - 用 Outlines / SGLang 约束解码
14.3 实体重叠(嵌套)
如"上海市黄浦区"中 city + district 重叠:
- 用 span-based 模型(如 W2NER、SpERT)
- 或拆成多任务 head
14.4 类别极度不平衡
- 损失加权:
CrossEntropyLoss(weight=class_weights) - Focal Loss
- Re-sampling
- LLM 合成少数类样本
14.5 多语言
- 用
xlm-roberta-base/bge-m3这类多语模型 - LLM 抽取天然多语
- 不同语种维护各自 few-shot 示例
14.6 私有化部署 LLM 太贵
- 蒸馏成 BERT:用 LLM 标 10w 条 → 训 BERT,准确率 90% LLM 但成本 1%
- 用小模型(Qwen2.5-1.5B/3B + LoRA)
- 量化(AWQ)
十五、完整示例:客服 NLU 系统
业务:电商客服机器人。
15.1 Schema
version: 1.0
intents:
- { name: CheckOrder, desc: 查询订单 }
- { name: ApplyRefund, desc: 申请退款 }
- { name: ShippingQuery, desc: 物流查询 }
- { name: ProductInfo, desc: 商品咨询 }
- { name: Other, desc: 其他 }
entities:
- order_id: {type: regex, pattern: "\\d{10,16}"}
- product_sku: {type: regex, pattern: "SKU[A-Z0-9]+"}
- amount: {type: money}
- reason: {type: enum, values: [质量问题, 不喜欢, 错发漏发, 其他]}
15.2 三层架构
import re, json, redis, hashlib
from sentence_transformers import SentenceTransformer
from transformers import pipeline
from openai import OpenAI
class NLUEngine:
def __init__(self):
self.encoder = SentenceTransformer("BAAI/bge-large-zh-v1.5")
self.bert_intent = pipeline("text-classification", model="./intent", top_k=1)
self.bert_ner = pipeline("ner", model="./ner", aggregation_strategy="simple")
self.llm = OpenAI()
self.r = redis.Redis()
self._build_index()
def _build_index(self):
examples = json.load(open("examples.json"))
self.example_texts = [e["text"] for e in examples]
self.example_labels = [e["label"] for e in examples]
embs = self.encoder.encode(self.example_texts, normalize_embeddings=True)
import faiss
self.idx = faiss.IndexFlatIP(embs.shape[1])
self.idx.add(embs.astype("float32"))
def _retrieve(self, text, k=5):
q = self.encoder.encode([text], normalize_embeddings=True).astype("float32")
scores, idx = self.idx.search(q, k)
return scores[0], idx[0]
def _bert_predict(self, text):
intent = self.bert_intent(text)[0][0]
ents = self.bert_ner(text)
return {"intent": intent["label"], "score": intent["score"], "entities": ents}
def _llm_predict(self, text, similar_examples):
examples = "\n".join(
f"输入: {self.example_texts[i]}\n输出: {json.dumps(self.example_labels[i], ensure_ascii=False)}"
for i in similar_examples
)
prompt = f"""你是 NLU 系统。根据示例识别意图与实体,输出 JSON。
示例:
{examples}
输入: {text}
输出:"""
resp = self.llm.chat.completions.create(
model="claude-haiku-4-5",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
return json.loads(resp.choices[0].message.content)
def _post_process(self, text, result):
# 用正则补全
order_match = re.search(r"\d{10,16}", text)
if order_match:
result.setdefault("entities", {})["order_id"] = order_match.group()
return result
def predict(self, text):
k = "nlu:" + hashlib.md5(text.encode()).hexdigest()
if cached := self.r.get(k):
return json.loads(cached)
# 1) 检索高置信
scores, idx = self._retrieve(text)
if scores[0] > 0.92:
result = {"intent": self.example_labels[idx[0]]["intent"],
"entities": self.example_labels[idx[0]].get("entities", {}),
"source": "retrieval"}
else:
# 2) BERT
bert_out = self._bert_predict(text)
if bert_out["score"] > 0.85:
result = {**bert_out, "source": "bert"}
else:
# 3) LLM 兜底
result = {**self._llm_predict(text, idx), "source": "llm"}
result = self._post_process(text, result)
self.r.setex(k, 600, json.dumps(result, ensure_ascii=False))
return result
15.3 FastAPI 服务
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
engine = NLUEngine()
class Req(BaseModel):
text: str
class Resp(BaseModel):
intent: str
entities: dict
source: str
@app.post("/nlu", response_model=Resp)
def nlu(req: Req):
return engine.predict(req.text)
@app.get("/health")
def health():
return {"status": "ok"}
15.4 监控指标
from prometheus_client import Counter, Histogram, start_http_server
REQ = Counter("nlu_requests_total", "请求数", ["intent", "source"])
LAT = Histogram("nlu_latency_seconds", "延迟", ["source"])
# 在 predict 内:
REQ.labels(intent=result["intent"], source=result["source"]).inc()
LAT.labels(source=result["source"]).observe(elapsed)
附录:速查
A.1 模型选型
| 任务 | 中文 | 英文/多语 |
|---|---|---|
| 句向量 | bge-large-zh-v1.5 | bge-m3 |
| 分类 | hfl/chinese-roberta | xlm-roberta-base |
| NER | hfl/chinese-roberta + CRF | xlm-roberta + CRF |
| LLM 抽取(小) | Qwen2.5-7B-Instruct | Llama-3.1-8B |
| LLM 抽取(大) | Qwen2.5-72B | Claude / GPT-4 |
A.2 数据量经验
| 路线 | 每意图样本 |
|---|---|
| LLM Few-shot | 3-10 |
| Embedding 检索 | 30-100 |
| BERT 分类 | 200-1000 |
| LLM SFT | 50-500 |
A.3 常用库
| 用途 | 库 |
|---|---|
| Tokenize/分类 | transformers |
| 句向量 | sentence-transformers |
| 向量索引 | faiss / qdrant / milvus |
| CRF | torchcrf / pytorch-crf |
| 评估 | seqeval / scikit-learn |
| 约束解码 | outlines / sglang |
| 标注 | Label Studio / doccano / Argilla |
A.4 性能基线
| 指标 | 良好 |
|---|---|
| 意图 macro-F1 | > 0.92 |
| NER span-F1 | > 0.88 |
| Frame Accuracy | > 0.80 |
| p95 延迟 | < 100ms (BERT) / 1s (LLM) |
| 兜底率 | < 8% |