正则+语义双层分类——让Agent没有漏网之鱼

现状之痛:上一篇文章讲了场景路由,但路由的前提是分类准确。正则只能覆盖90%,剩下10%的「帮我问下XX到YY多少钱」这种口语化表达,怎么准确路由?
读完你将:学会正则+语义双层分类——精确层保下限,模糊层拉上限。

一、问题:正则表达式的天花板

场景路由的早期版本,依赖正则表达式判断用户意图:

import re

QUOTING_PATTERN = r'到.*海运费|到.*空运费|报价|价格|quotation|rate'

def regex_match(text):
    if re.search(QUOTING_PATTERN, text):
        return "quoting"
    return None

它能覆盖常见的询价表达:「到法兰克福的海运费多少」能命中,「DDP至CDG报价」也能命中。

但问题来了,实际运营中发现:正则匹配的覆盖率大约只有60-70%。

大量真实用户表达——

  • 「帮我问下XX到YY的空运多少钱」
  • 「HK到LAX什么价」
  • 「这票运费多少」

这些在正则层面要么匹配失败,要么匹配到错误的场景。

更棘手的是那些需要「理解」而非「匹配」的表达:「转发这封邮件给sunny」——没有任何邮件处理关键词,但语义上显然是转发意图。正则对此无能为力。

核心洞察:正则擅长已知模式,语义擅长未知表达。两者互补,不是替代。


二、核心思路:正则+语义双层分类

方案是双层分类:第一层是正则匹配(精确层),第二层是语义匹配(模糊层)。

精确层处理有明确关键词的表达——「海运费」「空运费」「报价」「quotation」「rate」。模式稳定,正则效率极高,几乎零延迟。

模糊层处理精确层无法覆盖的表达——「帮我问下XX到YY多少钱」「HK到LAX什么价」。没有固定关键词,但语义上明确指向询价。

两层的关系:精确层优先。

用户请求
  │
  ├─① 正则精确匹配 → 命中?→ 直接使用结果(高置信度)
  │                        ↓ 未命中
  ├─② 语义模糊匹配 → 命中?→ 使用结果(中等置信度)
  │                        ↓ 未命中
  └─③ 兜底场景 → 通用对话

![双层分类流程图](/tmp/wechat-series/diagrams/dual-classification.png)

*▲ 双层分类:正则保底(90%)+ 语义扩展(10%)+ 兜底(0.1%)*


三、技术实现

3.1 语义层:嵌入向量 + Chroma

模糊层的技术基础是文本嵌入。我们用 all-MiniLM-L6-v2 —— 一个轻量级的Sentence-BERT模型,把文本映射到384维向量空间。

from sentence_transformers import SentenceTransformer
import chromadb

# 加载嵌入模型
model = SentenceTransformer('all-MiniLM-L6-v2')

# 初始化Chroma
client = chromadb.Client()
collection = client.get_or_create_collection("scene_prototypes")

# 每个场景有若干「场景原型」
prototypes = {
    "quoting": [
        "到法兰克福的海运费多少",
        "DDP至CDG报价",
        "帮我问下香港到LAX的空运费",
    ],
    "email": [
        "转发这封邮件给sunny",
        "帮我回复sunny的邮件",
        "发邮件给广州代理",
    ],
    # ... 6个场景
}

3.2 匹配逻辑

def semantic_match(text):
    # 编码用户请求
    query_vec = model.encode(text)
    
    # 在Chroma中搜索最相似的场景原型
    results = collection.query(
        query_embeddings=[query_vec],
        n_results=3
    )
    
    # 最高相似度
    top_score = results['distances'][0][0]
    top_scene = results['metadatas'][0][0]['scene']
    
    # 阈值检查
    if top_score > 0.55:
        return top_scene
    return None

3.3 调优过程:从0.45到0.55

阈值选择不是拍脑袋决定的,我经历了一个系统的调优过程:

阈值覆盖率误判率问题
0.45~90%~15%覆盖率好但误判高,「查航班」被误判成「询价」
0.50~82%~8%中等,仍有「转发邮件」误判
0.55~75%~3%可接受,漏掉的走兜底,但不误判

最终选择0.55——我接受「漏掉」一些边缘表达(它们走兜底场景),但不能接受「误判」(导致错误的场景路由)。

额外加了一个margin检查: 不仅要求最高相似度超过阈值,还要求最高相似度与次高相似度的差值超过margin值(0.1)。这进一步减少模糊边界上的误判。

def semantic_match_with_margin(text):
    results = collection.query(query_embeddings=[model.encode(text)], n_results=2)
    
    top_score = results['distances'][0][0]
    second_score = results['distances'][0][1]
    
    # 双重条件:超过阈值 + 超过margin
    if top_score > 0.55 and (top_score - second_score) > 0.1:
        return results['metadatas'][0][0]['scene']
    return None

3.4 场景原型设计

原型设计直接影响匹配质量,我的三个原则:

  1. 每个场景至少5个原型,覆盖常见变体。询价场景包括:海运询价、空运询价、DDP询价、DAP询价、多式联运询价。每个原型是真实的用户表达(从历史对话中搜集)。
  2. 原型之间需有区分度。两个场景的原型在语义上过于接近,匹配质量会下降。
  3. 原型需定期更新。随着用户表达习惯变化,旧原型的覆盖率会下降。每月从兜底场景搜集未匹配的表达,评估是否新增原型。

✅ 验证:双层分类上线后,分类准确率从80%提升到接近100%。「转发这封邮件给sunny」这类无关键词表达也能准确命中邮件场景。

踩坑:最初只用正则,覆盖率60-70%,大量口语化请求落到兜底场景。加语义层后覆盖大幅提升,但0.45阈值误判太多,调了整整一周才找到0.55+0.1 margin的最优组合。

价值:每个请求都能被准确路由到正确的场景,使用正确的工具集。没有漏网之鱼。

▸ 认知跃迁:精确和模糊不是二选一。正则提供「确定性」,语义提供「扩展性」,两者结合才能高精度+高覆盖。


四、优缺点

✅ 优点

  • 覆盖率极高——正则覆盖不了的表达,语义层能覆盖
  • 无关键词表达也能命中(「转发这封邮件给sunny」)
  • 正则+语义互补,不是替代——精确层保底,模糊层扩展
  • Chroma向量检索延迟低(<10ms),不影响用户体验
  • 场景原型可扩展——发现新表达只需新增原型

⚠️ 缺点

  • 语义匹配依赖embedding质量——模型换版可能影响稳定性
  • 首次加载慢——模型下载+Chroma索引构建约需30秒
  • 有误判风险——语义相近的场景(航班查询vs空运询价)可能混淆
  • 阈值调优凭经验——目前没有自动调优机制
  • 冷启动问题——新场景需要累积原型后才能稳定匹配

五、此刻的你

此刻的你,已经不再是那个「写一堆正则祈祷能覆盖所有表达」的悲观主义者。你正在成为一个能用双层分类让每个请求都有家可归的系统设计者。

在自然语言处理中,面对表达的千变万化,我们需要的不再是「找到一个精确匹配」,而是「在最可能的范围内做出最准确的判断」。这就是模糊匹配的艺术。

下一篇:路由和分类都到位了,但复杂任务怎么办?一份报告里有询价、有催单、有财务——一个场景装不下。复合流程容器,让报告变成一条管道。


️ 实体:正则匹配, 语义分类, Chroma, 嵌入模型 价值:Agent分类, 意图识别, 覆盖率提升 认知:从二选一到互补——精确层保下限,模糊层拉上限

posted @ 2026-08-05 21:17  魏无记  阅读(1)  评论(0)    收藏  举报