Files
xianjing-ai/engine/app/intent.py
T
Cursor Agentandhaijd 625a6a3a9d feat: 客服对话支持寒暄回复与跑题拒答
问候不再写入槽位;明显无关问题直接拒绝,知识库未命中时明确说明无法回答,并继续合理追问。

Co-authored-by: haijd <haijd@163.com>
2026-09-20 05:52:22 +00:00

117 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import re
# 寒暄:不写槽、先回问候再追问
_GREETING_RE = re.compile(
r"^\s*(hi|hello|hey|hola|yo|你好|您好|哈喽|嗨|早上好|上午好|中午好|下午好|晚上好|早|在吗|在不在)[\s!!.。~~??]*$",
re.I,
)
# 跳过/暂无类有效回答
_SKIP_RE = re.compile(r"暂无|没有|无|不清楚|不确定|随便|都可以|未知|n/?a|none|no\b", re.I)
# 鲜境业务相关词:冷链温控 / 官网产品线
_DOMAIN_RE = re.compile(
r"冷链|冷库|仓储|仓库|运输|配送|保温|冷冻|冷藏|温控|温度|湿度|"
r"药品|疫苗|生鲜|实验室|产线|户外|监测|告警|报警|"
r"TagPlus|BlueTag|GAEA|Fresh\s*Tag|USB|PDF|蓝牙|网关|记录仪|"
r"不开箱|行程|温区|WiFi|4G|LAN|API|方案|产品|报价|选型|"
r"实时|读数|报告|传感器|物联网|物联|预算|网络|对接|系统|次数|可重复",
re.I,
)
# 明显跑题话题
_OFFTOPIC_RE = re.compile(
r"天气|气温|下雨|新闻|股票|基金|彩票|足球|篮球|电影|电视剧|游戏|"
r"笑话|段子|星座|运势|做饭|菜谱|旅游|机票|酒店|恋爱|八卦|"
r"政治|选举|战争|加密货币|比特币|chatgpt|写诗|作文|数学题|"
r"编程作业|leetcode|今天吃什么",
re.I,
)
_QUESTION_RE = re.compile(r"[??]|什么是|怎么|如何|能否|可以吗|多少|哪[个些]|是否|介绍一下|有没有")
def is_greeting(text: str) -> bool:
return bool(_GREETING_RE.match((text or "").strip()))
def is_skip_answer(text: str) -> bool:
t = (text or "").strip()
if not t:
return False
if _SKIP_RE.fullmatch(t):
return True
return t in {"没有要求", "无要求", "没什么", "先不确定"}
def is_domain_related(text: str) -> bool:
return bool(_DOMAIN_RE.search(text or ""))
def looks_like_question(text: str) -> bool:
return bool(_QUESTION_RE.search(text or ""))
def has_offtopic_topic(text: str) -> bool:
return bool(_OFFTOPIC_RE.search(text or ""))
def looks_off_topic(text: str) -> bool:
"""明显与冷链方案无关的输入;寒暄单独处理,不算跑题。"""
t = (text or "").strip()
if not t or is_greeting(t) or is_skip_answer(t):
return False
if is_domain_related(t):
return False
if _OFFTOPIC_RE.search(t):
return True
# 纯英文闲聊 / 极短无意义且不像在答槽
if re.fullmatch(r"[a-zA-Z\s!.?~]{1,40}", t) and not is_domain_related(t):
lower = t.lower()
if any(w in lower for w in ("hello", "hi", "hey", "thanks", "bye", "ok", "test")):
return False # greeting/ack 另判
if looks_like_question(t):
return True
# 较长文本却无任何业务词 → 视为跑题
if len(t) >= 8 and not is_domain_related(t) and looks_like_question(t):
return True
if len(t) >= 16 and not is_domain_related(t):
return True
return False
def should_write_slot(text: str) -> bool:
"""是否允许本轮用户话写入槽位。"""
t = (text or "").strip()
if not t or is_greeting(t):
return False
if looks_off_topic(t):
return False
return True
def greeting_reply(next_prompt: str | None) -> str:
base = "你好!我是鲜境方案客服,可以帮您梳理冷链温控场景并匹配官网产品组合。"
if next_prompt:
return f"{base}\n\n{next_prompt}"
return f"{base}\n\n您可以直接描述场景,例如:「冷库仓储,要实时告警」。"
def refuse_reply(reason: str = "off_topic", next_prompt: str | None = None) -> str:
if reason == "kb_miss":
msg = (
"抱歉,我无法回答这个问题。"
"当前知识库里没有足够相关依据,请换一个与鲜境冷链温控产品/场景相关的问题,"
"或描述您的应用现场,我再帮您追问选型。"
)
else:
msg = (
"抱歉,我无法回答这类问题。"
"我主要协助鲜境物联的冷链温控方案咨询(运输记录、蓝牙读数、仓储网关等)。"
)
if next_prompt:
return f"{msg}\n\n我们继续:{next_prompt}"
return msg