本章定位
markdown
用户原始输入
│
▼
①~⑤ 预处理 → 护栏 → 分类 → 实体 → 多意图 ← 前五章
│
▼
⑥ 置信度决策路由(本章) ← 根据置信度决定:直接处理 / 确认 / 兜底
│
▼
路由到对应业务处理节点
意图分类的结果不是 100% 可靠的。置信度决策路由就是根据"系统有多确定"来决定走哪条路------高置信直接处理、中置信先确认、低置信兜底转人工。
一、三级路由策略
1.1 核心架构
markdown
意图分类结果(intent + confidence)
│
├── confidence >= 高阈值 ──→ 直接路由到业务处理节点
│
├── 低阈值 <= confidence < 高阈值 ──→ 确认式交互(向用户确认意图)
│ │
│ ├── 用户确认 → 执行
│ └── 用户否认 → 提供选项 / 转人工
│
└── confidence < 低阈值 ──→ 兜底处理
│
├── 提供选项让用户选
├── 引导用户重新描述
└── 转人工客服
1.2 阈值不是全局统一的
不同意图、不同风险等级应该有不同的阈值:
python
# 默认阈值
DEFAULT_THRESHOLDS = {
"high": 0.80, # 高置信线
"low": 0.45, # 低置信线
}
# 高风险操作的阈值更严格
HIGH_RISK_THRESHOLDS = {
"high": 0.90,
"low": 0.60,
}
# 从意图配置中读取风险等级,动态选择阈值
def get_thresholds(intent_l1: str, intent_l2: str, intent_schema: dict) -> dict:
"""
根据意图的风险等级返回对应的阈值。
高风险(退款、投诉等)→ 阈值更严格
普通操作 → 默认阈值
"""
intent_config = intent_schema["intent_map"].get(intent_l1, {}).get(intent_l2, {})
risk_level = intent_config.get("risk_level", "normal")
if risk_level == "high":
return HIGH_RISK_THRESHOLDS
return DEFAULT_THRESHOLDS
二、高置信路由:直接执行
2.1 做什么
系统对分类结果足够自信,直接路由到对应的业务处理节点。
2.2 实现
python
from typing import Literal
from langgraph.graph import END
def route_high_confidence(state: dict) -> str:
"""
高置信路由:根据 L1 意图路由到对应的业务处理节点。
返回节点名称。
"""
l1 = state["l1_intent"]
route_map = {
"售前咨询": "presale_handler",
"售后服务": "aftersale_handler",
"投诉建议": "complaint_handler",
"账号问题": "account_handler",
"闲聊": "chitchat_handler",
}
target = route_map.get(l1, "chitchat_handler")
return target
2.3 但在路由之前,还要检查槽位
即使置信度高,如果必填槽位缺失,也不能直接进入业务处理:
python
def pre_route_check(state: dict) -> str:
"""
路由前置检查:
1. 槽位齐全 → 进入业务处理
2. 槽位缺失 → 进入追问节点
"""
if state.get("slots_complete", False):
return "route_to_business"
else:
return "ask_for_missing_slots"
def ask_for_missing_slots_node(state: dict) -> dict:
"""追问缺失槽位"""
return {
"response": state["follow_up_question"],
"awaiting_slot_fill": True,
}
三、中置信路由:确认式交互
3.1 做什么
系统不够确定,先向用户展示识别结果,请求确认后再处理。
3.2 确认话术生成
python
def generate_confirmation(state: dict) -> dict:
"""
生成确认话术。
根据意图类型生成不同风格的确认问题。
"""
l1 = state["l1_intent"]
l2 = state["l2_intent"]
confidence = state["confidence"]
entities = state.get("filled_slots", {})
# 构建确认消息
intent_label = f"{l1} - {l2}"
# 如果有提取到的实体,附带在确认中
entity_parts = []
if entities.get("order_id"):
entity_parts.append(f"订单 {entities['order_id']}")
if entities.get("product"):
entity_parts.append(f"产品「{entities['product']}」")
entity_text = f"(涉及{','.join(entity_parts)})" if entity_parts else ""
confirmation = f"请确认:您是想要「{l2}」{entity_text}吗?"
# 提供明确的操作选项
options = f"请回复:\n1. 是的\n2. 不是,我想要的是..."
return {
"response": f"{confirmation}\n{options}",
"awaiting_confirmation": True,
"pending_intent": {"l1": l1, "l2": l2},
}
3.3 处理用户的确认回复
python
def handle_confirmation_response(state: dict) -> dict:
"""
处理用户对意图确认的回复。
"""
user_reply = state["processed_input"].strip()
pending = state.get("pending_intent", {})
# 注意:必须先检查否定再检查肯定!
# 错误做法:any("是" in "不是") → True,把"不是"误判为肯定
# 正确做法:否定词排前面,用 startswith / == 代替 in 子串匹配
# 否定信号(长词优先,避免"不"误匹配"不错")
negative_signals = ["不是", "不对", "错了", "换一个", "不", "2"]
# 肯定信号
positive_signals = ["是的", "没错", "确认", "对的", "好的", "是", "对", "嗯", "1"]
is_denied = any(user_reply == s or user_reply.startswith(s) for s in negative_signals)
is_confirmed = (not is_denied) and any(
user_reply == s or user_reply.startswith(s) for s in positive_signals
)
if is_confirmed:
return {
"l1_intent": pending["l1"],
"l2_intent": pending["l2"],
"confidence": 0.95, # 用户确认后提升置信度
"awaiting_confirmation": False,
"confirmation_status": "confirmed",
}
if is_denied:
return {
"awaiting_confirmation": False,
"confirmation_status": "denied",
# 否认后进入兜底流程
}
# 用户回复模糊,再问一次或直接转兜底
return {
"awaiting_confirmation": False,
"confirmation_status": "unclear",
}
3.4 确认不超过 2 轮
python
MAX_CONFIRMATION_ROUNDS = 2
def should_escalate_confirmation(state: dict) -> bool:
"""如果确认超过 2 轮用户还是不明确,放弃确认,转兜底"""
rounds = state.get("confirmation_rounds", 0)
return rounds >= MAX_CONFIRMATION_ROUNDS
四、低置信路由:兜底处理
4.1 三种兜底策略
python
def fallback_handler(state: dict) -> dict:
"""
低置信兜底处理。
根据场景选择不同的兜底策略。
"""
confidence = state["confidence"]
l1 = state.get("l1_intent", "")
# --- 策略 1:提供选项让用户选(优先) ---
if confidence >= 0.2:
# 还有一定线索,提供 top-3 可能的意图供选择
top_intents = state.get("top3_intents", [])
if top_intents:
options = "\n".join(
f"{i+1}. {t['l1']} - {t['l2']}"
for i, t in enumerate(top_intents[:3])
)
return {
"response": f"抱歉,我不太确定您的需求。请问您想要的是:\n{options}\n\n请回复序号,或者重新描述您的问题。",
"fallback_type": "provide_options",
}
# --- 策略 2:引导用户重新描述 ---
if confidence >= 0.1:
return {
"response": "抱歉,我没有完全理解您的问题。能再详细描述一下吗?比如:\n- 您遇到了什么问题?\n- 涉及哪个订单或产品?",
"fallback_type": "ask_rephrase",
}
# --- 策略 3:转人工(最后手段) ---
return {
"response": "抱歉,我暂时无法理解您的需求。正在为您转接人工客服,请稍候...",
"fallback_type": "transfer_human",
"transfer_to_human": True,
}
4.2 用户选择选项后的处理
python
def handle_option_selection(state: dict) -> dict:
"""
处理用户选择的选项。
用户回复"1"/"2"/"3" 或意图关键词。
"""
user_reply = state["processed_input"].strip()
top_intents = state.get("top3_intents", [])
# 尝试解析数字选项
if user_reply in ["1", "2", "3"]:
idx = int(user_reply) - 1
if idx < len(top_intents):
selected = top_intents[idx]
return {
"l1_intent": selected["l1"],
"l2_intent": selected["l2"],
"confidence": 0.90, # 用户选择后置信度拉高
"fallback_resolved": True,
}
# 用户可能用文字描述了意图,重新走分类
return {
"needs_reclassification": True,
"fallback_resolved": False,
}
五、完整的 LangGraph 路由节点
5.1 路由函数
python
def confidence_router(state: dict) -> str:
"""
置信度路由函数。
作为 add_conditional_edges 的路由函数使用。
"""
confidence = state["confidence"]
l1 = state["l1_intent"]
l2 = state["l2_intent"]
intent_schema = state.get("_intent_schema", intent_schema_global)
# 获取该意图的阈值
thresholds = get_thresholds(l1, l2, intent_schema)
# 高置信
if confidence >= thresholds["high"]:
# 还需要检查槽位
if state.get("slots_complete", True):
return "high_confidence_route"
else:
return "ask_missing_slots"
# 中置信
if confidence >= thresholds["low"]:
return "confirm_intent"
# 低置信
return "fallback"
5.2 完整图结构
python
from langgraph.graph import StateGraph, START, END
def build_routing_graph():
"""
构建完整的置信度决策路由图。
图结构:
┌→ presale_handler ──┐
START ├→ aftersale_handler ─┤
│ ├→ complaint_handler ─┤
▼ ├→ account_handler ───┤
confidence_check └→ chitchat_handler ──┤
│ │
├─(高置信+槽位全)─→ business_router ────────────┘
│ │
├─(高置信+槽位缺)─→ ask_missing_slots ──→ ... │
│ │
├─(中置信)────────→ confirm_intent │
│ │ │
│ ├─(确认)→ business_router│
│ ├─(否认)→ fallback │
│ └─(超限)→ fallback │
│ │
└─(低置信)────────→ fallback │
│ │
├─(选项)→ 重新路由 │
├─(重述)→ 重新分类 │
└─(转人工)→ END ────────→ END
"""
graph = StateGraph(RoutingState)
# 节点
graph.add_node("confidence_check", lambda s: s) # 透传,路由在边上做
graph.add_node("business_router", business_router_node)
graph.add_node("ask_missing_slots", ask_for_missing_slots_node)
graph.add_node("confirm_intent", generate_confirmation)
graph.add_node("handle_confirmation", handle_confirmation_response)
graph.add_node("fallback", fallback_handler)
# 业务处理节点
graph.add_node("presale_handler", presale_node)
graph.add_node("aftersale_handler", aftersale_node)
graph.add_node("complaint_handler", complaint_node)
graph.add_node("account_handler", account_node)
graph.add_node("chitchat_handler", chitchat_node)
graph.add_node("format_response", format_response_node)
# 入口
graph.add_edge(START, "confidence_check")
# 置信度路由
graph.add_conditional_edges("confidence_check", confidence_router, {
"high_confidence_route": "business_router",
"ask_missing_slots": "ask_missing_slots",
"confirm_intent": "confirm_intent",
"fallback": "fallback",
})
# 业务路由
graph.add_conditional_edges("business_router", route_high_confidence, {
"presale_handler": "presale_handler",
"aftersale_handler": "aftersale_handler",
"complaint_handler": "complaint_handler",
"account_handler": "account_handler",
"chitchat_handler": "chitchat_handler",
})
# 所有业务节点 → 格式化输出
for handler in ["presale_handler", "aftersale_handler", "complaint_handler",
"account_handler", "chitchat_handler"]:
graph.add_edge(handler, "format_response")
graph.add_edge("format_response", END)
graph.add_edge("ask_missing_slots", END) # 追问后等待用户下一轮输入
graph.add_edge("confirm_intent", END) # 确认后等待用户下一轮输入
graph.add_edge("fallback", END) # 兜底后等待用户下一轮输入
return graph.compile()
六、阈值调优
6.1 上线初期的保守策略
arduino
刚上线(数据少,不确定分类质量):
高阈值 = 0.85(宁可多确认)
低阈值 = 0.55(宁可多兜底)
目标:减少"答非所问"的灾难性体验
代价:多一些确认交互,用户会多回复一轮
6.2 基于数据的阈值校准
上线一段时间后,用积累的数据来找最优阈值:
python
import numpy as np
def calibrate_thresholds(
classification_logs: list[dict],
target_precision: float = 0.95,
) -> dict:
"""
基于历史分类日志校准阈值。
输入:分类日志列表,每条包含:
- confidence: 分类置信度
- is_correct: 是否正确(人工标注)
方法:找到使正确率 >= target_precision 的最低置信度作为高阈值。
返回:{"high": 0.82, "low": 0.43}
"""
# 按置信度排序
logs = sorted(classification_logs, key=lambda x: x["confidence"])
confidences = np.array([log["confidence"] for log in logs])
correctness = np.array([log["is_correct"] for log in logs])
# 从高到低扫描,找到正确率首次低于目标的点
best_high = 0.80 # 默认
for threshold in np.arange(0.95, 0.30, -0.01):
mask = confidences >= threshold
if mask.sum() < 10: # 样本太少,跳过
continue
precision = correctness[mask].mean()
if precision >= target_precision:
best_high = float(threshold)
# 低阈值:正确率 < 70% 的区域
best_low = best_high * 0.55 # 简单经验公式
for threshold in np.arange(best_high - 0.01, 0.10, -0.01):
mask = confidences >= threshold
if mask.sum() < 10:
continue
precision = correctness[mask].mean()
if precision < 0.70:
best_low = float(threshold + 0.01)
break
return {"high": round(best_high, 2), "low": round(best_low, 2)}
6.3 持续迭代流程
markdown
每 1~2 周执行一次阈值校准:
1. 导出最近的分类日志(input, confidence, l1, l2)
2. 抽样 200~500 条,人工标注"分类是否正确"
3. 运行 calibrate_thresholds 计算新阈值
4. 对比新旧阈值,分析变化原因
5. 灰度上线新阈值(10% 流量验证)
6. 全量切换
七、监控与告警
7.1 关键指标
python
def compute_routing_metrics(logs: list[dict]) -> dict:
"""计算路由相关的监控指标"""
total = len(logs)
if total == 0:
return {}
high_count = sum(1 for l in logs if l["route"] == "high_confidence")
confirm_count = sum(1 for l in logs if l["route"] == "confirm_intent")
fallback_count = sum(1 for l in logs if l["route"] == "fallback")
transfer_count = sum(1 for l in logs if l.get("transfer_to_human"))
# 确认后的转化率
confirmed_logs = [l for l in logs if l.get("confirmation_status") == "confirmed"]
denied_logs = [l for l in logs if l.get("confirmation_status") == "denied"]
confirm_success_rate = (
len(confirmed_logs) / (len(confirmed_logs) + len(denied_logs))
if (len(confirmed_logs) + len(denied_logs)) > 0 else 0
)
return {
"total_requests": total,
"high_confidence_rate": high_count / total, # 目标:60~80%
"confirm_rate": confirm_count / total, # 目标:10~25%
"fallback_rate": fallback_count / total, # 目标:< 15%
"human_transfer_rate": transfer_count / total, # 目标:< 5%
"confirm_success_rate": confirm_success_rate, # 目标:> 70%
}
7.2 告警规则
| 指标 | 正常范围 | 告警条件 | 可能原因 |
|---|---|---|---|
| 高置信率 | 60~80% | < 50% | 分类模型质量下降 / 新增未覆盖的意图 |
| 兜底率 | < 15% | > 20% | 意图体系覆盖不全 / 阈值设置过高 |
| 人工转接率 | < 5% | > 10% | 系统整体能力不足,需要优化 |
| 确认成功率 | > 70% | < 50% | 中置信区间的分类质量太差 |
| 确认否认后转人工率 | --- | > 30% | 兜底选项设计不合理 |
八、异常处理与降级
路由层的降级原则:路由异常时走 fallback,确保请求不会卡死在中间。
python
def confidence_router(state: dict) -> str:
"""置信度路由函数。异常时兜底。"""
try:
confidence = state.get("confidence", 0.0)
l1 = state.get("l1_intent", "")
l2 = state.get("l2_intent", "")
thresholds = config.get_thresholds(l1, l2)
if confidence >= thresholds.high:
return "business_router" if state.get("slots_complete", True) else "ask_missing_slots"
if confidence >= thresholds.low:
return "confirm_intent"
return "fallback"
except Exception:
# 路由异常 → 一定走 fallback,不能卡死
return "fallback"
九、本章小结
| 环节 | 企业级做法 |
|---|---|
| 三级路由 | 高置信直接处理 → 中置信确认后处理 → 低置信兜底 |
| 动态阈值 | 不同意图不同阈值,高风险操作(退款、投诉)阈值更严格 |
| 确认交互 | 生成确认话术 + 处理用户回复,最多确认 2 轮后转兜底 |
| 兜底分级 | 提供选项(有线索时)→ 引导重述(模糊时)→ 转人工(最后手段) |
| 槽位联动 | 即使高置信,槽位缺失也要先追问再处理 |
| 阈值调优 | 上线初期保守,积累数据后每 1~2 周校准一次 |
| 监控告警 | 跟踪高置信率、兜底率、人工转接率、确认成功率 |