# 把微信做成 AI 入口网关,而不是一个聊天脚本
技术支持 wechatapi.net

很多人做微信机器人,第一反应都是先写一个能跑通的脚本:
@app.post("/wechat/callback")
async def handle_wechat(request: Request):
data = await request.json()
text = data["Data"]["Content"]["string"]
result = call_ai(text)
send_text(result)
return {"ok": True}
这种写法最大的优点是快,当天就能跑起来;但它也有一个致命问题:你得到的只是一个脚本,不是一个能承载场景的入口层。
我最近在做的事情,是把微信接成 OpenClaw 的一个入口。做下来以后我最大的认知变化就是:
如果你的目标是长期做入口能力,就不要把项目理解成机器人脚本,而要把它理解成消息网关。

为什么“脚本思维”很快会碰到天花板
一开始,脚本思维的问题不明显,因为你的目标只有一个:
收消息,调模型,发回去。
但项目一旦继续往前走,就会很快碰到下面这些问题:
1. 私聊和群聊逻辑混在一起
私聊天然是单用户上下文。
群聊则至少有两种模式:
- 整个群共享一个上下文
- 群里每个人独立上下文
如果一开始没有 session 设计,后面会非常难补。
2. 消息回调和 AI 调用耦合太紧
如果回调一进来就直接等模型返回,慢一点就会把整个回调压住。
尤其是用 CLI 调 OpenClaw 时,每次消息都要重新起进程,延迟更明显。
3. 没有状态层
没有状态层,后面这些都很痛:
- 去重
- 限流
- 排队
- 日志
- 调试
- 会话积压控制
4. 无法自然扩展到企微、TG、飞书
如果从第一天就写死成“微信机器人脚本”,那以后每接一个入口都得重来一遍。
我后来是怎么改成“入口网关”的
我现在把整体链路拆成了更像网关的结构:
微信回调
↓
回调解析层
↓
消息标准化
↓
Session 路由
↓
Worker 队列
↓
OpenClaw 调用层
↓
微信发送层
核心思想很简单:
- 微信只是入口
- OpenClaw 是能力内核
- 中间的网关层负责把两边真正接起来
关键设计一:按 session 分片,而不是简单起线程
一个很常见的误区是:消息来了就开线程。
这样做在早期看起来很快,但很快就会遇到上下文乱序问题。
我最后用的是按 session 分片:
def shard_index_for_session(session_id: str, worker_count: int) -> int:
h = int(hashlib.md5(session_id.encode("utf-8")).hexdigest(), 16)
return h % worker_count
这样做的好处是:
- 不同会话可以并行
- 同一个会话固定进同一个 worker
- 不会把同一段上下文跑乱
这个设计在群聊场景尤其重要。
关键设计二:先把消息标准化,再谈业务逻辑
微信回调结构看起来很简单,实际上真正难的是不同消息场景的判断。
比如我后来按回调文档做了这些判断:
def parse_wechat_payload(data):
wxid = str(data.get("Wxid", "") or "").strip()
msg_data = data.get("Data", {}) or {}
from_user = (msg_data.get("FromUserName", {}) or {}).get("string", "")
to_user = (msg_data.get("ToUserName", {}) or {}).get("string", "")
raw_content = (msg_data.get("Content", {}) or {}).get("string", "")
msg_type = msg_data.get("MsgType")
is_self = bool(wxid and from_user == wxid)
is_group = from_user.endswith("@chatroom") or to_user.endswith("@chatroom")
if from_user.endswith("@chatroom"):
chat_id = from_user
elif to_user.endswith("@chatroom"):
chat_id = to_user
else:
chat_id = from_user if not is_self else to_user
sender_wxid = from_user
actual_text = (raw_content or "").strip()
if is_group and raw_content and ":\n" in raw_content:
possible_sender, possible_text = raw_content.split(":\n", 1)
if possible_sender.startswith("wxid_"):
sender_wxid = possible_sender.strip()
actual_text = possible_text.strip()
return {
"wxid": wxid,
"msg_type": msg_type,
"chat_id": chat_id,
"sender_wxid": sender_wxid,
"actual_text": actual_text,
"is_group": is_group,
"is_self": is_self,
}
这一步做对以后,后面的业务逻辑才会稳定。
关键设计三:先只处理文本,把链路跑稳
微信支持的消息类型很多:
- 文本
- 图片
- 语音
- 视频
- 文件
- 引用
- 名片
- 系统通知
如果一开始全部都接,系统很快会变得复杂。
所以我现在的策略很明确:
第一阶段只处理文本消息。
代码也非常直接:
if msg_type != 1:
logger.info("暂时只处理文本消息,忽略 MsgType=%s", msg_type)
return {"status": "ignored_msg_type"}
先把主链路做稳,比过早堆能力重要得多。
关键设计四:默认忽略自己发出去的消息
这是最容易忽略、但一定要做的防线。
if parsed.get("is_self"):
logger.info("忽略自己发送的消息: %s", parsed.get("msg_id"))
return {"status": "ignored_self"}
如果不做这一步,很容易出现:
- 机器人收到自己的回复
- 继续把自己的回复丢给 OpenClaw
- 最后形成自回环
这套思路对我最大的帮助
把项目从“脚本”改成“入口网关”以后,我最大的感受是:
很多之前混乱的问题,开始变得可以拆解了。
比如:
- 延迟问题,能明确判断是在微信层还是在 OpenClaw CLI 层
- 群聊问题,能明确知道是 chat_id 还是 sender_wxid 处理错了
- 会话问题,能通过 session 设计做策略切换
- 未来接企微,也不再需要完全重写
我现在的判断
如果你只是想做一个会回消息的 demo,脚本思维没问题。
但如果你想继续往下做:
- 私域入口
- 智能客服
- 群聊 AI 助手
- 多入口通道层
那就应该尽早把它往“入口网关”方向去设计。
一句话总结:
脚本解决的是“能不能跑”,网关解决的是“能不能长期承载场景”。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)