背景痛点:消息风暴与状态丢失的双重夹击
去年给一家电商客户做企微客服机器人,上线首日就踩了两个大坑:
- 早上 10 点促销推送,瞬间 3 k+ 用户同时@机器人,Web 服务直接 502。
- 用户问完“优惠券怎么用”后,紧接着追问“能叠加满减吗”,机器人却忘了上一轮给的券码,对话从头开始,被投诉“人工智障”。
这两个问题本质上是“高并发”与“多轮状态”叠加后的典型症状:
- 企微的“接收消息”API 是HTTP 长轮询,一次最多拉 20 条,默认 30 s 间隔;如果处理慢,下一轮请求还没回来,消息就堆积成山。
- 多轮对话需要把“槽位”(slot) 存在内存或缓存里,但进程重启或水平扩容后,状态灰飞烟灭。
传统 if/else 规则引擎在 200 QPS 以内还能扛,超过 500 QPS 时 CPU 飙满,意图分支一多,维护就是灾难。于是我们把目光转向 AI 辅助开发,让模型扛语义,让规则扛边界,两者互补。
技术选型:规则、ML、DL 的三角权衡
先给出实测数据(4 核 8 G Docker 内网压测,单模型单实例):
| 方案 | 平均 QPS | 准确率 | 冷启动时间 | 备注 |
|---|---|---|---|---|
| 规则引擎 | 800 | 0.78 | 0 s | 分支>300 后不可维护 |
| 传统 ML(TF-IDF+LR) | 1200 | 0.85 | 3 min | 需要人工标注 5 k 条 |
| 轻量 BERT(MiniLM+蒸馏) | 1800 | 0.91 | 15 s | 模型 30 MB,GPU 可选 |
结论:
- 对并发高、意图多、需求迭代快的场景,BERT 蒸馏模型在准确率与吞吐量上都更优;
- 冷启动成本用“AI 辅助标注”来抹平:先用规则跑一周,收集日志 → 主动学习 → 人工纠偏 10% 样本,就能训出 0.9+ 模型。
核心实现:三条代码搞定“收、识、答”
1. 异步消息队列——把长轮询变成生产消费
企微官方只保证“最多重试 3 次”,如果 5 s 内没回 200,同一条消息会再次推送,极易重复。我们采用“拉模式”:
- 定时器每 2 s 拉一次
https://qyapi.weixin.qq.com/cgi-bin/message/get_msg_list; - 把原始 JSON 直接塞进 Redis Stream(group=robot),返回 200,解耦业务;
- 消费者协程异步处理,即使重启也不丢事件。
核心代码(Python 3.11,PEP8):
import httpx, redis, asyncio, json async def pull_job(): r = redis.Redis(host='127.0.0.1', decode_responses=True) while True: async with httpx.AsyncClient() as cli: res = await cli.post( 'https://qyapi.weixin.qq.com/cgi-bin/message/get_msg_list', params={'access_token': await get_token()}, json={'limit': 20, 'seq': r.get('seq') or 0} ) data = res.json() seq = data['seq'] for msg in data['msg_list']: r.xadd('wecom:stream', {'payload': json.dumps(msg)}) r.set('seq', seq) await asyncio.sleep(2)2. 轻量化意图分类——30 MB 模型跑在 CPU
模型结构:MiniLM 6 层 + 全连接 128 → 48 个意图,量化 int8。
推理框架用 ONNXRuntime,单 CPU 线程 4 ms,完全摆脱 GPU 绑定。
训练完把model.onnx与label2id.json放到models/目录,推理封装如下:
import onnxruntime as ort, json, numpy as np from transformers import BertTokenizerFast class IntentEngine: def __init__(self, model_path='models/model.onnx'): self.sess = ort.InferenceSession(model_path) self.tok = BertTokenizerFast.from_pretrained('models/') with open('models/label2id.json') as f: self.id2label = {int(v): k for k, v in json.load(f).items()} def predict(self, text: str, threshold=0.7): inp = self.tok(text, return_tensors='np', max_length=32, truncation=True) logits, = self.sess.run(None, {k: v for k, v in inp.items()}) idx = int(np.argmax(logits)) prob = float(logits[idx]) return self.id2label[idx] if prob > threshold else 'unknown'3. 对话状态机——带超时重置的槽位填充
把“状态”抽象成DialogueState,用 Redis Hash 存储,key=user_id,ttl=300 s。
状态机只关心三件事:意图、已填槽、最后活跃时间。超时后自动回到INIT。
import pydantic, time, redis r = redis.Redis(decode_responses=True) class DialogueState(pydantic.BaseModel): intent: str = '' slots: dict = pydantic.Field(default_factory=dict) ts: float = 0 def load_state(uid: str) -> DialogueState: data = r.hgetall(f'dlg:{uid}') if not data or time.time() - float(data.get('ts', 0)) > 300: return DialogueState() return DialogueState(**data) def save_state(uid: str, state: DialogueState): state.ts = time.time() r.hset(f'dlg:{uid}', mapping=state.dict()) r.expire(f'dlg:{uid}', 300)业务层根据意图调用不同回复模板,并更新槽位;若意图为unknown则转人工。
性能优化:缓存、去重、幂等三板斧
缓存模型结果
用户问题重复率约 28 %,用 Redis 缓存<text_hash, intent>,ttl=10 min,直接砍掉 1/3 的 ONNX 调用。消息去重
企微每条消息有msgid,消费者先SETNX msgid 1,消息处理完再写ACK,保证重启也不重复。幂等性
对“发客服消息”接口,用client_msg_id=uuid+user_id实现幂等;5 分钟内重复调用,企微自动丢弃。
代码片段:
def handle_msg(payload: dict): msg_id = payload['msgid'] if not r.setnx(f'ack:{msg_id}', 1): return # 已处理 uid = payload['from']['user_id'] state = load_state(uid) intent = intent_eng.predict(payload['text']) # ...业务逻辑... save_state(uid, state)避坑指南:token、敏感词与合规
access_token 频率限制
企微强制 2000 次/小时,我们采用单例刷新+分布式锁:- 把 token 存在 Redis,key=
wecom:token,ttl=7000 s; - 刷新脚本用
SET NX EX 10抢锁,防止多实例并发刷新。
- 把 token 存在 Redis,key=
敏感词过滤
先过本地 DFA 树(10 万条 0.2 ms),再调云厂商文本审核 API,双保险。
若命中,机器人直接回“亲亲,换个词试试~”,并打日志留痕。合规性
记录user_id+question+answer7×24 小时,加密落盘,方便审计;
对“退款”“投诉”等高危意图,强制转人工,机器人不直接承诺。
延伸思考:飞书/钉钉也能复用
企微、飞书、钉钉的“拉模式”大同小异,差异主要在签名算法与消息格式。
把核心层(意图分类、状态机、缓存)抽象成独立包,再为每个平台写20 行适配器即可:
- 飞书:把
tenant_access_key换成自建 app 的token,消息 ID 字段叫message_id; - 钉钉:用
topapi/message/get轮询,消息体嵌套在content字段。
只要保证“收、识、答”三步接口不变,迁移基本一天搞定。
小结:让 AI 做 90 % 的体力活
整个项目下来,最大感受是——别硬写规则,让模型先跑。
先用规则收集数据 → 主动学习 → 蒸馏小模型 → 缓存 + 状态机兜底,一套组合拳把并发、状态、合规三个大坑同时填上。
最终机器人上线稳定 1.5 k QPS,平均响应 180 ms,准确率 91 %,夜里终于不用再盯着告警短信失眠。
如果你也在企微、飞书或钉钉上折腾智能客服,不妨把这套模板拿过去改两行,相信能少掉不少头发。祝开发顺利,早日让机器人替你值班。