社区物资申报系统设计与实现:毕业设计指南
2026/11/2 15:45:49
去年接手某头部物业集团的客服中台改造,短短两周就把痛点摸得门儿清:
一句话:没有智能化,客服中心就是成本黑洞。
先放结论:
微服务模块划分示意图(精简版):
┌──────────┐ │ 小程序 │ 电话 │ 网关 │ 线路 └────┬─────┘ │统一 API 网关(Kong) ▼ ┌──────────────────────────┐ │ 意图识别服务 │←BERT+语义相似度 └────────┬────────┬────────┘ │ │ ▼ ▼ ┌─────────────┐ ┌──────────────┐ │ 问答引擎 │ │ 工单中心 │ │知识图谱 Neo4j│ │RabbitMQ+MySQL│ └─────────────┘ └──────────────┘ │ │ ▼ ▼ ┌──────────────────────────┐ │ 坐席工作台(WebSocket) │ └──────────────────────────┘每个服务独立仓库、独立 DB,灰度发布互不影响。
需求:输入“业主描述”+ 图片 OCR 文本,输出 1 级分类(共 18 类)与 2 级子类(共 127 类)。
特征工程
模型微调
代码片段(关键步骤,可直接跑通):
# -*- coding: utf-8 -*- """ 工单分类微调脚本 依赖: transformers>=4.30 pandas scikit-learn torch """ import torch, json, random, jieba, re from torch.utils.data import Dataset, DataLoader from transformers import BertTokenizerFast, BertForSequenceClassification from sklearn.preprocessing import LabelEncoder from sklearn.metrics import f1_score BATCH = 32 LR = 2e-5 EPOCHS = 8 MAX_LEN =懿 128 MODEL_PATH = "chinese-bert-wwm-ext" # 1. 读取原始工单------------------------------------------------- def load_raw(csv_file): df = pd.read_csv(csv_file) # 只保留有分类标签的样本 df = df[df["label_l1"].notna()] return df[["desc", "ocr", "label_l1", "label_l2"]] # 2. 文本清洗----------------------------------------------------- stop = set(open("property_stopword.txt", encoding="utf8").read().split()) def clean(txt): txt = re.sub(r"[0-9]{3,}", "NUM", txt) # 电话、房号归一化 tokens = [w for w in jieba.lcut(txt) if w not in stop and len(w) > 1] return " ".join(tokens) # 3. 数据集封装--------------------------------------------------- class TicketDataset(Dataset): def __init__(self, texts, labels_l1, labels_l2, tokenizer, max_len): self.texts = texts self.labels_l1 = labels_l1 self.labels_l2 = labels_l2 self.tokenizer = tokenizer self.max_len = max_len def __len__(self): return len(self.texts) def __getitem__(self, idx): enc = self.tokenizer( self.texts[idx], truncation=True, padding="max_length", max_length=self.max_len, return_tensors="pt") item = {k: v.squeeze(0) for k, v in enc.items()} item["labels_l1"] = torch.tensor(self.labels_l1[idx], dtype=torch.long) item["labels_l2"] = torch.tensor(self.labels_l2[idx], dtype=torch.long) return item # 4. 训练循环----------------------------------------------------- def train(model, loader, optimizer): model.train() for step, batch in enumerate(loader): outputs = model(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"], labels=batch["labels_l2"]) # 用2级当主loss loss = outputs.loss loss.backward() optimizer.step(); optimizer.zero_grad() if step % 50 == 0: print(f"step={step} loss={loss.item():.4f}") # 5. 验证 F1------------------------------------------------------ @torch.no_grad() def validate(model, loader): model.eval() y_true, y_pred = [], [] for batch in loader: logits = model(input_ids=batch["input_ids"], attention_mask=batch["attention_mask"]).logits preds = logits.argmax(dim=1).cpu().numpy() y_pred.extend(preds) y_true.extend(batch["labels_l2"].cpu().numpy()) return f1_score(y_true, y_pred, average="macro") if __name__ == "__main__": df = load_raw("ticket_train.csv") tokenizer = BertTokenizerFast.from_pretrained(MODEL_PATH) # label 编码 l1_enc = LabelEncoder(); l2_enc = LabelEncoder() df["l1_id"] = l1_enc.fit_transform(df["label_l1"]) df["l2_id"] = l2_enc.fit_transform(df["label_l2"]) # 构造数据集 texts = (df["desc"].astype(str) + " " + df["ocr"].astype(str)).apply(clean).tolist() dataset = TicketDataset(texts, df["l1_id"].tolist(), df["l2_id"].tolist(), tokenizer, MAX_LEN) loader_train = DataLoader(dataset, batch_size=BATCH, shuffle=True) # 模型 model = BertForSequenceClassification.from_pretrained(MODEL_PATH, num_labels=len(l2_enc.classes_)) optimizer = torch.optim.AdamW(model.parameters(), lr=LR) # 训练 best_score = 0 for epoch in range(EPOCHS): train(model, loader_train, optimizer) score = validate(model, loader_train) print(f"Epoch={epoch} F1={score:.4f}") if score > best_score: best_score = score model.save_pretrained("bert_ticket_best") print("训练完成,最高宏平均 F1:", best_score)边界条件:
handle_unknown="ignore"包装。工单分类后,需要把“急修”等高优任务 0.5 s 内派给附近工程师,用 RabbitMQ 的死信队列(DLX)做超时重试与延迟派单。
队列设计
ticket.normal:普通工单,TTL=30 min。ticket.dlx:死信队列,消费失败或超时后进入,TTL=5 min。ticket.delay:延迟派单,利用 TTL 实现“30 min 后仍未接单则升级”。生产端(Go)关键代码:
package main import ( "context" "encoding/json" "log" "time" amqp "github.com/rabbitmq/amqp091-go" ) type TicketMsg struct { ID string `json:"id"` Type int `json:"type"` // 1急修 2投诉 3缴费 Address string `json:"address"` Timestamp int64 `json:"ts"` } func failOnError(err error, msg string) { if err != nil { log.Fatalf("%s: %s", msg, err) } } func main() { conn, err := amqp.Dial("amqp://user:pwd@rabbitmq:5672/") failOnError(err, "连接 MQ 失败") defer conn.Close() ch, err := conn.Channel() failOnError(err, "打开 Channel 失败") defer ch.Close() // 声明正常交换机与队列 q, err := ch.QueueDeclare( "ticket.normal", // name true, // durable false, // delete when unused false, // exclusive false, // no-wait amqp.Table{ // 设置死信 "x-dead-letter-exchange": "ticket.dlx", "x-dead-letter-routing-key": "dlx", }, ) failOnError(err, "声明队列失败") body, _ := json.Marshal(TicketMsg{ ID: "T123456", Type: 1, Address: "6-2-301", Timestamp: time.Now().Unix()}) // 发布消息 err = ch.PublishWithContext( context.Background(), "", // exchange q.Name, // routing key false, // mandatory false, // immediate amqp.Publishing{ DeliveryMode: amqp.Persistent, ContentType: "application/json", Body: body, Expiration: "1800000", // 30 min 毫秒 }) failOnError(err, "发布失败") log.Printf(" [x] 已发送工单 %s", body) }ticket_id的派单状态,SETNX 原子抢锁,防止工程师 App 重复接单。nack(requeue=false),让消息进入 DLX,而不是无限重试。压测指标(4 核 8 G 容器 * 10 实例):
鉴权:
脱敏:
138****1234。对话状态管理常见错误
session_state(短期)与user_profile(长期),前者 TTL=15 min,后者持久化到 Postgres。知识图谱更新兼容
ver=2024Q2,引擎优先用最新版,老版本保留 24 h,支持回滚。上线三个月,系统把 90% 的“水电费怎么交”“门禁卡丢失”类问题拦在机器人层,工单平均流转时长从 26 min 压到 8 min。但我们也发现,当业主情绪激烈(语音分贝>80 dB)或涉及赔偿时,模型置信度即使 0.85 也挡不住用户一句“转人工”。
如何平衡智能回复准确率与人工接管率?
是继续调高阈值“宁错勿滥”,还是让模型学会识别“情绪”主动退让?欢迎留言聊聊你的做法。