多数人对“个人微信聊天接口 API”的第一理解是:一个发消息的接口。但真正做过客服机器人就会知道,发一条消息是最简单的一步——难的是让机器人像人一样完成一段对话。核心原因是聊天是有状态的:一句“那多少钱”本身没有意义,它的含义取决于上一句问了什么,而单个发送接口对此一无所知。

所以个人微信聊天接口 API 实际需要四层能力,缺一层对话就不成立:发送(把消息送出去)、接收(Webhook 拿到对方的话)、上下文(记住这段会话之前聊了什么)、时序(保证同一会话的消息按顺序处理)。这篇按这四层展开,最后给出一个能跑多轮对话的完整实现。

接口来自 WTAPI——基于 iPad/Mac 协议的微信机器人接口服务,把消息、好友、群、朋友圈这些能力做成标准 HTTP API。说直白点:你的程序调用它给的网址,就能用这个微信号收消息/发消息/管群,扫码登录、掉线重连这些协议层的工作由它完成。

先给最基础的收发闭环——这是四层里的前两层,跑通它才有后面的讨论:

# 个人微信聊天接口: 收发最小闭环 | 真实接口, 替换 TOKEN 后可直接运行
# 文档站: weiti.apifox.cn   官网: www.chuapi.com
import os, requests
from flask import Flask, request, jsonify

BASE = os.environ.get("WTAPI_BASE", "https://wx.chuapi.com")
TOKEN, APP_ID = os.environ["WTAPI_TOKEN"], os.environ["WTAPI_APPID"]
HDR = {"Content-Type": "application/json", "X-finder-TOKEN": TOKEN}
seen = set()      # 第一层: 去重, 键必须是 Appid + NewMsgId

def send_text(to_wxid: str, content: str):
    """发送接口: toWxid 填好友 wxid 即私聊, 填 xxx@chatroom 即群聊"""
    return requests.post(f"{BASE}/finder/v2/api/message/postText",
        headers=HDR,
        json={"appId": APP_ID, "toWxid": to_wxid, "content": content},
        timeout=15).json()

app = Flask(__name__)

@app.route("/webhook", methods=["POST"])
def webhook():
    p = request.get_json() or {}
    # 先应答: 3 秒内必须返回, 否则平台判定失败并重推
    if p.get("TypeName") == "AddMsg":
        d = p.get("Data", {})
        key = f"{p.get('Appid')}:{d.get('NewMsgId')}"
        if key not in seen:
            seen.add(key)
            from_wxid = d.get("FromUserName", {}).get("string", "")
            text = d.get("Content", {}).get("string", "")
            is_group = from_wxid.endswith("@chatroom")   # 群聊标识看后缀
            if is_group and ":\n" in text:
                text = text.split(":\n", 1)[1]           # 剥发言人前缀
            print(f"{'群' if is_group else '私聊'} {from_wxid}: {text[:40]}")
    return jsonify({})

app.run(host="0.0.0.0", port=8080)

跑通后,手机发消息,终端能打印出来,说明收发两层已经通了。但如果此时直接接业务,你会发现机器人只能“一问一答”,第二句就断片——因为它没有第三层。

一、聊天接口的四层能力

层解决问题缺了会怎样
① 发送把消息送出去完全没有输出
② 接收拿到对方消息无法感知用户
③ 上下文记住这段会话历史多轮对话断片,答非所问
④ 时序同会话消息按序处理回答错乱,回复上上个问题

第 ① ② 层是接口能力,第 ③ ④ 层是你要在应用侧做的事。这也解释了为什么很多人“接口都调通了,机器人却很笨”——问题不在接口,在应用层的状态管理。

二、发送接口:一个方法覆盖私聊和群聊

发送这块最容易产生误解的是:私聊和群聊不需要两个接口。postText 的 toWxid 填什么,消息就发到哪:

toWxid 填什么效果
filehelper发到自己的文件传输助手,开发自测专用
好友 wxid私聊发给该好友
xxx@chatroom发到该群

同一个接口族里还有图片、文件、链接卡片等消息类型,路径不同但参数结构一致:appId + 目标 + 内容。

发送侧有两个工程要点:

  1. 必须有超时。不设 timeout,一次网络卡死就会挂住线程,回调队列随之堵死。
  2. 批量发送必须限流。单号每分钟控制在 30 条以内,多个会话的批量任务逐条错开 3-5 秒——个人号的风控对发送速率比对企业号敏感得多。

三、接收层:聊天和普通接口的本质区别

发送是同步调用,接收是异步回调,这个结构差异决定了聊天接口的特殊性:你无法主动“拉取”消息,只能被动接收。

回调处理有三个硬约束,在聊天场景下尤其不能违反:

约束具体要求聊天场景下的后果
3 秒内应答立即返回 200,业务异步超时重推,用户收到重复回复
幂等去重键为 Appid + Data.NewMsgId单独用 NewMsgId,多号场景会撞
群消息剥前缀正文是 发言人wxid:\n内容群里关键词永远匹配不上

第三条是聊天场景独有的坑:私聊聊得通,一进群就失灵,原因几乎都是没剥前缀。

四、上下文层:让对话能接上第二句

这是把“接口”变成“对话”的关键。最小的上下文管理只需要一个按会话 ID 索引的历史队列:

# 第四层: 会话上下文 + 多轮对话 | 需先装 flask requests
from collections import defaultdict, deque
import threading, time

# 会话 ID: 私聊用对方 wxid, 群聊用群 ID + 发言人(群内每人独立上下文)
history = defaultdict(lambda: deque(maxlen=10))   # 每个会话保留最近 10 条
lock = threading.Lock()                            # 串行保证时序

def conv_key(from_wxid: str, is_group: bool, speaker: str = "") -> str:
    return f"{from_wxid}#{speaker}" if is_group else from_wxid

def on_message(from_wxid: str, text: str, is_group: bool, speaker: str = ""):
    key = conv_key(from_wxid, is_group, speaker)
    with lock:                                     # 第四层: 时序保证
        history[key].append({"role": "user", "content": text, "ts": time.time()})
        ctx = list(history[key][-6:])              # 取最近 3 轮作为上下文

        # 这里接你的意图识别 / 大模型调用
        reply = decide(ctx)

        history[key].append({"role": "assistant", "content": reply, "ts": time.time()})
    send_text(from_wxid, reply)                    # 锁外发送, 避免阻塞

def decide(ctx: list) -> str:
    """占位: 用规则或大模型替换。上下文 ctx 里就是最近几轮对话"""
    last = ctx[-1]["content"] if ctx else ""
    if "多少钱" in last or "价格" in last:
        return "价格表已私信, 再回复「报价」获取完整方案。"
    if "人工" in last:
        return "正在为你转接人工, 工作时间 9:00-21:00。"
    return "收到, 我看看。"

这段代码里有两个设计决定值得说明:

  1. 群聊的会话键是“群 ID + 发言人”。如果只用群 ID,群里三个人的对话会混进同一个上下文,机器人会答非所问。这是群聊场景最常见的上下文错误。
  2. 加锁保证同一会话串行。两个回调并发进来时,不加锁会导致历史顺序错乱,机器人回复上一条的问题。锁只保护内存操作,发送放到锁外,避免接口延迟拖住整个队列。

decide() 用规则占位,换成大模型时只需把 ctx 拼成 messages 数组传入——上下文结构已经是标准格式。

五、群聊和私聊的四处差异

聊天接口在群聊场景下不是“加个参数”那么简单:

维度私聊群聊
会话标识对方 wxid群 ID(@chatroom 结尾)
正文格式纯文本发言人wxid:\n内容
上下文隔离一人一份按发言人分别隔离
响应策略可全量响应建议只响应 / 命令或 @

最后一条是运营层面的经验:群聊里如果不加限制地响应所有消息,机器人很快就会因为刷屏被群主踢掉。这也是为什么第四节的实现里,群消息走的是独立分支。

六、接入自检清单

  1. 发送是否设置了 timeout
  2. 回调是否 3 秒内应答,业务是否异步
  3. 去重键是否含 Appid
  4. 群消息正文前缀是否剥掉
  5. 群聊上下文是否按“群 ID + 发言人”隔离
  6. 同一会话是否有串行保护,避免回复错乱
  7. 发送是否限流,单号每分钟是否控制在 30 条内
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐