限时福利领取


背景痛点:正则匹配为什么撑不住客服场景

先交代一下老系统的“黑历史”。我们原来的固定问答模块,全靠正则+关键词硬怼,规则写到 3000+ 条时已经没人敢动:

  1. 新问题上线平均要 0.5 人/天,运营同学得先翻 30 页正则表,确认不会误杀才敢加一条,扩展成本指数级上升
  2. 中文同义表达太灵活,“怎么退货”“我想把货退掉”这类说法,正则一写就是 N 套,语义歧义导致召回率掉到 60% 以下。
  3. 双 11 前临时活动文案一改,正则全崩,紧急热修把值班同学逼到通宵。

一句话:纯规则系统像老房子刷漆,刷到最后墙都酥了。

技术选型:为什么不是纯 ML,也不是纯规则

我们先后跑了三组小流量 AB Test:

方案 准确率 召回率 开发人日 备注
纯规则 94% 58% 5 高准低召,维护噩梦
纯 BERT 二分类 87% 85% 12 需要 2w+ 标注样本,冷启动哭晕
BERT + 规则引擎(混合) 92% 81% 8 样本降到 5k,运营可干预

结论很现实:

  • 纯 ML 对标注量在线 GPU 成本都敏感,小公司扛不住。
  • 规则引擎在“必须 100% 精准”的场景(例如资金类答复)依旧不可替代。
  • 混合架构把“不确定”交给模型,把“必须确定”交给规则,ROI 最高

核心实现:Sentence-BERT + 动态阈值

1. 离线构建语义向量库

我们选用 paraphrase-multilingual-MiniLM-L12-v2,维度 384,在 CPU 上 latency 友好。关键代码如下:

# embedding_service.py
from typing import List
import numpy as np
from sentence_transformers import SentenceTransformer

class SentenceEncoder:
    def __init__(self, model_name: str = "paraphrase-multilingual-MiniLM-L12-v2"):
        self.model = SentenceTransformer(model_name)

    def encode(self, sentences: List[str], batch_size: int = 64) -> np.ndarray:
        """
        返回 shape=(len(sentences), 384) 的向量
        复杂度: O(n) 线性,n 为句子数量
        """
        if not sentences:
            return np.empty((0, 384))
        try:
            embs = self.model.encode(
                sentences,
                batch_size=batch_size,
                show_progress_bar=True,
                convert_to_numpy=True,
            )
            return embs.astype("float32")
        except Exception as e:
            logger.exception("Encoding failed")
            raise

把历史 FAQ(约 1.2w 条)批量编码后写入 .npy平均一条 10 ms

2. 动态相似度阈值算法

固定阈值 0.8 在高峰期会误拒大量合理问法,低峰期又误召。我们根据近 5 分钟拒绝率做滑动调整:

# threshold_adaptor.py
import numpy as np

class DynamicThreshold:
    def __init__(self, base: float = 0.75, window: int = 50):
        self.base = base
        self.window = window
        self.reject_window: List[bool] = []

    def update(self, matched: bool) -> None:
        self.reject_window.append(matched)
        if len(self.reject_window) > self.window:
            self.reject_window.pop(0)

    def get_threshold(self) -> float:
        if not self.reject_window:
            return self.base
        reject_rate = 1 - np.mean(self.reject_window)
        # 拒绝率高 → 阈值放松;拒绝率低 → 阈值收紧
        return float(np.clip(self.base - 0.1 * (reject_rate - 0.2), 0.6, 0.9))

线上实测把拒识率从 18% 降到 7%,而准确率保持 >92%。

3. 规则引擎与模型融合策略

融合顺序:

  1. 规则优先——若命中“资金”“注销”等高危关键词,直接走规则答复,旁路模型
  2. 模型兜底——向量相似度 > 动态阈值,且最高相似 FAQ 的置信度 > 0.85,返回对应答案。
  3. 其余情况标记为“未知”,走人工或引导式多轮。

这样规则保证合规模型保证泛化,二者互补。

性能优化:Faiss + 异步管道

1. 基于 Faiss 的向量检索

384 维暴力比对 1.2w 条 FAQ latency 约 120 ms,扛不住 QPS 500 的目标。用 Faiss IndexFlatIP(内积)+ 归一化向量,单次 5 ms

import faiss

class FaissIndex:
    def __init__(self, d: int = 384):
        self.index = faiss.IndexFlatIP(d)   # 内积 = cos 相似度
        self.mapping = []                   # idx -> FAQ id

    def add(self, vecs: np.ndarray, ids: List[int]) -> None:
        faiss.normalize_L2(vecs)
        self.index.add(vecs)
        self.mapping.extend(ids)

    def search(self, query: np.ndarray, k: int = 5) -> Tuple[np.ndarray, List[int]]:
        if self.index.ntotal == 0:
            return np.array([]), []
        faiss.normalize_L2(query)
        scores, idxs = self.index.search(query, k)
        return scores[0], [self.mapping[i] for i in idxs[0] if i >= 0]

2. 异步处理管道

非阻塞对用户体验至关重要。我们把“日志写入 + 阈值更新 + 埋点”丢给 Celery:

# tasks.py
from celery import Celery
app = Celery("bot", broker="redis://localhost:6379/0")

@app.task(bind=True, max_retries=2)
def log_async(self, query: str, matched: bool, answer_id: int):
    try:
        session.add(DialogLog(q=query, matched=matched, answer_id=answer_id))
        session.commit()
        threshold_adaptor.update(matched)   # 更新动态阈值
    except Exception as exc:
        logger.warning("Log failed: %s", exc)
        raise self.retry(exc=exc, countdown=1)

Flask 主线程只负责召回与返回,整体 P99 延迟从 380 ms 降到 160 ms。

避坑指南:数据清洗与零停机更新

1. 对话日志清洗 3 步

  • 去重:同一用户 5 min 内重复问句,只留第一条,减少 30% 噪声
  • 脱敏:手机号、订单号用正则 mask,避免隐私泄露导致训练违法
  • 标注纠偏:运营同学每日抽 200 条“模型拒识”复核,把误拒纠正后回流训练池,持续提升召回。

2. 模型热更新零停机

  1. 新模型在影子环境加载,构建新 Faiss 索引写入 Redis Stream
  2. API 网关通过 Etag 对比模型版本号,双实例并行 30 s。
  3. 确认新模型无 5xx 后,旧容器优雅下线,用户无感知

压测与效果

指标 正则旧系统 混合新系统
平均响应 290 ms 150 ms
P99 响应 680 ms 260 ms
意图准确率 89% 92%
单 CPU 核 QPS 120 420

延伸思考:用反馈自动调阈值

动态阈值目前靠拒绝率单变量调整,粒度粗。下一步计划:

  1. 用户点踩/点赞信号接入,构造 reward。
  2. 多臂老虎机(Thompson Sampling)把阈值离散成 0.05 步长,在线探索最优
  3. 贝叶斯更新减少震荡,期望把人工调参降到 0

小结

这套 BERT+规则混合的固定问答系统,已在生产环境稳定运行 6 个月,意图准确率提升 40%,QPS 提升 3 倍,而标注量只有纯 ML 方案的 1/4。对于人力有限、合规要求高的中小团队,混合架构在成本与效果之间给出了可落地的折中点。如果你也在被正则规则折磨,不妨从 Sentence-BERT + Faiss 的最小可用版本开始,先跑通 80% 场景,再逐步让模型“长”出更精细的枝桠。

限时福利领取


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐