从0到1搭建一个企业微信消息聚合平台
我们团队用 Java 给企业微信做了个 AI 客服机器人:从 Hook 截获消息到 DeepSeek 自动回复,全链路架构公开
一个小团队,三套代码,约 17 万行,把「企业微信机器人」从一句口号做成了一个能 7×24 小时接客、答不上来会摇人、全程不骚扰客户的 AI 客服系统。这篇文章不讲概念,只讲我们真实上线的架构、协议和踩过的坑。
关键词:企业微信、AI 客服、DeepSeek、Netty、Xposed、SCRM、大模型应用
0. 先说为什么要自己造
我们团队做 To B 服务,客户全在企业微信里。运营一段时间后,几个问题反复出现:
- 客服下班后没人回消息。客户晚上 10 点问的问题,第二天早上才回复,早跑去别家了;
- 同一个问题答 100 遍。价格、安装、售后政策,纯重复劳动,新人还总答错;
- 客服离职,客户跟着走。聊天记录和客户关系全在员工个人企微号上,人走茶凉;
- 官方 API 能力有天花板。官方接口对群发、触达频次限制很死,而「接管自己员工的企业微信」这件事,官方根本不开放。
市面上的 SCRM 我们买过、也调研过:走官方 API 路线的,能力受限;走协议路线的,本质是个黑盒——你不知道它什么时候封你的号,数据还全在别人服务器上。
所以我们自己做了一套。定位很清楚:企业自有设备、自有账号、自有数据,机器人只是在员工的企业微信里帮忙干活。整体三端:
- 安卓端:一个注入企业微信 App 的自动化模块(约 4.8 万行 Java),负责消息感知和动作执行;
- 服务端:Spring Boot 3 + Netty,约 1000 个 Java 类,负责设备接入、业务逻辑和 AI 大脑;
- 管理后台:Vue 2,约 11 万行,其中有一个 1.3 万行的「企微中控」页面,人机共用一个工作台。
这篇文章以「客户发来一条消息,AI 自动接待,答不上来转人工」这条最核心的链路为主线,把三端串起来讲。
1. 全局架构:一条消息要经过的 5 个世界
协议是我们自定义的一套 Protobuf 消息信封,167 种消息类型,长这样:
message TransportMessage {
int64 id = 1; // 消息ID(雪花算法)
string accessToken = 2; // 认证后颁发的通道令牌
int32 msgType = 3; // 消息类型, 如 1010认证 / 1026消息 / 1070发消息
Any content = 4; // 具体消息体, 167种之一
int64 refMessageId= 5; // 关联的上行消息ID
}
几个贯穿全文的协议号先混个脸熟:
| 协议号 | 方向 | 含义 |
|---|---|---|
| 1010 / 1011 | ↑↓ | 设备认证请求 / 响应 |
| 1020 / 1021 | ↑ | 企微上线 / 下线通知 |
| 1026 | ↑ | 聊天消息上行(客户发来的消息走这里) |
| 1028 | ↑ | 发消息任务结果回执 |
| 1070 | ↓ | 下发「发消息」任务 |
| 3094 | ↓ | 接待开关(是否订阅客户消息) |
| 1001 | 双向 | 心跳(同一个码,服务端主动发起) |
2. 「机器人」的身份模型:设备是设备,微信号是微信号
在讲消息之前,必须先讲清楚我们踩过的第一个大坑:连接身份和业务身份必须分离。
一台安卓手机是一个物理设备(可能被 root、装了注入框架),手机上登录的企业微信账号是业务身份。用户会换手机,也会在同一台手机上换号登录。如果把这两者绑死,换一次设备数据就全乱了。
我们的模型:
- 设备凭证:一台物理设备一个,管连接、踢线、授权计费。首次认证随机生成
DEV_xxxx的设备码,物理设备标识用 APP 上报的 IMEI / android_id 回填; - 业务身份:企微账号 ID(wechatId),所有客户、群、聊天记录归属于它。换号 = 同一设备凭证下换一个业务属性,而不是换一个连接身份。
认证握手(协议 1010)由 APP 在 TCP 连上后立即发起,请求里三个关键字段:
username = 租户登录用户名 // 用来定位这是哪家企业的设备
credential = 企微账号ID // 白名单正则 ^[A-Za-z0-9_-]{1,64}$ 清洗
clientValue= 设备指纹(IMEI等) // 同样做正则清洗, 防注入
这里有一个真实的事故教训。认证逻辑里我们要查用户、查租户、建设备、排同步任务,最初整个方法包在一个事务里。结果 8 月份某天凌晨服务重启,上万台设备同时重连,认证风暴把数据库连接池打满了——事务在持有 DB 连接期间还夹杂网络 I/O,连接迟迟不归还。
现在的做法:
- 认证入口限流 100 次/秒,超了直接关连接,APP 5 秒后重连——刻意把万级并发重连打散;
- 认证入口方法不加事务,只在必要的写库片段上开短事务;
- 需要下发的同步指令全部放到事务
afterCommit之后执行。
APP 侧的重连也配合做了错峰:固定 10 秒延迟 + 按设备 hash 散列 0~30 秒抖动,避免大家卡点同时连进来。
3. 上行:一条客户消息怎么从企微走进 Java
3.1 Hook:在企业微信自己的进程里「听墙角」
安卓端的注入框架是一套基于 Zygisk / Riru 的 Xposed 兼容实现(懂行的同学知道这是 LSPosed 一脉的技术),注入目标是企业微信主进程 com.tencent.wework,我们适配的版本是 5.0.11.81206。
核心思路:企业微信收到新消息时,内部一定会回调它自己的消息监听器。我们 Hook 的就是会话引擎的消息回调(混淆后的 Conversation$2.onAddMessages),在回调里拿到消息对象,反射解析出发送人、内容、类型、引用关系,再交给 16 个消息类型处理器(文本/图片/语音/视频/链接/名片/小程序……)统一转成我们自己的 Bean。
为什么不直接截屏、点屏幕那种「无障碍服务」方案?因为那种方案本质是模拟盲人操作手机,慢、脆、拿不到消息内容、还被官方点名为外挂。Hook 是在进程内部直接调用对方的内部 API,消息是结构化的,这是两个时代的技术。
3.2 三进程架构:别把人家的 App 搞崩
Hook 代码跑在企业微信进程里,但网络连接、重连、队列这些重活绝不能放那儿——你 Hook 进去的每一行代码崩了,崩的都是企业微信,客户直接看到闪退。
所以我们做了三进程隔离:
- 企微进程只做「感知」和「执行」,通过 AIDL 把消息绑给主控进程,队列分了优先级——任务回执比消息重要,消息比批量同步重要;
- 主控进程持有到服务端的 TCP 长连,断连时本地缓存,指数退避重连;
- 看门狗进程干两件事:双进程前台服务保活,以及检测企微是不是「假死」了。
假死检测是个很实战的设计:Hook 层每 5 秒会回一个 pong 作为企微进程真实健康信号。连续 6 个周期(30 秒)收不到 pong,即使 TCP 心跳还正常,也判定企微卡死,root 权限强杀重启——因为心跳只能证明我们自己的进程活着,证明不了企微没 ANR。
3.3 Netty 接入层:万台设备的连接怎么扛
服务端 Netty 用了 boss / worker / business 三组 EventLoopGroup,业务处理全部丢到独立业务线程池,慢业务绝不允许污染 IO 线程。Pipeline 是标准的 Protobuf 帧编解码:
IdleStateHandler(120s读空闲)
→ ProtobufVarint32FrameDecoder
→ ProtobufDecoder(TransportMessage)
→ LengthFieldPrepender + ProtobufEncoder
→ NettyServerHandler(业务线程组)
上行消息进来后不直接处理,而是进 MessageDispatcher:
- 一个 20000 容量的阻塞队列把 Netty IO 线程和业务线程池隔开;
- 单独的消费线程每秒最多向业务池提交 500 条,把设备端突发上报削成数据库扛得住的匀速;
- 一个协议号可以挂多个处理器(注册表多播),比如聊天消息既要落库、又要触发自动回复、还要推 WebSocket;
- 队列满了直接丢并记降级日志——对客服场景来说,过载时丢一条「正在批量同步的群列表」远好过整个集群被拖死。
另外两个保命细节:
- 写缓冲水位线背压:高低水位设 32KB / 256KB,通道积压到高水位就
setAutoRead(false)停止读,防止慢消费设备把服务端内存堆爆; - 心跳由服务端主动发起:每 30 秒下发心跳包,120 秒没响应掐线重连;设备数超过 500 台后分 9 片轮扫,控制单次扫描成本。
4. AI 大脑:这套系统最难的部分
消息落库之后,进入这篇文章的主角:AiAutoReplyService。
先说结论:把大模型接进客服场景,最难的根本不是调 API,而是「什么时候答、答什么、什么时候闭嘴、怎么优雅地把人摇来」。
4.1 消息聚合:客户说话是「一段一段」的
真实的微信对话长这样:
客户: 在吗
客户: 问一下
客户: 你们那个充电桩功率多大的
如果来一条调一次大模型,会发生三件事:① 回复三条,像个傻子;② token 费用 ×3;③ 前两条上下文不完整,模型只能反问。
所以我们做了静默窗口聚合:按「会话 + 发送人」缓冲消息,等客户停下来再整体结算成一个问题。
- 群聊窗口 10 秒、单聊 5 秒(按租户可配);
- 自适应提速:消息以
??!!。结尾,说明问题完整了,窗口缩到 3 秒,大多数客户不用陪跑满全程; - 30 秒封顶:总有话痨能一直输入,连续输入最长只等 30 秒强制结算,防止窗口无限续期。
缓冲不放在内存,而是写 Redis List(TTL 30 秒)。为什么?结算调度器万一重启,内存里的消息就丢了;Redis 里的还在,新消息到达会重新挂调度——最多晚回复,绝不丢消息。
结算时也不是一把删掉整个 List,而是 leftPop 逐条取;pop 的过程中如果又有新消息压进来,1 秒后挂一个「救援结算」兜底。这些都是被竞态教训出来的。
4.2 让大模型输出「程序能用的结果」:四选一 JSON 协议
普通玩家让大模型吐一段话,我们要求它必须吐严格 JSON,因为回复之后的程序动作需要机器可判定:
private static final String BASE_PROMPT =
"你是微信社群的客服助手,现在代表运营者回复客户消息。回答要求:\n" +
"1. 必须用中文,语气自然亲切,像真人客服一样口语化\n" +
"2. 回复简洁,通常1~3句话,适合微信聊天场景,不使用任何markdown格式\n" +
"3. 优先且仅依据下面提供的业务知识库回答;知识库没有的内容绝不编造价格、政策、承诺\n" +
"4. 不透露你是AI或模型相关信息\n" +
"5. 每次回复必须输出严格的JSON对象,格式四选一:\n" +
" {\"type\":\"normal\",\"reply\":\"给客户的回复内容\",\"confidence\":0.9,\"sourceIds\":[1,3]}\n" +
" {\"type\":\"ignore\"}\n" +
" {\"type\":\"handoff\",\"reply\":\"给客户的过渡话术\",\"summary\":\"客户问题一句话摘要\"}\n" +
" {\"type\":\"urgent\",\"reply\":\"给客户的应急话术\",\"summary\":\"紧急情况摘要\"}\n" +
// ……四种 type 的语义说明
"6. 结合「业务场景」理解客户问题的行业语境……";
四种类型对应四条程序分支:
| type | 含义 | 程序动作 |
|---|---|---|
normal | 能依据知识库回答 | 检查置信度后下发回复 |
ignore | 闲聊、寒暄、表情、与业务无关 | 什么都不发,不花一次 token 陪聊,也不拿通用知识瞎编 |
handoff | 需要人工 | 先发过渡安抚话术,再通知客服,AI 进入静默 |
urgent | 紧急情况 | 先发客户当下可执行的应急指引,再最高优先级通知客服 |
注意 normal 里的两个字段:confidence 是模型自评的正确率把握,低于 0.6 一律转人工,不硬答;sourceIds 记录引用了知识库哪几条,运营可以审计「这条回复是有依据的还是模型自己发挥的」。
企业客服场景,幻觉是不可接受的。 客户问价格,模型编一个数字出去,损失的是真金白银。所以我们的系统提示词里写死了「知识库没有的内容绝不编造价格、政策、承诺」,再加一层置信度兜底。
这里有一个非常关键的设计决策:系统提示词里只放与行业无关的协议约束,不放任何业务判定标准。 什么问题算紧急、什么语气该转人工,全部由租户自己配置的「业务场景」和知识库决定。同一个系统,卖充电桩的客户和卖化妆品的客户,AI 的判定标准天然隔离——每个租户用自己的 API Key、自己的 baseUrl、自己的模型(DeepSeek / 通义 / Kimi / GPT 都行,OpenAI 兼容协议),平台不提供共享 Key,费用和数据都归租户。
4.3 知识库:全量文档 + 相似度召回 FAQ
租户在后台维护两类知识:文档类(产品说明、售后政策)全量塞进 system prompt;问答类(FAQ)按当前聚合问题做相似度召回,只取相关的。历史对话默认带最近 10 条,单条截断 500 字,防止长文撑爆上下文。
4.4 转人工:先安抚客户,再摇客服
这是我们和 demo 级 AI 客服最大的区别。转人工不是弹个日志完事,而是一个完整闭环:
几个细节值得单独说:
第一,必须先给客户回话,再通知客服。 绝不能客户屏幕上晾半天没人理。过渡话术由模型现场生成(handoff 是安抚,urgent 是可执行的应急指引),生成失败有模板兜底——转人工可能恰好是因为 AI 服务挂了,兜底逻辑不能反过来依赖 AI 活着。
第二,静默是「用户级」的,不搞连坐。 静默期的 Redis key 是 ai:silent:{机器人}:{会话}:{触发用户wxid}。群里 A 触发了转人工,只有 A 的消息 AI 不抢答,B 正常提问照常回答。这个 bug 我们真踩过——早期 senderId 没随聚合消息持久化,key 串到群维度,结果一个人转人工,全群沉默。
第三,静默期客户追问,不能装死。 客服还没认领时,客户追问要回一句催促话术,并且 @值班客服:
private static final String URGE_REPLY =
"人工同事正在加急赶来,请再稍等一下,马上就有人跟进您的问题~";
// 同一个客户 5 分钟内最多发一条催促, 用 Redis 原子计数节流, 防客户刷屏时轰炸群聊
第四,客服怎么算「认领」? 不需要客服点什么按钮——值班客服在客户群(或通知群)里开口说话,或者在中控后台手动发了消息,就判定已认领,AI 继续静默;客服在中控发一句「已处理 / 恢复AI」,静默立刻结束。这套零成本认领靠聊天消息方向 + 发言人身份就能判定。
第五,重入抑制。 AI 不能当复读机:人工处理中再次触发转人工,只续静默不打扰;通知 webhook 10 分钟内不重推,超时重推最多 3 次封顶。
4.5 防护栏:一个生产级 AI 模块的自我修养
AiAutoReplyService 里密密麻麻的注释,记录了这个模块是怎么被真实客户调教出来的:
- 规则优先:关键词自动回复规则的优先级高于 AI,命中规则就不走 AI,防止客户收到双重回复;
- 窗口内防双回复:结算前查一次库,这期间如果关键词规则、SOP 或者坐席已经回过了,AI 这批直接丢弃;
- 下发前二次复检:AI 生成要 5~15 秒,这期间人工可能已经介入。回复真正发出前再查一次静默状态,落后的回答绝不抢发;
- 每会话限频:默认每分钟最多 AI 回复 5 次(按聚合批次计),防异常循环;
- 致谢清零:客户说「谢谢 / 好的 / 明白了」这类收尾词,不回复、不计数、轮次清零——否则客户礼貌一下,反而因为「聊满 3 句」被转了人工;
- 重复追问判定:轮次超限时,用字符二元组 Jaccard 相似度(阈值 0.35)判断客户是不是「还在纠结同一件事」;换新话题就重新计数,不冤枉;
- 同会话串行:同一个会话的 AI 任务用
CompletableFuture串成链,上一条生成完再处理下一条;不同会话并行。群里两个人同时提问,回复严格按顺序来,不会张冠李戴; - 线程池隔离:大模型 HTTP 调用是同步阻塞的(连接超时 10 秒、读超时 60 秒),严禁在 Netty IO 线程上跑,全部丢独立的
aiReplyExecutor; - 失败静默降级:AI 服务挂了只记日志、发告警,绝不影响消息主链路——大不了人工顶上,不能让整个消息收发瘫痪。
4.6 一个排查了很久的坑:max_tokens
上线初期出现过一批「AI 时灵时不灵」的灵异现象,日志只显示生成失败。最后定位到:用的是 DeepSeek 推理模型,思维链(reasoning_content)和正文共享输出预算,默认 max_tokens=4096。复杂问题的思维链实测能跑到 600~1200 tokens,一波动直接把预算吃光,finish_reason=length,正文 content 为空。
显式设成 8192,并对 content 为空的三种成因(预算耗尽 / 服务端偶发空回复 / 报文异常)全部补齐日志字段后,问题才收敛。这种坑不看 finish_reason 根本猜不到。
5. 下行:AI 生成的回复怎么发到客户微信里
AI 生成的内容不是直接 HTTP 调接口发出去的——企业微信没有给我们这个接口。它走的是和所有指令一样的下发链路:
- 服务端构造
TalkToFriendTask(协议 1070)消息体,雪花算法生成消息 ID; - 只做通道在线探测,不立刻发。真正的
writeAndFlush注册到事务afterCommit——防止「事务回滚了但指令已经发出去」的幽灵消息; - TCP 发到手机,主控进程收到后通过广播转回企微进程,入任务队列,主线程调用企微内部发消息 API(群聊时带上原生 @ 和引用渲染);
- 企微真正发送成功后,Hook 到消息状态变更,回执
TalkToFriendTaskResultNotice(1028)上报; - 服务端拿到回执更新消息状态,前端 WebSocket 推送,气泡从「发送中」变成已送达。
协议层没有 TCP ACK,所以一切以 APP 主动上报的业务回执为准。中控页面上发消息采用乐观 UI:先显示「发送中」,失败了气泡变红可重发——交互完全对标你熟悉的微信。
AI 发出的消息会打 msg_source=AI_REPLY 的角标,在中控会话里和人工消息、规则消息明确区分,运营随时能审计 AI 说了什么。
6. 效果与一些数字
上线后我们自己客户侧的体感:
- 夜间和节假日的首次响应时间从「小时级」变成秒级,大量咨询在非工作时间被完整闭环;
- 重复性问题(占客服咨询量的大头)由 AI 承接,人工只处理 AI 转来的真问题,客服人效肉眼可见地提升;
- 所有客户消息、聊天记录沉淀在企业自己的后台,员工离职在后台一键交接,客户无感;
- 成本可控:聚合机制把调用次数压到「一个问题一次」,加上每租户自带 Key,费用完全可预期。
7. 必须聊的合规与风控
做这类系统,克制比能力重要。我们的原则写在代码里:
- 只在企业自有设备、自有账号上运行,定位是员工的「效率助手」,不是群控、不是爆粉工具;
- 封号只监听,不对抗。检测到封号信号只上报、只告警,不做任何本地绕过——你能想到的所有「防封技巧」,风控团队也想得到,对抗只会加速死亡;
- 反检测保持最小动作,只处理注入框架自身的痕迹,「原版没有的一律不做」,越折腾特征越多;
- 所有主动行为拟人化:批量动作加随机间隔抖动、有时间窗(如加好友默认每号每天 20 条、09:00–21:00)、有频控;
- AI 回复同样有节制:限频、静默期、防骚扰、可审计。
2025 年市面上群控相关的判例(有判赔 300 万的)已经说明了红线在哪。技术是中性的,拿它服务自己的客户还是拿去骚扰别人,是两条完全不同的路。
8. 写在最后
这套系统做到现在,三端约 17 万行代码,沉淀下来的真正资产不是某个 Hook 点,而是:一套版本适配方法论(新版企微适配基本等于改一个混淆名字典类)、一条设备到 AI 的可靠消息链路、和一个知道什么时候该闭嘴的客服大脑。
后续我会继续拆这个系列:
- 万台安卓设备的 Netty 长连接:限流、背压、心跳与集群路由;
- Hook 企业微信 5.0.11:R8 混淆迁移与版本适配体系;
- SOP 自动化运营引擎:CAS 状态机如何保证多实例下不重复打扰客户;
- 20 个 Java 类写一个商业级 OpenAPI 网关:HMAC 签名、保序投递与死信队列。
我把可以公开的网关层代码、Protobuf 协议定义和架构图整理到了 GitHub(链接放在我主页简介里,自取)。
如果这篇文章对你有帮助,欢迎点赞收藏;想要完整 167 个协议号文档或在做类似系统想交流的同学,评论区扣「企微AI」,我看到都会回,也欢迎私信聊架构和落地问题。
本文仅用于企业自有设备的效率工具技术研究,不提供任何针对微信/企业微信的攻击、破坏手段。请遵守平台规则与法律法规。
我们团队用 Java 给企业微信做了个 AI 客服机器人:从 Hook 截获消息到 DeepSeek 自动回复,全链路架构公开
一个小团队,三套代码,约 17 万行,把「企业微信机器人」从一句口号做成了一个能 7×24 小时接客、答不上来会摇人、全程不骚扰客户的 AI 客服系统。这篇文章不讲概念,只讲我们真实上线的架构、协议和踩过的坑。
关键词:企业微信、AI 客服、DeepSeek、Netty、Xposed、SCRM、大模型应用
0. 先说为什么要自己造
我们团队做 To B 服务,客户全在企业微信里。运营一段时间后,几个问题反复出现:
- 客服下班后没人回消息。客户晚上 10 点问的问题,第二天早上才回复,早跑去别家了;
- 同一个问题答 100 遍。价格、安装、售后政策,纯重复劳动,新人还总答错;
- 客服离职,客户跟着走。聊天记录和客户关系全在员工个人企微号上,人走茶凉;
- 官方 API 能力有天花板。官方接口对群发、触达频次限制很死,而「接管自己员工的企业微信」这件事,官方根本不开放。
市面上的 SCRM 我们买过、也调研过:走官方 API 路线的,能力受限;走协议路线的,本质是个黑盒——你不知道它什么时候封你的号,数据还全在别人服务器上。
所以我们自己做了一套。定位很清楚:企业自有设备、自有账号、自有数据,机器人只是在员工的企业微信里帮忙干活。整体三端:
- 安卓端:一个注入企业微信 App 的自动化模块(约 4.8 万行 Java),负责消息感知和动作执行;
- 服务端:Spring Boot 3 + Netty,约 1000 个 Java 类,负责设备接入、业务逻辑和 AI 大脑;
- 管理后台:Vue 2,约 11 万行,其中有一个 1.3 万行的「企微中控」页面,人机共用一个工作台。
这篇文章以「客户发来一条消息,AI 自动接待,答不上来转人工」这条最核心的链路为主线,把三端串起来讲。
1. 全局架构:一条消息要经过的 5 个世界
协议是我们自定义的一套 Protobuf 消息信封,167 种消息类型,长这样:
message TransportMessage {
int64 id = 1; // 消息ID(雪花算法)
string accessToken = 2; // 认证后颁发的通道令牌
int32 msgType = 3; // 消息类型, 如 1010认证 / 1026消息 / 1070发消息
Any content = 4; // 具体消息体, 167种之一
int64 refMessageId= 5; // 关联的上行消息ID
}
几个贯穿全文的协议号先混个脸熟:
| 协议号 | 方向 | 含义 |
|---|---|---|
| 1010 / 1011 | ↑↓ | 设备认证请求 / 响应 |
| 1020 / 1021 | ↑ | 企微上线 / 下线通知 |
| 1026 | ↑ | 聊天消息上行(客户发来的消息走这里) |
| 1028 | ↑ | 发消息任务结果回执 |
| 1070 | ↓ | 下发「发消息」任务 |
| 3094 | ↓ | 接待开关(是否订阅客户消息) |
| 1001 | 双向 | 心跳(同一个码,服务端主动发起) |
2. 「机器人」的身份模型:设备是设备,微信号是微信号
在讲消息之前,必须先讲清楚我们踩过的第一个大坑:连接身份和业务身份必须分离。
一台安卓手机是一个物理设备(可能被 root、装了注入框架),手机上登录的企业微信账号是业务身份。用户会换手机,也会在同一台手机上换号登录。如果把这两者绑死,换一次设备数据就全乱了。
我们的模型:
- 设备凭证:一台物理设备一个,管连接、踢线、授权计费。首次认证随机生成
DEV_xxxx的设备码,物理设备标识用 APP 上报的 IMEI / android_id 回填; - 业务身份:企微账号 ID(wechatId),所有客户、群、聊天记录归属于它。换号 = 同一设备凭证下换一个业务属性,而不是换一个连接身份。
认证握手(协议 1010)由 APP 在 TCP 连上后立即发起,请求里三个关键字段:
username = 租户登录用户名 // 用来定位这是哪家企业的设备
credential = 企微账号ID // 白名单正则 ^[A-Za-z0-9_-]{1,64}$ 清洗
clientValue= 设备指纹(IMEI等) // 同样做正则清洗, 防注入
这里有一个真实的事故教训。认证逻辑里我们要查用户、查租户、建设备、排同步任务,最初整个方法包在一个事务里。结果 8 月份某天凌晨服务重启,上万台设备同时重连,认证风暴把数据库连接池打满了——事务在持有 DB 连接期间还夹杂网络 I/O,连接迟迟不归还。
现在的做法:
- 认证入口限流 100 次/秒,超了直接关连接,APP 5 秒后重连——刻意把万级并发重连打散;
- 认证入口方法不加事务,只在必要的写库片段上开短事务;
- 需要下发的同步指令全部放到事务
afterCommit之后执行。
APP 侧的重连也配合做了错峰:固定 10 秒延迟 + 按设备 hash 散列 0~30 秒抖动,避免大家卡点同时连进来。
3. 上行:一条客户消息怎么从企微走进 Java
3.1 Hook:在企业微信自己的进程里「听墙角」
安卓端的注入框架是一套基于 Zygisk / Riru 的 Xposed 兼容实现(懂行的同学知道这是 LSPosed 一脉的技术),注入目标是企业微信主进程 com.tencent.wework,我们适配的版本是 5.0.11.81206。
核心思路:企业微信收到新消息时,内部一定会回调它自己的消息监听器。我们 Hook 的就是会话引擎的消息回调(混淆后的 Conversation$2.onAddMessages),在回调里拿到消息对象,反射解析出发送人、内容、类型、引用关系,再交给 16 个消息类型处理器(文本/图片/语音/视频/链接/名片/小程序……)统一转成我们自己的 Bean。
为什么不直接截屏、点屏幕那种「无障碍服务」方案?因为那种方案本质是模拟盲人操作手机,慢、脆、拿不到消息内容、还被官方点名为外挂。Hook 是在进程内部直接调用对方的内部 API,消息是结构化的,这是两个时代的技术。
3.2 三进程架构:别把人家的 App 搞崩
Hook 代码跑在企业微信进程里,但网络连接、重连、队列这些重活绝不能放那儿——你 Hook 进去的每一行代码崩了,崩的都是企业微信,客户直接看到闪退。
所以我们做了三进程隔离:
- 企微进程只做「感知」和「执行」,通过 AIDL 把消息绑给主控进程,队列分了优先级——任务回执比消息重要,消息比批量同步重要;
- 主控进程持有到服务端的 TCP 长连,断连时本地缓存,指数退避重连;
- 看门狗进程干两件事:双进程前台服务保活,以及检测企微是不是「假死」了。
假死检测是个很实战的设计:Hook 层每 5 秒会回一个 pong 作为企微进程真实健康信号。连续 6 个周期(30 秒)收不到 pong,即使 TCP 心跳还正常,也判定企微卡死,root 权限强杀重启——因为心跳只能证明我们自己的进程活着,证明不了企微没 ANR。
3.3 Netty 接入层:万台设备的连接怎么扛
服务端 Netty 用了 boss / worker / business 三组 EventLoopGroup,业务处理全部丢到独立业务线程池,慢业务绝不允许污染 IO 线程。Pipeline 是标准的 Protobuf 帧编解码:
IdleStateHandler(120s读空闲)
→ ProtobufVarint32FrameDecoder
→ ProtobufDecoder(TransportMessage)
→ LengthFieldPrepender + ProtobufEncoder
→ NettyServerHandler(业务线程组)
上行消息进来后不直接处理,而是进 MessageDispatcher:
- 一个 20000 容量的阻塞队列把 Netty IO 线程和业务线程池隔开;
- 单独的消费线程每秒最多向业务池提交 500 条,把设备端突发上报削成数据库扛得住的匀速;
- 一个协议号可以挂多个处理器(注册表多播),比如聊天消息既要落库、又要触发自动回复、还要推 WebSocket;
- 队列满了直接丢并记降级日志——对客服场景来说,过载时丢一条「正在批量同步的群列表」远好过整个集群被拖死。
另外两个保命细节:
- 写缓冲水位线背压:高低水位设 32KB / 256KB,通道积压到高水位就
setAutoRead(false)停止读,防止慢消费设备把服务端内存堆爆; - 心跳由服务端主动发起:每 30 秒下发心跳包,120 秒没响应掐线重连;设备数超过 500 台后分 9 片轮扫,控制单次扫描成本。
4. AI 大脑:这套系统最难的部分
消息落库之后,进入这篇文章的主角:AiAutoReplyService。
先说结论:把大模型接进客服场景,最难的根本不是调 API,而是「什么时候答、答什么、什么时候闭嘴、怎么优雅地把人摇来」。
4.1 消息聚合:客户说话是「一段一段」的
真实的微信对话长这样:
客户: 在吗
客户: 问一下
客户: 你们那个充电桩功率多大的
如果来一条调一次大模型,会发生三件事:① 回复三条,像个傻子;② token 费用 ×3;③ 前两条上下文不完整,模型只能反问。
所以我们做了静默窗口聚合:按「会话 + 发送人」缓冲消息,等客户停下来再整体结算成一个问题。
- 群聊窗口 10 秒、单聊 5 秒(按租户可配);
- 自适应提速:消息以
??!!。结尾,说明问题完整了,窗口缩到 3 秒,大多数客户不用陪跑满全程; - 30 秒封顶:总有话痨能一直输入,连续输入最长只等 30 秒强制结算,防止窗口无限续期。
缓冲不放在内存,而是写 Redis List(TTL 30 秒)。为什么?结算调度器万一重启,内存里的消息就丢了;Redis 里的还在,新消息到达会重新挂调度——最多晚回复,绝不丢消息。
结算时也不是一把删掉整个 List,而是 leftPop 逐条取;pop 的过程中如果又有新消息压进来,1 秒后挂一个「救援结算」兜底。这些都是被竞态教训出来的。
4.2 让大模型输出「程序能用的结果」:四选一 JSON 协议
普通玩家让大模型吐一段话,我们要求它必须吐严格 JSON,因为回复之后的程序动作需要机器可判定:
private static final String BASE_PROMPT =
"你是微信社群的客服助手,现在代表运营者回复客户消息。回答要求:\n" +
"1. 必须用中文,语气自然亲切,像真人客服一样口语化\n" +
"2. 回复简洁,通常1~3句话,适合微信聊天场景,不使用任何markdown格式\n" +
"3. 优先且仅依据下面提供的业务知识库回答;知识库没有的内容绝不编造价格、政策、承诺\n" +
"4. 不透露你是AI或模型相关信息\n" +
"5. 每次回复必须输出严格的JSON对象,格式四选一:\n" +
" {\"type\":\"normal\",\"reply\":\"给客户的回复内容\",\"confidence\":0.9,\"sourceIds\":[1,3]}\n" +
" {\"type\":\"ignore\"}\n" +
" {\"type\":\"handoff\",\"reply\":\"给客户的过渡话术\",\"summary\":\"客户问题一句话摘要\"}\n" +
" {\"type\":\"urgent\",\"reply\":\"给客户的应急话术\",\"summary\":\"紧急情况摘要\"}\n" +
// ……四种 type 的语义说明
"6. 结合「业务场景」理解客户问题的行业语境……";
四种类型对应四条程序分支:
| type | 含义 | 程序动作 |
|---|---|---|
normal | 能依据知识库回答 | 检查置信度后下发回复 |
ignore | 闲聊、寒暄、表情、与业务无关 | 什么都不发,不花一次 token 陪聊,也不拿通用知识瞎编 |
handoff | 需要人工 | 先发过渡安抚话术,再通知客服,AI 进入静默 |
urgent | 紧急情况 | 先发客户当下可执行的应急指引,再最高优先级通知客服 |
注意 normal 里的两个字段:confidence 是模型自评的正确率把握,低于 0.6 一律转人工,不硬答;sourceIds 记录引用了知识库哪几条,运营可以审计「这条回复是有依据的还是模型自己发挥的」。
企业客服场景,幻觉是不可接受的。 客户问价格,模型编一个数字出去,损失的是真金白银。所以我们的系统提示词里写死了「知识库没有的内容绝不编造价格、政策、承诺」,再加一层置信度兜底。
这里有一个非常关键的设计决策:系统提示词里只放与行业无关的协议约束,不放任何业务判定标准。 什么问题算紧急、什么语气该转人工,全部由租户自己配置的「业务场景」和知识库决定。同一个系统,卖充电桩的客户和卖化妆品的客户,AI 的判定标准天然隔离——每个租户用自己的 API Key、自己的 baseUrl、自己的模型(DeepSeek / 通义 / Kimi / GPT 都行,OpenAI 兼容协议),平台不提供共享 Key,费用和数据都归租户。
4.3 知识库:全量文档 + 相似度召回 FAQ
租户在后台维护两类知识:文档类(产品说明、售后政策)全量塞进 system prompt;问答类(FAQ)按当前聚合问题做相似度召回,只取相关的。历史对话默认带最近 10 条,单条截断 500 字,防止长文撑爆上下文。
4.4 转人工:先安抚客户,再摇客服
这是我们和 demo 级 AI 客服最大的区别。转人工不是弹个日志完事,而是一个完整闭环:
几个细节值得单独说:
第一,必须先给客户回话,再通知客服。 绝不能客户屏幕上晾半天没人理。过渡话术由模型现场生成(handoff 是安抚,urgent 是可执行的应急指引),生成失败有模板兜底——转人工可能恰好是因为 AI 服务挂了,兜底逻辑不能反过来依赖 AI 活着。
第二,静默是「用户级」的,不搞连坐。 静默期的 Redis key 是 ai:silent:{机器人}:{会话}:{触发用户wxid}。群里 A 触发了转人工,只有 A 的消息 AI 不抢答,B 正常提问照常回答。这个 bug 我们真踩过——早期 senderId 没随聚合消息持久化,key 串到群维度,结果一个人转人工,全群沉默。
第三,静默期客户追问,不能装死。 客服还没认领时,客户追问要回一句催促话术,并且 @值班客服:
private static final String URGE_REPLY =
"人工同事正在加急赶来,请再稍等一下,马上就有人跟进您的问题~";
// 同一个客户 5 分钟内最多发一条催促, 用 Redis 原子计数节流, 防客户刷屏时轰炸群聊
第四,客服怎么算「认领」? 不需要客服点什么按钮——值班客服在客户群(或通知群)里开口说话,或者在中控后台手动发了消息,就判定已认领,AI 继续静默;客服在中控发一句「已处理 / 恢复AI」,静默立刻结束。这套零成本认领靠聊天消息方向 + 发言人身份就能判定。
第五,重入抑制。 AI 不能当复读机:人工处理中再次触发转人工,只续静默不打扰;通知 webhook 10 分钟内不重推,超时重推最多 3 次封顶。
4.5 防护栏:一个生产级 AI 模块的自我修养
AiAutoReplyService 里密密麻麻的注释,记录了这个模块是怎么被真实客户调教出来的:
- 规则优先:关键词自动回复规则的优先级高于 AI,命中规则就不走 AI,防止客户收到双重回复;
- 窗口内防双回复:结算前查一次库,这期间如果关键词规则、SOP 或者坐席已经回过了,AI 这批直接丢弃;
- 下发前二次复检:AI 生成要 5~15 秒,这期间人工可能已经介入。回复真正发出前再查一次静默状态,落后的回答绝不抢发;
- 每会话限频:默认每分钟最多 AI 回复 5 次(按聚合批次计),防异常循环;
- 致谢清零:客户说「谢谢 / 好的 / 明白了」这类收尾词,不回复、不计数、轮次清零——否则客户礼貌一下,反而因为「聊满 3 句」被转了人工;
- 重复追问判定:轮次超限时,用字符二元组 Jaccard 相似度(阈值 0.35)判断客户是不是「还在纠结同一件事」;换新话题就重新计数,不冤枉;
- 同会话串行:同一个会话的 AI 任务用
CompletableFuture串成链,上一条生成完再处理下一条;不同会话并行。群里两个人同时提问,回复严格按顺序来,不会张冠李戴; - 线程池隔离:大模型 HTTP 调用是同步阻塞的(连接超时 10 秒、读超时 60 秒),严禁在 Netty IO 线程上跑,全部丢独立的
aiReplyExecutor; - 失败静默降级:AI 服务挂了只记日志、发告警,绝不影响消息主链路——大不了人工顶上,不能让整个消息收发瘫痪。
4.6 一个排查了很久的坑:max_tokens
上线初期出现过一批「AI 时灵时不灵」的灵异现象,日志只显示生成失败。最后定位到:用的是 DeepSeek 推理模型,思维链(reasoning_content)和正文共享输出预算,默认 max_tokens=4096。复杂问题的思维链实测能跑到 600~1200 tokens,一波动直接把预算吃光,finish_reason=length,正文 content 为空。
显式设成 8192,并对 content 为空的三种成因(预算耗尽 / 服务端偶发空回复 / 报文异常)全部补齐日志字段后,问题才收敛。这种坑不看 finish_reason 根本猜不到。
5. 下行:AI 生成的回复怎么发到客户微信里
AI 生成的内容不是直接 HTTP 调接口发出去的——企业微信没有给我们这个接口。它走的是和所有指令一样的下发链路:
- 服务端构造
TalkToFriendTask(协议 1070)消息体,雪花算法生成消息 ID; - 只做通道在线探测,不立刻发。真正的
writeAndFlush注册到事务afterCommit——防止「事务回滚了但指令已经发出去」的幽灵消息; - TCP 发到手机,主控进程收到后通过广播转回企微进程,入任务队列,主线程调用企微内部发消息 API(群聊时带上原生 @ 和引用渲染);
- 企微真正发送成功后,Hook 到消息状态变更,回执
TalkToFriendTaskResultNotice(1028)上报; - 服务端拿到回执更新消息状态,前端 WebSocket 推送,气泡从「发送中」变成已送达。
协议层没有 TCP ACK,所以一切以 APP 主动上报的业务回执为准。中控页面上发消息采用乐观 UI:先显示「发送中」,失败了气泡变红可重发——交互完全对标你熟悉的微信。
AI 发出的消息会打 msg_source=AI_REPLY 的角标,在中控会话里和人工消息、规则消息明确区分,运营随时能审计 AI 说了什么。
6. 效果与一些数字
上线后我们自己客户侧的体感:
- 夜间和节假日的首次响应时间从「小时级」变成秒级,大量咨询在非工作时间被完整闭环;
- 重复性问题(占客服咨询量的大头)由 AI 承接,人工只处理 AI 转来的真问题,客服人效肉眼可见地提升;
- 所有客户消息、聊天记录沉淀在企业自己的后台,员工离职在后台一键交接,客户无感;
- 成本可控:聚合机制把调用次数压到「一个问题一次」,加上每租户自带 Key,费用完全可预期。
7. 必须聊的合规与风控
做这类系统,克制比能力重要。我们的原则写在代码里:
- 只在企业自有设备、自有账号上运行,定位是员工的「效率助手」,不是群控、不是爆粉工具;
- 封号只监听,不对抗。检测到封号信号只上报、只告警,不做任何本地绕过——你能想到的所有「防封技巧」,风控团队也想得到,对抗只会加速死亡;
- 反检测保持最小动作,只处理注入框架自身的痕迹,「原版没有的一律不做」,越折腾特征越多;
- 所有主动行为拟人化:批量动作加随机间隔抖动、有时间窗(如加好友默认每号每天 20 条、09:00–21:00)、有频控;
- AI 回复同样有节制:限频、静默期、防骚扰、可审计。
2025 年市面上群控相关的判例(有判赔 300 万的)已经说明了红线在哪。技术是中性的,拿它服务自己的客户还是拿去骚扰别人,是两条完全不同的路。
8. 写在最后
这套系统做到现在,三端约 17 万行代码,沉淀下来的真正资产不是某个 Hook 点,而是:一套版本适配方法论(新版企微适配基本等于改一个混淆名字典类)、一条设备到 AI 的可靠消息链路、和一个知道什么时候该闭嘴的客服大脑。
后续我会继续拆这个系列:
- 万台安卓设备的 Netty 长连接:限流、背压、心跳与集群路由;
- Hook 企业微信 5.0.11:R8 混淆迁移与版本适配体系;
- SOP 自动化运营引擎:CAS 状态机如何保证多实例下不重复打扰客户;
- 20 个 Java 类写一个商业级 OpenAPI 网关:HMAC 签名、保序投递与死信队列。
我把可以公开的网关层代码、Protobuf 协议定义和架构图整理到了 GitHub(链接放在我主页简介里,自取)。
如果这篇文章对你有帮助,欢迎点赞收藏;想要完整 167 个协议号文档或在做类似系统想交流的同学,评论区扣「企微AI」,我看到都会回,也欢迎私信聊架构和落地问题。
本文仅用于企业自有设备的效率工具技术研究,不提供任何针对微信/企业微信的攻击、破坏手段。请遵守平台规则与法律法规。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)