企业微信 AI 智能客服实战:Spring Boot+Vue 接入豆包、扣子、DeepSeek

企业微信AI智能客服别让人通宵盯着回:Spring Boot + Vue 把豆包、DeepSeek、扣子接到消息回调里

摘要:私域客服的痛点不是「没有大模型」,而是客户随时来、标准化问题占人工、人一接手机器人必须闭嘴。本文用 Spring Boot + Vue 把豆包、DeepSeek、扣子接到企业微信消息回调里:回调进线程池、厂家可插拔、口令/关键词/AI/自动拉人四段互斥、暂停键放 Redis。全文含真实菜单名、源码和现网边界,可直接照着落地。

目录

  • [1. 产品里三个入口,别搞混](#1. 产品里三个入口,别搞混)
  • [2. 技术架构:回调进线程池,厂家可插拔](#2. 技术架构:回调进线程池,厂家可插拔)
  • [3. 一条客户消息,服务端怎么走](#3. 一条客户消息,服务端怎么走)
    • [3.1 回调落库后立刻调度](#3.1 回调落库后立刻调度)
    • [3.2 四段互斥链](#3.2 四段互斥链)
    • [3.3 tryAiReply 真正会跳过的情况](#3.3 tryAiReply 真正会跳过的情况)
  • [4. 智能体怎么存、怎么路由](#4. 智能体怎么存、怎么路由)
  • [5. 人机协同:暂停键在 Redis,不在会话表](#5. 人机协同:暂停键在 Redis,不在会话表)
  • [6. 现网边界(写进文章比吹得满更有转化)](#6. 现网边界(写进文章比吹得满更有转化))
  • [7. 落地清单(给要对着做的同学)](#7. 落地清单(给要对着做的同学))
  • [8. 总结](#8. 总结)

技术栈说明

本教程全文使用的底层API调用地址:https://wechatapi.apifox.cn/

代码调用示例参考官网:https://www.jikehudong.com/

开发语言:c + java

开发框架:Spring Boot + Vue

私域客服的真实痛点不是「没有大模型」,而是这三件事叠在一起:

  1. 客户随时会来:晚上 9 点问价格、周末问怎么进群,人不可能 24 小时盯着手机。
  2. 标准化问题不该占人工:价格、地址、活动口令,本来就该规则先挡一层。
  3. 人一旦接手,机器人必须闭嘴:客服已经在工作台打字了,AI 再回一句,客户会觉得你们在演戏。

很多团队的做法是:ChatGPT 开个网页,复制粘贴到企微。这解决不了多账号、会话暂停、下班停机、口令入群抢消息这些问题。

我们在 极客聚合(企业微信聚合平台) 里把这条链路做成了现网功能:智能体在后台配一次,账号勾一下「AI 接管」,客户发来的文本走回调,服务端异步调厂家,再用 SendTextMsg 打回手机。下面全部是真实菜单名和源码,没有 GPT / 通义,厂家只有 豆包、DeepSeek、扣子


1. 产品里三个入口,别搞混

AI 客服不是「一个大开关」,而是三层配置:

入口 菜单路径 干什么
智能体目录 AI客服设置 配厂家、Key、模型、人设,测连通
账号绑定 企微账号管理 → 账号设置 → AI 智能回复 这个号开不开 AI、绑哪个智能体、好友还是群
会话接管 客服工作台 标签「AI 接管中 / AI 已暂停」,按钮「暂停 AI / 恢复 AI」

页面上的引导文案写得很直白:先在本页配智能体,再到账号设置里绑定。

点「新建智能体」只有三家,没有「随便填一个 OpenAI 兼容地址」的万能入口。豆包走火山方舟 ep- 接入点,DeepSeek 走 deepseek-chat,扣子走 PAT + Bot ID。

智能体配好后,去 企微账号管理 → 账号设置,先点账号卡片:

进账号后能看到四张能力卡。注意 AI 那张的产品定义------关键词未命中时才由智能体接管,不是大模型包打天下:

点进「AI 智能回复」,开关文案也把人机边界写死了:

AI 接管本账号:关键词 / 口令未命中的文本消息交给智能体回复;工作台人工发送后该会话会暂停 AI

工作台是人工最后一公里。账号绑定 AI 且选中会话后,标题会出现「AI 接管中」,输入栏左侧有「暂停 AI」。人工点发送,前端会把该会话标成已暂停,后端落库时再写 Redis pause key,两边一起保证机器人不再抢话。


2. 技术架构:回调进线程池,厂家可插拔

整体不是「前端调大模型再发给客户」。客户消息先到企微网关,网关把回调打到我们的 Spring Boot,落 Redis / MySQL 之后再异步走自动化。前端只负责配智能体、绑账号、暂停会话。

用 mermaid 把同一张图画成可维护版本:
#mermaid-svg-0avkNbLDlK05oiDE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0avkNbLDlK05oiDE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0avkNbLDlK05oiDE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0avkNbLDlK05oiDE .error-icon{fill:#552222;}#mermaid-svg-0avkNbLDlK05oiDE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0avkNbLDlK05oiDE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0avkNbLDlK05oiDE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0avkNbLDlK05oiDE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0avkNbLDlK05oiDE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0avkNbLDlK05oiDE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0avkNbLDlK05oiDE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0avkNbLDlK05oiDE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0avkNbLDlK05oiDE .marker.cross{stroke:#333333;}#mermaid-svg-0avkNbLDlK05oiDE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0avkNbLDlK05oiDE p{margin:0;}#mermaid-svg-0avkNbLDlK05oiDE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0avkNbLDlK05oiDE .cluster-label text{fill:#333;}#mermaid-svg-0avkNbLDlK05oiDE .cluster-label span{color:#333;}#mermaid-svg-0avkNbLDlK05oiDE .cluster-label span p{background-color:transparent;}#mermaid-svg-0avkNbLDlK05oiDE .label text,#mermaid-svg-0avkNbLDlK05oiDE span{fill:#333;color:#333;}#mermaid-svg-0avkNbLDlK05oiDE .node rect,#mermaid-svg-0avkNbLDlK05oiDE .node circle,#mermaid-svg-0avkNbLDlK05oiDE .node ellipse,#mermaid-svg-0avkNbLDlK05oiDE .node polygon,#mermaid-svg-0avkNbLDlK05oiDE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0avkNbLDlK05oiDE .rough-node .label text,#mermaid-svg-0avkNbLDlK05oiDE .node .label text,#mermaid-svg-0avkNbLDlK05oiDE .image-shape .label,#mermaid-svg-0avkNbLDlK05oiDE .icon-shape .label{text-anchor:middle;}#mermaid-svg-0avkNbLDlK05oiDE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0avkNbLDlK05oiDE .rough-node .label,#mermaid-svg-0avkNbLDlK05oiDE .node .label,#mermaid-svg-0avkNbLDlK05oiDE .image-shape .label,#mermaid-svg-0avkNbLDlK05oiDE .icon-shape .label{text-align:center;}#mermaid-svg-0avkNbLDlK05oiDE .node.clickable{cursor:pointer;}#mermaid-svg-0avkNbLDlK05oiDE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0avkNbLDlK05oiDE .arrowheadPath{fill:#333333;}#mermaid-svg-0avkNbLDlK05oiDE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0avkNbLDlK05oiDE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0avkNbLDlK05oiDE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0avkNbLDlK05oiDE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0avkNbLDlK05oiDE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0avkNbLDlK05oiDE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0avkNbLDlK05oiDE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0avkNbLDlK05oiDE .cluster text{fill:#333;}#mermaid-svg-0avkNbLDlK05oiDE .cluster span{color:#333;}#mermaid-svg-0avkNbLDlK05oiDE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0avkNbLDlK05oiDE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0avkNbLDlK05oiDE rect.text{fill:none;stroke-width:0;}#mermaid-svg-0avkNbLDlK05oiDE .icon-shape,#mermaid-svg-0avkNbLDlK05oiDE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0avkNbLDlK05oiDE .icon-shape p,#mermaid-svg-0avkNbLDlK05oiDE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0avkNbLDlK05oiDE .icon-shape .label rect,#mermaid-svg-0avkNbLDlK05oiDE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0avkNbLDlK05oiDE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0avkNbLDlK05oiDE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0avkNbLDlK05oiDE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 存储
Spring Boot
Vue 管理端
企微网关
企微客户端
私聊客户
群聊
消息回调
SendTextMsg
AI客服设置
账号设置 AI智能回复
工作台 暂停/恢复AI
WxworkCallbackController
WxworkRobotAutomationExecutor

异步线程池 4~16
口令入群 → 关键词 → AI → 自动拉人
AiAgentRouter
豆包 Ark
DeepSeek
扣子 Coze
MySQL

sys_ai_agent

automation_json
Redis

pause / interval / dedup
消息 Redis

关键设计就四条:

  1. 回调不能阻塞scheduleAfterIncomingMessage 丢进独立线程池,避免关键词 / AI / 拉群把第三方回调拖超时。
  2. 规则互斥,不是并行喷。 同一条文本:口令入群命中 → 不再走关键词;关键词命中 → 不再走 AI;AI 回成功 → 不再自动拉人。
  3. 厂家可插拔。 AiAgentRouterproviderAiAgentClientdoubao / deepseek 共用 OpenAI 兼容 chat/completionscoze/v3/chat 非流式 + 轮询。
  4. 人机状态在 Redis。 pause 无 TTL(人停了就一直停,直到点「恢复 AI」);同会话 2 秒间隔;server_id 去重 2 天。

3. 一条客户消息,服务端怎么走

3.1 回调落库后立刻调度

java 复制代码
wxworkMessageRedisService.saveMessage(msg);
wxworkRobotAutomationExecutor.scheduleAfterIncomingMessage(msg);

自动化入口先过滤:无 uuid、本端发出(isSent=1)、非文本、不在工作时间、没有 automation_json,直接 return。工作时间按 Asia/Shanghai 判断,避免 JVM 时区是 UTC 时白天被当成下班。

3.2 四段互斥链

java 复制代码
boolean cmdHit = tryCommandJoin(root.getJSONObject("command"), msg, content.trim(), sender, root);

boolean kwHit = false;
if (!cmdHit) {
    kwHit = tryKeywordReply(root.getJSONObject("keyword"), msg, content, sender);
}

boolean aiHit = false;
if (!cmdHit && !kwHit) {
    aiHit = tryAiReply(root.getJSONObject("ai"), msg, content, sender);
}

if (!cmdHit && !kwHit && !aiHit) {
    tryAutoPull(root.getJSONObject("autopull"), msg, sender);
}

产品语义对应到代码就是:

  • 客户发「进群暗号」→ 调 InvitationToRoomLink,这条消息被消费掉。
  • 客户发「价格」→ 关键词规则回固定文本,不浪费 Token。
  • 上面都没打中 → AI 用最近 20 条文本当上下文。
  • AI 也没回(没开、暂停、范围不符)→ 才考虑「私聊首次消息自动拉群」。

欢迎语是另一套 Handler,不走这条工作时间时钟。别把「加好友 3 秒欢迎语」和大模型混成一个开关。

3.3 tryAiReply 真正会跳过的情况

java 复制代码
if (ai == null || !isJsonTruthy(ai, "enabled")) return false;          // 账号没开 AI
if (ai.getLong("agentId") == null) return false;                      // 没绑智能体
if (!aiScopeMatched(ai, isRoomMsg)) return false;                     // 好友/群聊范围
if (aiSessionService.isPaused(uuid, sendUserid, isRoomMsg)) return false;
if (aiSessionService.alreadyReplied(uuid, msg.getServerId())) return false;
if (!aiSessionService.tryAcquireInterval(uuid, sendUserid, isRoomMsg, 2)) return false;

通过之后:拉最近 20 条文本 → 转成 user/assistantAiAgentRouter.chat → 成功则 SendTextMsg,并把发出去的文本再写入消息 Redis,工作台才能回显。

AI 只回文本 。图片、文件、语音不会丢给大模型,回调里非 msgtype 0/2 在自动化入口就被跳过。


4. 智能体怎么存、怎么路由

表很薄,凭证放 config_json,页面上的 API Key / 模型 / Bot ID 都是展开字段:

sql 复制代码
CREATE TABLE IF NOT EXISTS sys_ai_agent (
    agent_id BIGINT NOT NULL AUTO_INCREMENT,
    agent_name VARCHAR(64) NOT NULL,
    provider VARCHAR(32) NOT NULL COMMENT 'doubao/deepseek/coze',
    enabled CHAR(1) NOT NULL DEFAULT '1',
    config_json MEDIUMTEXT NULL,
    last_error VARCHAR(500) DEFAULT NULL,
    PRIMARY KEY (agent_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI智能体目录';

账号侧绑定不另建表,塞在该企微号的 automation_json

json 复制代码
{
  "keyword": { "enabled": true, "scope": ["customer"], "rules": [] },
  "command": { "enabled": false, "rules": [] },
  "autopull": { "enabled": false, "mode": "all", "delaySec": 0 },
  "ai": {
    "enabled": true,
    "agentId": "3",
    "scope": ["customer"]
  }
}

路由只有十几行,加第四家厂家就是再实现一个 AiAgentClient

java 复制代码
@Component
public class AiAgentRouter {
    @Autowired
    private List<AiAgentClient> clients;

    public AiChatResult chat(SysAiAgent agent, List<AiChatMessage> history,
                             String userText, String conversationId) throws Exception {
        for (AiAgentClient c : clients) {
            if (c != null && c.supports(agent.getProvider())) {
                return c.chat(agent, history, userText, conversationId);
            }
        }
        throw new Exception("不支持的厂家:" + agent.getProvider());
    }
}

豆包 / DeepSeek 组 messages 时会带上系统提示词(客服人设)和历史,默认 max_tokens=800、超时 60s。扣子不把本地历史全塞进去,而是把 conversation_id 存在 Redis,30 天,让 Bot 自己续上下文。

页面上「测试连接」打的是同一条路由,固定发一句「你好」,成功会清 last_error,失败写回卡片上的红色错误,运营不用翻日志也能看出 Key 填错了还是接入点 ID 填成了模型名。


5. 人机协同:暂停键在 Redis,不在会话表

java 复制代码
private static final String PAUSE    = "wxwork:ai:pause:";
private static final String INTERVAL = "wxwork:ai:interval:";
private static final String DEDUP    = "wxwork:ai:dedup:";
private static final String COZE     = "wxwork:ai:coze:conv:";

// pause:wxwork:ai:pause:{uuid}:{peerId}_{isRoom}   无 TTL
// interval:setIfAbsent 2 秒
// dedup:wxwork:ai:dedup:{uuid}:{serverId}        TTL 2 天

三条接口给工作台:

接口 作用
GET /system/aiAgent/sessionStatus 标题展示「AI 接管中」还是「AI 已暂停」
POST /system/aiAgent/sessionPause 点「暂停 AI」
POST /system/aiAgent/sessionResume 点「恢复 AI」

更关键的是:客服在工作台点发送 ,保存消息时会无条件 pauseSession。客服不用记得去点暂停,只要自己回了,这个会话的 AI 就停。

工作时间是整条自动化的总闸,不只关 AI。没配规则 = 全天有效;配了 09:00--18:00 且启用,晚上关键词、口令、AI、自动拉人全部失效。欢迎语仍然独立。


6. 现网边界

能做的:

  • 多账号共用智能体目录,每个号自己决定开不开、绑哪一个。
  • 好友 / 群聊范围可多选;默认只好友。
  • 口令必须私聊且完全一致;关键词默认「包含」,可改完全匹配。
  • 人工发送自动暂停;可手动恢复。
  • 厂家调用失败写 last_error,卡片上直接红字。

明确没做、避免售前被问穿:

  • 不回图片 / 语音 / 文件,只回文本。
  • 没有 GPT、通义、本地 Ollama。要接第四家,得加 Client,不是改个 Base URL。
  • 没有按标签分流智能体 。一个号同一时间只绑一个 agentId
  • 没有知识库检索(RAG)。人设靠系统提示词,扣子侧知识库在扣子控制台配,不在我们后台。
  • 自动拉人当前是「全部客户首次私聊」,不是按标签拉群。
  • 暂停 key 无 TTL:客服忘了点「恢复 AI」,这个会话会一直停,这是有意的,防止误唤醒。

7. 落地清单

  1. sys_ai_agent,菜单放 AI客服设置/aiCustomer)。
  2. 账号 automation_json 增加 ai.enabled / ai.agentId / ai.scope
  3. 回调里 saveMessage 之后 scheduleAfterIncomingMessage,线程池不要用 ForkJoinPool.commonPool()
  4. Redis 准备 pause / interval / dedup;人工 saveMessagepauseSession
  5. 工作时间规则按上海时区;欢迎语不要挂在同一把锁上。
  6. 工作台读 automation.ai 决定要不要渲染「AI 接管中」和「暂停 AI」。

8. 总结

大模型本身越来越便宜,难的是把它嵌进企微私域的运营节奏里:规则先挡、AI 兜底、人一出手就让位、下班就全体静音。极客聚合这条链路已经按这个节奏在跑。

如果你也在管一批企微号,被「晚上没人回」「客服和机器人抢话」「口令进群和大模型互相打架」折磨过,欢迎在评论区留言场景(几号、要不要群聊 AI、用豆包还是扣子)。需要看演示或交流私域自动化的,直接私信,我们按现网功能给你对一下是否匹配,不会拿还没做的能力画饼。

系列相关:多账号扫码托管、工作台 WebSocket 未读、工作时间总闸,可以和本文对照着看------AI 客服只是自动化链上的第三段,前面还有口令和关键词,后面还有自动拉人。

相关推荐
广州灵眸科技有限公司40 分钟前
瑞芯微(EASY EAI)RV1126B display
开发语言·数据库·人工智能·科技·嵌入式硬件
今天AI了吗44 分钟前
Codex使用技巧:深度解析 Plan Mode 与 Goal Mode
java·网络·人工智能·架构·java-ee
Huangxy__1 小时前
java+ai 全栈项目
java·开发语言
FX1317887KP_1 小时前
AI陪练正在重塑KET PET FCE口语备考,传统外教课还有必要吗?
人工智能·小程序·ket口语·pet口语·fce备考·剑桥英语
天远Date Lab1 小时前
零信任架构实战:基于天远全能个人大数据报告构建自动化核心KYC网关
大数据·人工智能·架构·自动化
智购科技智能售货柜1 小时前
2026自动售货机商品掉落声学计数方案:从麦克风到频谱识别的工程实践~YH
人工智能·算法
QXWZ_IA1 小时前
输电线路沉降监测用什么技术?三种高精度路线对比与精度解析
人工智能·科技
阿维的博客日记1 小时前
Example API指南2
java·mybatis
盛世宏博智慧档案1 小时前
基于PLC与物联网网关的档案库房温湿度超标自动联动控制实现思路(含拓扑图与协议对接)
大数据·人工智能·物联网