支持Web/Discord/WhatsApp多渠道,集成HITL、情感检测判断转人工时机,六阶段RAG+FAISS检索提升回答准确性。
每个人都遇到过那种客服机器人——它一遍遍说"抱歉,我没听懂",而你却越来越火大。
我想做的是相反的东西:一个能回答它能回答的问题、在需要真人时安静退出的机器人。它运行在 Web、Discord 和 WhatsApp 上,代码开源:github.com/Shahzaib30/ai-support-agent
这个系统有四层。客户从任意渠道与它对话,一个 FastAPI 后端处理所有事务,一些服务存储状态和发送告警,监控则监视着一切。

FastAPI 核心中,答案引擎前面有三重关卡:HITL 关卡(human-in-the-loop,检查是否已经有客服在处理对话)、显式升级检查,以及情绪升级检查。PostgreSQL 记录对话历史,Redis 缓存答案,一切通过一条 Docker Compose 命令即可运行。
大多数 RAG 机器人搜一次就完事。我的系统把每个问题过六道关:

问题进来后,首先经过一个查询压缩器(query condenser)重写它。客户会说"那打折商品呢?"这样的话,单独看毫无意义。一个 DeepSeek 步骤先把后续问题转成独立问题。
然后两个搜索同时跑。FAISS 配合 BGE-small 嵌入模型按语义查找段落。BM25 按精确词汇查找。每个返回各自的前 20 条。
Reciprocal Rank Fusion 将它们合并成一个 20 条候选的列表。
交叉编码器(cross-encoder)重新排序。它把问题和每个段落一起读取,只有前 5 条通过。
DeepSeek 用这 5 个片段、聊天历史和长期记忆来撰写答案。
为什么要两个搜索?向量搜索理解语义,但在精确内容上弱——比如价格、邮箱、电话号码。问"Pro 方案多少钱?"它可能返回一段讲定价策略的好话,而不是带具体数字的那条。BM25 恰恰相反。两者合在一起互补,对于固定事实类问题,混合搜索明显优于单独使用向量搜索。
这里有两个触发器,而且我有意用不同方式实现。
每条消息都会以一个严格 prompt 发给 DeepSeek API,回复必须是 JSON 格式,包含一个标签和 -1.0 到 1.0 的分数:
Rules:
- negative: customer is CLEARLY angry, frustrated, or complaining
- neutral: casual phrases, short responses, confusion, greetings
- positive: happy, satisfied, thankful
"oh no" → neutral
"wtf" → neutral (not clearly angry enough)
"this is terrible I want a refund NOW" → negative
Reply ONLY with JSON: {"label": ..., "score": ...}
这些示例才是让这套机制生效的关键。人们会随口说"oh no"和"wtf",所以它们算中立。只有明确的愤怒才算负面。
一条愤怒消息不会触发任何操作。Bot 需要连续三条负面消息才会升级,所以冷静下来的客户不会因为早期的坏情绪而受罚。
def wants_human(text: str) -> bool:
text = text.lower()
return any(p in text for p in HUMAN_REQUEST_PHRASES)
# "talk to a human", "real person", "live agent", ...
如果消息中包含"talk to a human"或"real person"这样的短语,Bot 立即升级。要调用 API 来理解"I want a human"既费钱又费时间,所以一个简单的短语列表免费搞定。
这成了我的主要原则:需要判断时用 LLM,规则清晰时用纯代码。
在回答任何问题之前,HITL 关卡会检查对话处于什么状态:

AI Active:正常 RAG 流程,Bot 回复。
Human Pending:升级已发出但还没有人工回复。如果 5 分钟内无人应答,Bot 会收回对话,以免客户干等。
Human Active:人工正在处理,所以 Bot 保持沉默,并把客户的消息转发给客服。客户永远不会收到两个回复。
当问题解决后,客户直接回到与 AI 对话。
发送告警很简单。两个触发器中任意一个触发时,Bot 会向 Slack 的 #escalations 频道发帖,包含客户、渠道、原因、情绪分数和对话 ID:

人工在帖子中回复,不需要特殊格式。客户的新消息也会出现在这个线程里,所以整个对话在一个地方完成。人工只需输入 resolved 就可以把对话交回给 Bot。
读取 Slack 回复并把它发给 Discord 上的某个人,这是最棘手的部分。我用 n8n 工作流解决了:

Slack Trigger 在线程中有新消息时触发。
Is real user reply 检查这是真人而不是 Bot 自己的消息。
Extract conversation_id and message 提取出 API 需要的两个东西。
Is resolved 分支:Yes:调用 API 在 PostgreSQL 中解决对话。No:调用 API(/human_reply)存储回复。
Discord 端,一个小 Bot 每 5 秒检查一次 API,把新的人工回复 DM 给客户。客户看到的是这样的:

客户的消息被确认已发送给客服,专员的回复出现在对话中,一旦问题解决 Bot 会告知客户并重新开始回复。
Grafana 仪表板有六个面板:总消息数、总升级数、缓存命中率、平均 RAG 响应时间、活跃对话数、缓存命中与未命中。

LangSmith 追踪每个请求,所以我可以打开任意答案,准确看到管道内部发生了什么:

我的测试运行(23 条追踪)中,端到端答案耗时约 1.4 到 2.4 秒。这些数字来自我自己的测试对话,不是真实客户。
并非所有事情都需要 LLM。需要判断时用它,比如读情绪。规则清晰时用纯代码,比如短语匹配。
追踪节省大量时间。LangSmith 让我看到每个请求实际做了什么,所以我不再猜测哪里出了问题。
连接各种工具才是工作的主体。Slack、Discord、n8n 和数据库各有各的脾气,真正的功夫花在让它们互相通话上。
需要快速集成时用 n8n。它让棘手的步骤变得简单,并保持工作流对其他人可读。当需要深度定制、低延迟或自定义逻辑时,绕过它。对于那些场景,直接写代码。
Code: github.com/Shahzaib30/ai-support-agent
Upwork: My Upwork profile
Linkedin: My Linkedin profile
我是一名全栈 AI 工程师,构建 AI 代理、RAG 系统和自动化。如果你也在做类似的东西,欢迎联系我。
你遇到过最烂的客服机器人是什么样的?在评论区告诉我。