ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

从“你说得对”到智能兜底:Python规则引擎设计对话防重复策略

从“你说得对”到智能兜底:Python规则引擎设计对话防重复策略 “先生你说得都对……”当用户带着情绪说出这句话时机器人再回一句“你说的一点都对先生。”结果可能是对话框又多了三句同样的话。很多时候这种问题并不是模型不好而是“兜底话术”设计得太随意没有判断上下文、没有重复抑制、没有人工接管提示。这篇文章我会从一句常见的“兜底回复”——“你说的一点都对先生”——出发用一个完整的 Python 项目演示如何把这种固定话术从“写死在 if 里”升级成可配置、可监控、可防重复的轻量级规则引擎。代码不复杂但设计思路同样适用于企业微信机器人、客服机器人、内部 RPA 助手等场景。1. 一句话背后的新问题为什么兜底回复需要设计1.1 什么是兜底回复无论传统 NLP 意图识别还是现在的大模型对话系统都有一个绕不开的问题某条用户消息不能被准确识别或者离线对话机器人在没有命中任何规则时总要返回点什么。这个“不知道说什么时随便说的话”就是兜底回复。常见的兜底话术包括我没有完全理解您的意思您可以换一种说法吗 这个问题我先记录一下稍后由人工客服跟进。 你说的一点都对先生。最后一句看起来礼貌但在真实系统里非常危险。如果用户在表达不满机器人的回答依然客客气气却没有实际解决任何问题用户会认为系统“阴阳怪气”。更严重的是一旦兜底逻辑写死用户连续发几条消息机器人却不断重复同一句话工单还没生成用户体验已经崩了。1.2 我们要解决的四个问题以一个在线客服示例项目为例我希望最终代码实现以下能力对用户输入先做意图判断而不是全部落到“你说得对”这个兜底分支对未识别内容返回兜底话术并对兜底次数做好记录同一用户短时间内连续触发多次兜底时自动换成人工接管提示话术与代码分离后续修改文案不需要重新发版。围绕这四点我会把完整工程拆成模块来实现。1.3 适合阅读本文的读者正在学习 Flask/FastAPI 接口开发的 Python 初学者需要给内部聊天机器人设计“答非所问”策略的研发同学想把简单 QA 规则从 Web 框架的if else中解放出来的朋友。读完你会掌握文本归一化、规则意图识别、会话兜底频控的设计方法并得到一个能直接运行的最小示例。2. 环境准备与项目总体设计2.1 运行环境说明由于标题涉及的是通用对话服务设计我只使用常见的 Python 基础库和轻量 Web 框架。你需要准备以下环境操作系统Windows 10/11、macOS 或 Linux 均可 Python3.8 及以上 依赖fastapi、uvicorn版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。Python 3.8 以下版本对类型注解和dataclass的支持不够好建议直接装 Python 3.9。如果没有特别要求不需要安装机器学习库也不用申请大模型 API规则引擎已经能完成绝大多数基础场景。2.2 项目结构fallback_engine/ ├── main.py # FastAPI 入口 ├── service.py # 核心对话逻辑 ├── config.py # 话术与规则配置 ├── requirements.txt # 依赖文件 └── README.md # 项目说明把代码按文件拆开是为了让“规则配置”和“对话服务”互相独立。后面如果运营人员要改一句“先生您是否可以提供订单号”不用去代码里搜索字符串直接改config.py或数据库配置即可。2.3 一次请求的处理链路一次完整的机器人回复请求大致如下用户输入文本 ↓ 文本预处理去空格、统一标点 ↓ 意图关键词匹配 ↓ 命中 → 返回对应话术 未命中 → 返回兜底话术 ↓ 兜底次数检查 ↓ 输出结果 日志这个流程与复杂 NLP 平台的区别是意图识别部分暂时用关键词规则实现。优点是轻量、可解释、方便单测缺点是它只能识别预设说法。所以在工程章节我会保留一个扩展入口方便以后切换成向量检索或大模型意图分类。3. 核心代码拆解从规则引擎到兜底防重复3.1 预处理先做文本归一化多数用户输入包含多余空格、全角/半角标点、大小写不一致等问题。例如你说的一点都对 先生 你说的一点都对先生 你说的一点都对 Mr.如果代码对每条原文本直接做匹配规则会爆炸。因此第一步是归一化。先编写text_utils.py。这不是项目结构里的必需文件但单独拆出来更清晰。# text_utils.py import re import unicodedata def normalize_text(text: str) - str: 对用户输入做轻度归一化 1. NFC 标准化把全角字符转换成标准形式字符 2. 去除首尾空白 3. 将连续空白合并为单个空格 4. 统一常见中文标点 if not text: return # NFC 标准化兼容全角、半角字符 text unicodedata.normalize(NFC, text) # 把中文逗号、感叹号、句号统一为英文标点 text text.replace(, ,) text text.replace(, !) text text.replace(。, .) text text.replace(, ?) text text.replace(, ;) # 合并空白 text re.sub(r\s, , text).strip() return text if __name__ __main__: samples [ 你说的一点都对 先生, 你说的一点都对先生, 你好 我现在 有问题, ] for sample in samples: print(repr(normalize_text(sample)))为什么要保留英文逗号后再匹配因为归一化就是为了让后续匹配逻辑不需要关心标点差异。把中文逗号统一为英文逗号后规则里的关键词只需要写一种形式即可。3.2 配置化话术让文案与代码分离接着我们定义话术配置。这是一份演示配置后续可以把内容迁到 MySQL、Redis 或 Apollo 中。# config.py from dataclasses import dataclass, field from typing import List dataclass class IntentRule: 一条意图规则 intent_name: str # 意图名称如 praise、query_order keywords: List[str] # 触发的关键词列表 priority: int 10 # 优先级越大越靠前 reply_template: str # 当前默认回复模板 need_human: bool False # 是否需要人工接入 dataclass class BotConfig: 机器人配置兜底话术与频控参数 # 最大允许连续兜底次数 max_fallback_times: int 3 # 时间窗口单位秒 fallback_window_seconds: int 60 # 默认未命中意图时的兜底话术 fallback_reply: str 你说的一点都对先生。 # 超过频控后需要提示人工介入的话术 human_handover_reply: str ( 这个问题好像没有被我的知识库覆盖到 为了避免反复打扰我先把你的问题转给人工客服。 请稍候也可以直接提供订单号或联系方式。 ) # 意图规则表 intent_rules: List[IntentRule] field(default_factorylist) def build_default_config() - BotConfig: 构造默认配置真实项目可改为从配置中心加载 rules [ IntentRule( intent_namegreet, keywords[你好, 您好, 早上好, 下午好, hello, hi], priority100, reply_template您好我是智能助手请描述您遇到的问题。, ), IntentRule( intent_namepraise, keywords[你说得对, 说得对, 都对, 太对了, 先生], priority80, reply_template感谢您的肯定我会继续为您服务。, ), IntentRule( intent_namequery_human, keywords[人工, 转人工, 客服, 真人], priority200, reply_template正在为您转接人工客服请稍候。, need_humanTrue, ), IntentRule( intent_namenegative_feedback, keywords[垃圾, 很差, 不解决, 投诉, 没用], priority150, reply_template非常抱歉给您带来不好的体验我会将问题记录并优先反馈。, need_humanTrue, ), ] config BotConfig() config.intent_rules rules return config这里把关键字分成多组。你可以看到praise意图中有“先生”这样用户输入“先生你说得对”时会先落入表扬意图而不是走到兜底。设置负面情绪意图等级比普通表扬更高是为了防止用户在投诉时被一句话敷衍过去。3.3 对话状态与兜底计数要实现“同一用户短时间内连续触发多次兜底”必须留一个会话级状态容器。实际生产环境会使用 Redis 保存状态因为接口服务通常是多实例部署。本地演示用 Python 字典就可以说明思路# session_store.py import threading import time from collections import defaultdict, deque from typing import Dict class MemorySessionStore: 简单的基于内存的会话状态存储适用于单机演示。 生产环境建议替换为 Redis。 def __init__(self): self._lock threading.Lock() self._fallback_times: Dict[str, deque] defaultdict(deque) def register_fallback(self, session_id: str, current_ts: float None): 记录一次兜底回复触发时间 ts current_ts or time.time() with self._lock: self._fallback_times[session_id].append(ts) def count_recent_fallbacks( self, session_id: str, window_seconds: int 60, current_ts: float None ) - int: 统计当前时间窗口内兜底次数 ts current_ts or time.time() with self._lock: q self._fallback_times[session_id] while q and ts - q[0] window_seconds: q.popleft() return len(q) def clear(self, session_id: str): 清除指定会话状态 with self._lock: self._fallback_times.pop(session_id, None)这段代码的关键是deque双端队列每次兜底时向右追加时间戳统计时先把超过时间窗口的旧时间戳从左边弹出剩下的就是最近窗口里的次数。threading.Lock保证在并发接口调用时不出现队列错乱。3.4 意图识别与回复生成下面编写核心的service.py。这个文件会把前面的文本归一化、话术配置、会话状态组合起来。# service.py from config import BotConfig, build_default_config, IntentRule from text_utils import normalize_text from session_store import MemorySessionStore class ChatBotService: def __init__(self, config: BotConfig None): self.config config or build_default_config() self.session_store MemorySessionStore() staticmethod def _text_hit_rule(text: str, rule: IntentRule) - bool: 判断文本是否命中某条规则。 这里采用最简单的“包含关键词”策略 可根据实际项目替换为正则或语义向量匹配。 lowered text.lower() return any(kw.lower() in lowered for kw in rule.keywords) def _match_intent(self, text: str) - IntentRule | None: 按照优先级从高到低匹配意图。 priority 数值大的规则优先。 rules sorted(self.config.intent_rules, keylambda r: r.priority, reverseTrue) for rule in rules: if self._text_hit_rule(text, rule): return rule return None def handle_message(self, session_id: str, user_message: str) - dict: 用户消息处理入口返回给前端的内容 clean_text normalize_text(user_message) if not clean_text: return { reply: 您好我暂时没有收到有效内容请描述您的问题。, intent: empty, fallback: True, } # 第一步尝试匹配业务意图 rule self._match_intent(clean_text) if rule: # 明确命中意图清除历史兜底计数至少说明问题被接住了 self.session_store.clear(session_id) reply rule.reply_template return { reply: reply, intent: rule.intent_name, fallback: False, need_human: rule.need_human, } # 第二步未命中任何意图进入兜底逻辑 fallback_times self.session_store.count_recent_fallbacks( session_id, window_secondsself.config.fallback_window_seconds, ) # 如果最近已经兜底过太多次需要切换人工接入提示 if fallback_times self.config.max_fallback_times: self.session_store.register_fallback(session_id) return { reply: self.config.human_handover_reply, intent: human_handover, fallback: True, need_human: True, } # 正常兜底 self.session_store.register_fallback(session_id) return { reply: self.config.fallback_reply, intent: fallback, fallback: True, need_human: False, }这里需要重点说明两个边界第一我在命中真实意图后会clear兜底计数。这样用户先正常提问两次又闲聊一句话不会立刻被误判为“持续答非所问”。第二当用户已经连续触发多次兜底我会继续register_fallback。这是为了让后续日志能完整记录用户在完全未被满足前一共发了多少轮消息而不是只记到“转人工”就停止。3.5 使用 FastAPI 暴露接口现在把核心服务包装成 HTTP 接口。# main.py from fastapi import FastAPI from pydantic import BaseModel from service import ChatBotService app FastAPI(titleFallback Reply Engine) chat_service ChatBotService() class ChatRequest(BaseModel): session_id: str message: str app.post(/api/chat) def chat(req: ChatRequest): 对话接口 return chat_service.handle_message(req.session_id, req.message) app.get(/healthz) def healthz(): return {status: ok}到这里一个可运行的最小对话服务就完成了。它不会真的理解世界但它做到了把“你说的一点都对先生”这类话术作为受控资源管理起来而不是在代码里到处粘贴字符串。4. 完整运行与验证4.1 准备依赖文件新建requirements.txt并安装依赖。fastapi uvicorn安装命令pip install -r requirements.txt4.2 启动服务在项目根目录执行uvicorn main:app --host 0.0.0.0 --port 8000如果你使用 Windows出现模块找不到错误时先确认当前命令行是否进入虚拟环境。4.3 使用接口测试打开新终端执行几个curl请求。请求 1正常问候。curl -X POST http://127.0.0.1:8000/api/chat -H Content-Type: application/json -d {\session_id\: \u_001\, \message\: \你好请问在吗\}预期返回{ reply: 您好我是智能助手请描述您遇到的问题。, intent: greet, fallback: false, need_human: false }请求 2发送“先生你说得一点都对”。curl -X POST http://127.0.0.1:8000/api/chat -H Content-Type: application/json -d {\session_id\: \u_001\, \message\: \先生你说得一点都对\}预期返回{ reply: 感谢您的肯定我会继续为您服务。, intent: praise, fallback: false, need_human: false }请求 3发送完全无关内容例如“今天天气怎么样啊”。curl -X POST http://127.0.0.1:8000/api/chat -H Content-Type: application/json -d {\session_id\: \u_002\, \message\: \今天天气怎么样啊\}预期返回{ reply: 你说的一点都对先生。, intent: fallback, fallback: true, need_human: false }请求 4、5、6对同一个session_id连续发送三条未命中内容。curl -X POST http://127.0.0.1:8000/api/chat -H Content-Type: application/json -d {\session_id\: \u_003\, \message\: \随便聊聊\} curl -X POST http://127.0.0.1:8000/api/chat -H Content-Type: application/json -d {\session_id\: \u_003\, \message\: \再随便聊聊\} curl -X POST http://127.0.0.1:8000/api/chat -H Content-Type: application/json -d {\session_id\: \u_003\, \message\: \继续随便聊聊\}到第三次预期返回{ reply: 这个问题好像没有被我的知识库覆盖到为了避免反复打扰我先把你的问题转给人工客服。请稍候也可以直接提供订单号或联系方式。, intent: human_handover, fallback: true, need_human: true }这个结果说明频控逻辑已经生效机器人不会无限重复同一句“你说得对”。4.4 加一个命令行测试入口没有 HTTP 客户端时也可以直接在service.py末尾添加测试代码体验效果。if __name__ __main__: svc ChatBotService() messages [你好, 你说得对先生, 今天聊点别的, 再聊点别的, 还聊点别的] for i, msg in enumerate(messages): res svc.handle_message(demo_user, msg) print(f用户: {msg}) print(f机器人: {res[reply]}) print(f意图: {res[intent]}, fallback: {res[fallback]}) print(---)运行结果类似用户: 你好 机器人: 您好我是智能助手请描述您遇到的问题。 意图: greet, fallback: False --- 用户: 你说得对先生 机器人: 感谢您的肯定我会继续为您服务。 意图: praise, fallback: False --- 用户: 今天聊点别的 机器人: 你说的一点都对先生。 意图: fallback, fallback: True --- 用户: 再聊点别的 机器人: 你说的一点都对先生。 意图: fallback, fallback: True --- 用户: 还聊点别的 机器人: 这个问题好像没有被我的知识库覆盖到为了避免反复打扰我先把你的问题转给人工客服。请稍候也可以直接提供订单号或联系方式。 意图: human_handover, fallback: True5. 常见问题与排查方向下面整理开发这类规则引擎时会遇到的几个典型问题。问题现象常见原因解决思路用户发送“先生你说得对”结果走了兜底规则关键词没覆盖“先生”在前的情况检查文本归一化结果并增加“先生”为 praise 意图关键词连续兜底次数没有生效每次请求都使用了不同session_id确认前端是否重复生成会话 ID生产建议使用 Redis 保存状态Windows 命令行输出中文乱码终端编码不是 UTF-8在cmd中执行chcp 65001或在 Python 启动时设置PYTHONIOENCODINGutf-8收到包含政治敏感词的不安全输入文本没有做安全过滤在意图识别前增加敏感词过滤和合规审核模块正常业务偶尔被误判为兜底关键词过少、同义表达多增加同义词词典或用语义向量召回替代纯包含匹配意图规则冲突多条规则都能命中优先级设计不够清晰为负面反馈、转人工等强业务意图设置更高优先级重点说一下优先级问题。在上面的config.py中我把negative_feedback的priority150设得比praise的 80 高。这么做的原因是用户可能在一条消息里夹杂表扬和批评例如“你们系统很垃圾不过你说得对”。如果表扬优先级更高机器人就会忽略不满情绪。无论何时负面情绪都应该优先响应。6. 工程落地中的最佳实践6.1 不要在生产环境硬编码话术上面的 demo 为了讲解方便把所有话术放进了config.py。线上环境建议使用配置中心或后台管理系统管理话术并保留版本记录和历史生效人。修改一句“你说得对先生”不需要改代码也不需要重启服务。如果使用 Spring Cloud 体系可以接入 Apollo 的配置热更新如果纯 Python 体系可以使用 Redis 存储键值对再配合定时刷新或事件监听。6.2 兜底回复里不要放无意义表达很多团队只把“你说得对先生”当作一个幽默测试用例却没想到它会真实出现在生产环境。无意义兜底回复会让用户觉得被嘲讽。最低限度的兜底回复应包含三个信息识别状态我暂时没懂下一步动作请换个说法或者转人工可能的解决入口提供订单号、选择菜单。比如我还没有完全理解您的意思。您可以直接输入“人工”联系客服也可以补充订单号或手机号我会继续为您查询。6.3 每次兜底必须记日志很多系统最后复盘“为什么用户和机器人互相复读”都发现没有日志。兜底日志至少要记录以下字段{ session_id: u_003, user_message: 随便聊聊, normalized_text: 随便聊聊, intent: fallback, reply: 你说的一点都对先生。, fallback_times: 2, timestamp: 2025-01-20T12:00:0008:00 }有了这份日志你才能回答三个问题用户是因为哪个词没有命中而兜底兜底前系统已经失败了几轮最后是人工介入解决的还是用户主动离开的6.4 不要把所有失败都交给同一句话一段成熟的兜底策略应该分成多级第一轮未命中返回“请您再描述一次”第二轮未命中返回相关 FAQ 推荐第三轮未命中直接转人工同时携带完整上下文如果接口超时或后端异常返回“系统繁忙请稍后再试”并触发告警。“你说的一点都对先生”在真实系统里根本不应该作为业务兜底它更适合作为调试机器人连通性的测试口令。如果你想保留它请至少放在一个单独的debug意图下而不是让它在生产环境向外输出。6.5 关键词规则与语义模型的取舍规则引擎的优势是可控和可解释但缺点是遇到长尾表达就失灵。建议的路线是冷启动阶段先用规则覆盖高频高频场景数据积累阶段记录所有未命中样本人工打标规模化阶段引入向量匹配或大模型意图分类无论采用哪种模型都要保留人工兜底、频控和日志。也就是说不要让模棱两可的模型回复成为压垮用户体验的最后一根稻草。规则引擎作为最低层的安全网永远不会过时。7. 总结与后续扩展方向这篇文章围绕一句“你说的一点都对先生”展开实现了一个轻量对话兜底引擎。核心收获可以归纳为用户输入要先做归一化统一标点、空格和大小写否则规则关键词很容易漏匹配意图规则要区分优先级投诉、转人工等强业务意图必须高于普通寒暄兜底回复不能无限重复需要用滑动窗口做频控和人工接管文案配置要与代码分离并把每次兜底记录成结构化日志。如果你想继续深入研究可以从这几个方向入手把MemorySessionStore替换成 Redis 实现支持多实例部署把关键词包含匹配升级为正则模板例如“我要查询订单{order_no}”给意图规则增加一个置信度阈值例如多次命中但置信度低时也走兜底在对话接口前面增加敏感词过滤模块保障内容安全。对话系统从来不是“能回话”就结束了真正难的是“知道什么时候该闭嘴、什么时候该找人、什么时候只能老老实实承认不知道”。希望这套兜底设计思路能帮你和你的机器人少一点“你说得对”式的无效沟通。如果本文中的代码或思路对你有帮助建议收藏备用也可以对照自己的项目试着改造一版。
返回列表