前言
大多数微信机器人只能做"一问一答"——用户发一句话,机器人回一句,下一条消息又从零开始。这在客服场景、AI助手场景中严重制约体验:用户说"帮我查一下上海的天气",紧接着说"那北京呢",机器人却完全不知道"那北京"指什么。多轮对话与上下文记忆正是解决这一问题的核心技术。本文从原理到实操,结合 WechatApi 的 iPad 协议接入方案,带你完整搭建一套有记忆的微信机器人系统。
一、多轮对话的核心挑战:状态管理
单轮对话无状态,处理起来最简单——收到消息,生成回复,结束。多轮对话的难点在于:谁在说话、说了什么、上下文是什么,这三件事必须在每一轮都正确维护。
具体来说,开发者面临三个层面的挑战:
1. 会话识别:微信消息进来时,如何判断这条消息属于哪个"对话线程"?个人微信中,一个 fromUser(发消息人的微信ID)通常对应一个对话,但群聊里多人同时聊,需要用 fromUser + roomId 联合作为会话键。
2. 上下文窗口:大语言模型(LLM)有 token 限制,不可能把从第一条消息开始的所有历史都塞进去。需要设计滑动窗口、摘要压缩等策略,在"记得够多"和"不超限"之间取得平衡。
3. 超时与重置:用户可能隔了一天再回来聊,此时继续上次的上下文还是重置?通常以会话超时时间(如 30 分钟)为界,超时则自动开启新对话。
理解了这三个挑战,后面的方案设计就有了明确目标。
二、WechatApi 接入:拿到稳定的消息流
要实现多轮对话,首先得有稳定可靠的消息接收机制。个人微信并没有官方开放接口,市面上常见方案是 Hook 注入或协议层模拟。WechatApi 基于 iPad 协议实现,无需在 Windows 上运行微信客户端,设备以 iPad 身份登录,稳定性和存活率显著优于 PC Hook 方案。
接入流程简述:
- 前往 控制台 注册账号,获取
appId(设备ID)和VideosApi-token(鉴权 Token)。 - 在控制台配置消息回调 Webhook 地址(你自己服务器的 HTTPS 接口)。
- 扫码登录个人微信账号,设备上线后,所有收到的消息都会以 POST 请求推送到你的 Webhook。
消息体示例(WechatApi 推送格式):
json{
"appId": "wx_device_001",
"msgType": 1,
"content": "那北京呢",
"fromUser": "wxid_abc123",
"toUser": "wxid_self",
"roomId": "",
"msgId": "7890123456",
"createTime": 1718000000
}
roomId 为空表示私聊,有值则是群消息。fromUser 是发送者 wxid,这两个字段组合起来就是我们的会话键。
三、上下文存储方案设计
拿到消息流后,需要设计上下文存储结构。推荐使用 Redis 作为会话存储,原因是:读写速度快、天然支持过期(TTL 即超时重置)、结构灵活。
会话键设计:
| 场景 | 会话键格式 | 示例 |
|---|---|---|
| 私聊 | session:{appId}:{fromUser} | session:wx001:wxid_abc123 |
| 群聊(区分发言人) | session:{appId}:{roomId}:{fromUser} | session:wx001:room456:wxid_abc123 |
| 群聊(群共享上下文) | session:{appId}:{roomId} | session:wx001:room456 |
群聊是否共享上下文取决于业务需求。客服机器人通常用私聊或区分发言人;群助手(如 FAQ 机器人)有时需要群共享,让后续问题能引用前面的讨论。
存储结构:
每个会话键对应一个 JSON 列表,按时间顺序存放对话轮次:
json[
{"role": "user", "content": "帮我查一下上海的天气", "ts": 1718000000},
{"role": "assistant", "content": "上海今天晴,26°C。", "ts": 1718000005},
{"role": "user", "content": "那北京呢", "ts": 1718000060},
{"role": "assistant", "content": "北京今天多云,22°C。", "ts": 1718000065}
]
设置 TTL(如 1800 秒 = 30 分钟),用户超过 30 分钟不说话,下次发消息时 Redis 键已过期,自动开启新对话,无需手动清理。
滑动窗口控制:
为了不超出 LLM 的 token 限制,每次组装 prompt 时只取最近 N 轮(如最近 10 轮)。如果需要保留更长期的记忆,可以定期对历史做摘要压缩,把"前 20 轮"压缩成一段 200 字的摘要,插入到上下文开头。
四、完整代码实现:Python Flask 示例
下面给出一个完整的 Python 实现骨架,涵盖消息接收、上下文管理、调用 LLM、通过 WechatApi 回复三个环节。
pythonimport json
import redis
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
WECHAT_API_BASE = "https://api.wechatapi.net" # 示意域名,以控制台为准
VIDEOS_API_TOKEN = "your_videos_api_token_here"
APP_ID = "your_app_id_here"
SESSION_TTL = 1800 # 30 分钟超时
MAX_HISTORY = 10 # 最多保留最近 10 轮
def get_session_key(from_user, room_id=""):
if room_id:
return f"session:{APP_ID}:{room_id}:{from_user}"
return f"session:{APP_ID}:{from_user}"
def load_history(session_key):
raw = r.get(session_key)
if raw:
return json.loads(raw)
return []
def save_history(session_key, history):
# 只保留最近 MAX_HISTORY 轮(每轮 2 条:user + assistant)
if len(history) > MAX_HISTORY * 2:
history = history[-(MAX_HISTORY * 2):]
r.set(session_key, json.dumps(history, ensure_ascii=False), ex=SESSION_TTL)
def call_llm(messages):
"""调用你选择的 LLM,返回回复文本(此处为示意,替换为实际 SDK)"""
# 例如调用 OpenAI / Claude / 本地模型 等
# response = openai.ChatCompletion.create(model="gpt-4o", messages=messages)
# return response.choices[0].message.content
return "(LLM 回复占位)"
def send_wechat_message(to_user, content, room_id=""):
"""通过 WechatApi 发送消息"""
payload = {
"appId": APP_ID,
"toUser": to_user,
"content": content,
}
if room_id:
payload["roomId"] = room_id
headers = {
"VideosApi-token": VIDEOS_API_TOKEN,
"Content-Type": "application/json"
}
resp = requests.post(
f"{WECHAT_API_BASE}/send-text", # 示意路径
json=payload,
headers=headers
)
return resp.json()
@app.route("/webhook", methods=["POST"])
def webhook():
data = request.json
msg_type = data.get("msgType", 0)
if msg_type != 1: # 只处理文本消息
return jsonify({"ret": 200})
from_user = data["fromUser"]
room_id = data.get("roomId", "")
user_text = data["content"]
session_key = get_session_key(from_user, room_id)
history = load_history(session_key)
# 追加用户消息
history.append({"role": "user", "content": user_text})
# 组装带系统提示的消息列表
messages = [{"role": "system", "content": "你是一个专业的微信智能助手。"}]
messages += [{"role": h["role"], "content": h["content"]} for h in history]
# 调用 LLM
reply = call_llm(messages)
# 追加 assistant 回复,保存历史
history.append({"role": "assistant", "content": reply})
save_history(session_key, history)
# 发送回复
send_wechat_message(from_user, reply, room_id)
return jsonify({"ret": 200})
if __name__ == "__main__":
app.run(port=8080)
代码结构清晰,核心逻辑在 /webhook 路由中:取历史 → 组装 prompt → 调用 LLM → 存历史 → 发回复,五步走。
五、主动重置与指令识别
上下文记忆并非越多越好。有几种情况需要主动重置会话:
用户主动重置:识别特定指令,如"重新开始"、"清除记录"、"新对话",收到后立即删除 Redis 键。
话题切换检测:可以在每轮调用 LLM 时加一个判断:*当前用户输入与上一轮的话题是否发生了明显切换?* 如果是,可以只保留最近 2 轮而非 10 轮,减少无关上下文干扰。
业务场景隔离:对于微信客服机器人而言,一次客服会话结束(如用户说"谢谢,再见")后应自动归档并重置上下文,下次来的是新问题,不应该继承上次的投诉记录。
用 bash 快速测试 Redis 会话状态:
bash# 查看某用户的会话历史(原始 JSON)
redis-cli GET "session:wx_device_001:wxid_abc123"
# 查看剩余 TTL(秒)
redis-cli TTL "session:wx_device_001:wxid_abc123"
# 手动重置某用户会话
redis-cli DEL "session:wx_device_001:wxid_abc123"
# 查看所有活跃会话数量
redis-cli KEYS "session:wx_device_001:*" | wc -l
六、群聊多轮对话的特殊处理
群聊场景远比私聊复杂。几个关键问题:
@触发 vs 关键词触发:群里消息量大,机器人不应该响应所有消息。通常有两种激活方式——被 @ 时响应(content 里包含 @机器人昵称);或者消息包含特定前缀/关键词时响应。WechatApi 推送的消息体中 content 字段包含完整原始文本,开发者自行判断是否需要回复即可。
多人并发上下文:群里同时有多人在和机器人对话,需要以 roomId + fromUser 组合作为会话键(而不是只用 roomId),确保 A 的上下文不会干扰 B。
群共享摘要:微信群管理机器人有时需要对群内讨论做摘要,这种场景下可以维护一个群级别的"公共上下文",独立于每个用户的私有会话,专门用于群内容聚合。
防刷保护:多轮对话系统要做好频率限制。同一用户 1 分钟内发超过 N 条消息,进入冷却状态,不再调用 LLM,避免被刷爆接口费用。Redis 的 INCR + EXPIRE 组合可以轻松实现滑动窗口限流。
七、上下文摘要压缩:让记忆走得更远
当对话超过 20 轮,直接截断会丢失重要信息(比如用户在第 3 轮说了自己的姓名和需求,但第 20 轮才追问细节)。摘要压缩是更优雅的解法:
压缩时机:当历史长度超过阈值(如 20 轮),触发摘要。
压缩方式:取前 15 轮历史,让 LLM 生成一段 200 字以内的摘要,描述对话的核心背景信息(用户身份、已解决问题、待处理事项等)。
重组上下文:把摘要作为一条特殊的 system 消息插入,保留最近 5 轮原始对话,组合成新的上下文继续对话。
这样,即使对话持续几小时,机器人也不会"失忆",同时 token 消耗始终在可控范围内。对于需要长期跟进用户的微信SCRM场景,还可以将摘要持久化到数据库(如 MySQL),实现跨天、跨设备的长期用户画像积累。
八、注意事项与常见坑
1. 消息去重:网络抖动可能导致同一条消息被推送两次。用 msgId 做幂等判断,同一 msgId 收到后直接忽略第二次。
2. 异步化处理:Webhook 接口必须在 5 秒内返回 200,否则 WechatApi 会认为推送失败并重试。耗时的 LLM 调用要放入消息队列(Celery、RQ 等)异步处理,Webhook 本身只负责接收和入队。
3. 长文本分段发送:LLM 有时会生成很长的回复,微信单条消息过长会被截断。检测回复长度,超过阈值(如 500 字)时自动分段,多次调用发送接口。
4. 表情与特殊字符:微信消息中可能含 emoji、引用消息、语音消息等非文本内容。非文本消息(msgType != 1)在 Webhook 处直接过滤,避免传入 LLM 导致报错。
5. 多设备一致性:如果同一个微信号在多台设备(多个 appId)上登录,需要统一会话键前缀策略,或者限定只在一台设备上运行机器人逻辑。WechatApi 的 个人微信API 文档中有关于多设备管理的说明,上线前建议仔细阅读。
小结
多轮对话与上下文记忆不是一个单点功能,而是消息接收、会话管理、LLM 调用、回复发送四个环节的协同设计。核心要素归纳为:稳定的消息流(iPad 协议接入,避免 PC Hook 频繁掉线)、结构化的会话存储(Redis + TTL,私聊/群聊分别建键)、合理的上下文窗口(滑动截断 + 摘要压缩)、健壮的工程细节(消息去重、异步化、限流)。
基于 WechatApi 搭建的多轮对话机器人,从接入到上线,核心代码量不超过 300 行,是个人开发者和中小团队快速落地 AI 微信助手的高性价比路径。如有疑问,可前往 开发文档 或 控制台 进一步了解。
