2026-09-10 19:51:08 +08:00
|
|
|
"""一期客服的确定性路由,先处理安全和边界,再允许公开知识检索。"""
|
|
|
|
|
|
|
|
|
|
from collections.abc import Sequence
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class CustomerServiceRoute:
|
|
|
|
|
intent: str
|
|
|
|
|
knowledge_intents: tuple[str, ...] = ()
|
|
|
|
|
is_chitchat: bool = False
|
2026-09-11 16:11:30 +08:00
|
|
|
requires_context: bool = False
|
2026-09-10 19:51:08 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class CustomerServiceIntentRouter:
|
|
|
|
|
_SECURITY_KEYWORDS = ("验证码", "密码泄露", "被盗", "诈骗", "非本人交易")
|
2026-09-11 16:11:30 +08:00
|
|
|
# 明确的凭据披露和提示词注入必须在知识检索前拦截,避免把攻击内容当作普通 FAQ。
|
|
|
|
|
_SECURITY_DISCLOSURE_PHRASES = ("密码是", "密码为", "我的密码", "验证码是", "验证码为")
|
|
|
|
|
_PROMPT_INJECTION_KEYWORDS = (
|
|
|
|
|
"忽略之前", "忽略所有规则", "系统提示词", "开发者消息", "泄露提示词", "越过限制",
|
|
|
|
|
"不要遵守规则", "显示内部指令",
|
|
|
|
|
)
|
2026-09-10 19:51:08 +08:00
|
|
|
_COMPLIANCE_KEYWORDS = ("推荐", "收益最高", "稳赚", "保本", "帮我买", "替我交易")
|
|
|
|
|
_ACCOUNT_KEYWORDS = ("持仓", "收益", "订单", "定投", "银行卡", "风险测评", "投诉进度")
|
|
|
|
|
_HUMAN_TRANSFER_KEYWORDS = ("转人工", "人工客服", "投诉", "赔偿", "法律", "纠纷")
|
|
|
|
|
_POLICY_KEYWORDS = (
|
|
|
|
|
"申购", "赎回", "到账", "费率", "手续费", "确认份额", "交易日", "分红", "规则", "政策"
|
|
|
|
|
)
|
|
|
|
|
_PRODUCT_KEYWORDS = (
|
|
|
|
|
"产品", "基金代码", "基金经理", "份额类别", "a类", "c类", "净值", "风险等级"
|
|
|
|
|
)
|
|
|
|
|
_CHITCHAT_MESSAGES = frozenset({
|
|
|
|
|
"你好", "您好", "嗨", "哈喽", "在吗", "谢谢", "谢谢你", "再见", "拜拜",
|
|
|
|
|
"你是谁", "你叫什么", "你今天开心吗",
|
|
|
|
|
})
|
|
|
|
|
_CHITCHAT_PHRASES = ("今天天气", "讲个笑话", "你几岁", "你开心吗", "你忙吗")
|
2026-09-11 16:11:30 +08:00
|
|
|
_REFERENCE_PHRASES = ("这个", "那个", "它的", "刚才", "上面", "前面", "这只", "那只")
|
2026-09-10 19:51:08 +08:00
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def classify(cls, message: str) -> CustomerServiceRoute:
|
|
|
|
|
normalized = message.strip().lower()
|
2026-09-11 16:11:30 +08:00
|
|
|
if (cls._contains(normalized, cls._SECURITY_KEYWORDS)
|
|
|
|
|
or cls._contains(normalized, cls._SECURITY_DISCLOSURE_PHRASES)):
|
2026-09-10 19:51:08 +08:00
|
|
|
return CustomerServiceRoute(intent="security_notice")
|
2026-09-11 16:11:30 +08:00
|
|
|
if cls._contains(normalized, cls._PROMPT_INJECTION_KEYWORDS):
|
|
|
|
|
return CustomerServiceRoute(intent="compliance_refusal")
|
2026-09-10 19:51:08 +08:00
|
|
|
if cls._contains(normalized, cls._COMPLIANCE_KEYWORDS):
|
|
|
|
|
return CustomerServiceRoute(intent="compliance_refusal")
|
|
|
|
|
if cls._contains(normalized, cls._ACCOUNT_KEYWORDS):
|
|
|
|
|
return CustomerServiceRoute(intent="account_entry")
|
|
|
|
|
if cls._contains(normalized, cls._HUMAN_TRANSFER_KEYWORDS):
|
|
|
|
|
return CustomerServiceRoute(intent="human_transfer")
|
|
|
|
|
if cls._is_chitchat(normalized):
|
|
|
|
|
return CustomerServiceRoute(intent="chitchat", is_chitchat=True)
|
|
|
|
|
if cls._contains(normalized, cls._POLICY_KEYWORDS):
|
|
|
|
|
return CustomerServiceRoute(
|
2026-09-11 16:11:30 +08:00
|
|
|
intent="public_knowledge", knowledge_intents=("policy_explain",),
|
|
|
|
|
requires_context=cls._contains(normalized, cls._REFERENCE_PHRASES),
|
2026-09-10 19:51:08 +08:00
|
|
|
)
|
|
|
|
|
if cls._contains(normalized, cls._PRODUCT_KEYWORDS):
|
|
|
|
|
return CustomerServiceRoute(
|
2026-09-11 16:11:30 +08:00
|
|
|
intent="public_knowledge", knowledge_intents=("product_inquiry",),
|
|
|
|
|
requires_context=cls._contains(normalized, cls._REFERENCE_PHRASES),
|
2026-09-10 19:51:08 +08:00
|
|
|
)
|
|
|
|
|
return CustomerServiceRoute(intent="public_knowledge", knowledge_intents=("faq",))
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def chitchat_streak(cls, prior_messages: Sequence[str], message: str) -> int:
|
|
|
|
|
"""返回当前消息在同一会话中连续闲聊的次数,最大只需记录到第五句。"""
|
|
|
|
|
if not cls._is_chitchat(message.strip().lower()):
|
|
|
|
|
return 0
|
|
|
|
|
streak = 1
|
|
|
|
|
for prior_message in reversed(prior_messages):
|
|
|
|
|
if not cls._is_chitchat(prior_message.strip().lower()):
|
|
|
|
|
break
|
|
|
|
|
streak += 1
|
|
|
|
|
if streak == 5:
|
|
|
|
|
break
|
|
|
|
|
return streak
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _contains(message: str, keywords: tuple[str, ...]) -> bool:
|
|
|
|
|
return any(keyword in message for keyword in keywords)
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _is_chitchat(cls, message: str) -> bool:
|
|
|
|
|
return message in cls._CHITCHAT_MESSAGES or cls._contains(message, cls._CHITCHAT_PHRASES)
|