我最近在跑一批 AI Agent 定时任务,发现最大的痛点不是 Agent 跑得慢,而是它跑完以后我只能干等。尤其是一些异步任务,比如夜间批量处理日志、凌晨抓取数据、后台做 RAG 索引更新,Agent 执行完成后如果没人盯着,出错了也很难第一时间发现。后来我干脆写了一个微信推送服务,把 Agent 的任务状态、耗时、错误信息、Token 消耗这些都推到微信里。这篇文章就把我完整的实现思路、代码、部署经验和踩坑记录分享出来,给同样在搞 AI Agent 开发的人一个参考。
文章适合这几类人看:一是正在做 AI Agent 开发,想让任务通知不再依赖“人肉刷新”的开发者;二是想了解微信生态里有哪些合规、稳定、成本低的推送通道的运维或后端同学;三是想自己封装一个可复用推送服务,但不知道从哪下手的入门者。我会讲清楚每一个设计背后的原因,以及我在实际使用中踩过的坑,确保你拿到这套方案后能直接落地。
1. 需求拆解:AI Agent 跑完任务,通知这事为什么值得专门做
1.1 Agent 任务的异步特性与通知的必要性
AI Agent 和传统接口最大的不同在于:它不是一个“请求-响应”模型。传统 API 调用,你发一个请求,几百毫秒或者几秒内返回结果,同步等待完全没问题。但 Agent 不一样,它内部会有多步推理、工具调用、上下文管理,甚至可能循环执行多个子任务。一个完整任务跑完,短则几十秒,长则十几分钟甚至几小时。如果你在代码里同步等待,调用方会被长时间占用,超时风险也非常大。
所以我一般会把 Agent 任务设计成异步执行:提交任务后立刻拿到一个 task_id,Agent 在后台跑,跑完再通过回调或轮询获取结果。这个模式下,通知就成了刚需。没有通知,用户就只能不停查任务状态、看日志,这完全违背了 Agent“自动化”的初衷。你甚至可以让 Agent 在处理完一批数据后,自己调用推送服务把结果汇报给你,这样整个链路就是一个完整的自动闭环。
1.2 为什么选择微信通道而不是邮件、钉钉、Slack
很多人第一反应是发邮件,但邮件在移动端的触达率其实不高。我自己的经验是,邮件很容易被埋没在垃圾箱和营销邮件里,除非你设置了专门的收件规则,否则经常是半天后才看到。钉钉、飞书、Slack 在开发者群体中也有不少支持者,但它们都有一个共同问题:你需要在对应的 App 里才能收到通知,如果对方不使用这个协作工具,通道就废了。
微信在国内的普及率和使用频率是最高的,哪怕是凌晨,微信消息的到达率也远高于邮件。而且微信消息支持长文本、Markdown、卡片消息,足够承载 Agent 的任务摘要和错误堆栈。正因如此,我把目标锁定在微信通道,而不是搞一套复杂的多端推送矩阵。微信本身也提供了多种合规的推送方式,只要选对,稳定性和触达率都有保障。
1.3 微信生态里有哪些合规可行的推送通道
这里我要先说一个原则:不推荐、也不讨论任何通过逆向或非官方接口操作个人微信的方案。这类方案风险极高,轻则账号被限制,重则直接封号,而且依赖破解接口,随时可能失效。合规可用的通道,我实际用过的有以下几种:
| 通道 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Server酱 | 通过微信服务号模板消息推送到微信 | 接入简单,个人可申请,有免费额度 | 免费版有每日条数限制,消息模板固定 | 个人开发者、小流量任务通知 |
| PushPlus | 通过微信服务号推送 | 支持一对多推送,免费额度较宽 | 消息格式有限,需要关注服务号 | 多用户通知、团队小规模使用 |
| 企业微信群机器人 | 通过企业微信群 Webhook 发送群消息 | 免费、支持 Markdown、可 @ 成员 | 需要建企业微信群,通知发到群里而非个人 | 团队协作、多人共享任务状态 |
| 微信公众号模板消息 | 通过公众号接口向关注者推送 | 官方渠道,稳定 | 需要认证服务号,权限申请繁琐 | 生产级、面向 C 端用户的通知 |
| 邮件通知 | SMTP 发送 | 通用、无平台限制 | 到达率低、实时性差 | 兜底通道、非实时通知 |
我自己最终采用的是“Server酱 + 企业微信群机器人”双通道方案。Server酱负责把消息推到个人微信,适合一个人管理多个 Agent 的场景;企业微信群机器人则把关键任务状态同步到团队群,方便协作。两套通道互相独立,其中一个挂了另一个还能兜底。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 服务设计:一个轻量微信推送服务的核心逻辑
2.1 整体架构与消息流程
我先说清楚整个推送服务的架构,不复杂,但每个模块都有它存在的理由。
code复制AI Agent 任务执行完毕
│
▼
推送服务客户端(封装 API)
│
▼
消息格式化 + 去重 + 限流
│
▼
推送通道适配层(Server酱 / PushPlus / 企业微信机器人)
│
▼
微信服务器
│
▼
用户手机 / 企业微信群
这个架构看起来像一个简单的 HTTP 调用链,但我在实践中把消息格式化、去重、限流、失败重试都加到了客户端 SDK 里。原因很简单:Agent 任务的通知请求量不算大,完全没必要为了它独立部署一套 Kafka 之类的消息队列。用一个带内存队列的客户端就能扛住,而且部署成本为零。如果你的 Agent 集群规模很大、推送量很高,再把消息格式化和发送逻辑拆成独立服务也不迟。
2.2 消息模板与结构化设计
通知不是“任务跑完了”五个字那么简单。我最初只推一句话,后来发现完全没用——收到消息后还是得点开日志看详细情况,效率一点没提升。所以我重新设计了消息模板,区分“成功”和“失败”两种场景。
成功消息包含这些字段:
- 任务名称:一眼看出是哪个任务
- 任务 ID:方便关联日志
- 执行状态:Success / Failed
- 开始时间、结束时间、总耗时:判断任务性能
- Token 消耗:LLM 任务必看,用于成本统计
- 数据概览:比如处理了多少条记录、生成了几个文件
- 详情链接:如果任务系统有 Web 页面,直接把 URL 带过来
失败消息在成功消息的基础上,额外增加:
- 错误类型:超时、API 报错、数据异常等
- 错误摘要:异常信息的精简版
- 重试建议:是可重试的错,还是需要人工介入的错
- 堆栈片段:如果通道支持,附上关键堆栈,方便快速定位
用 Server酱的 Markdown 格式举一个实际例子:
markdown复制## 夜间日志分析任务执行失败
- 任务名称:nginx-error-log-analysis
- 任务 ID:task_8f7e_20250115
- 执行状态:**Failed**
- 错误类型:OpenAI API Timeout
- 错误摘要:Request timed out after 120s
- 耗时:113.8 秒
- Token 消耗:2,403
- 详情链接:https://agent.example.com/tasks/task_8f7e_20250115
这个模板你直接抄就能用,字段顺序按“用户最关心的信息”排列,优先看到的是成败状态和错误类型。实践下来,运维同事收到消息后 90% 的情况下不需要再打开日志系统,问题定位效率提升非常明显。
2.3 去重与限流:为什么需要,怎么做
推送服务的两个隐形杀手是重复消息和频率超限。
重复消息在 Agent 场景里太常见了。比如一个任务失败后,重试机制自动触发,又执行了一次,如果 Agent 底层有回调逻辑,可能每个节点都会触发一次通知。你可能会收到“任务失败”连发三遍。所以推送服务必须有去重机制。
去重我用的方案是基于任务 ID + 执行批次号(run_id)做消息指纹。在推送客户端里维护一个最近 N 条消息指纹的缓存,如果新消息的指纹和缓存里的重复,直接丢弃。单机部署用内存就能实现,多实例部署换成 Redis,用 SETNX 或者 SET key value EX 600 NX 做幂等控制。我建议去重窗口至少 10 分钟,因为 Agent 的重试通常在这个时间窗口内完成。
限流则是为了防止你或你的 Agent 把推送通道的配额打爆。Server酱、PushPlus 都有单日发送量限制,企业微信群机器人官方限制是每个机器人每分钟最多 20 条消息。你可以在客户端里实现一个简单的令牌桶,每发一条取一个令牌,桶里没有令牌就排队等待而不是丢弃,保证消息最终都能发出去。
2.4 失败重试与优雅降级
推送服务本身也可能失败。微信接口偶发超时、网络抖动、通道服务商升级维护,这些我都遇到过。所以在推送客户端里必须有重试机制。
我的重试策略是:失败后延迟 1 秒重试,再失败延迟 5 秒重试,第三次失败就不重试了,把消息标记为“推送失败”并写入本地日志。同时走降级通道——如果主通道失败,自动尝试备用通道。比如 Server酱失败时转 PushPlus,两个都失败时发邮件到自己的邮箱。
这里有个坑要注意:重试一定要控制次数。如果通道已经挂了,你重试 10 次只会加重通道压力,还会让自己被限流。而重试 3 次已经是极限,除非你有明确的理由认为这是一个瞬时错误。
3. 核心实现:从零写一个适配 Agent 的微信推送客户端
3.1 选型:requests 封装 vs FastAPI 独立服务
我考虑过两种实现形态:一种是直接封装一个 Python 库,在 Agent 进程内调用;另一种是做一个独立的 FastAPI 推送服务,Agent 通过 HTTP API 调用。
这两种方案我都实际用过,最后选择的是“Python SDK 为主,轻量 HTTP 服务为辅”的混合方案。原因很简单:
- 如果 Agent 本身就是 Python 写的,直接在进程内调用 SDK,不需要额外的网络开销和部署维护。
- 如果团队里有不同语言栈的 Agent(比如 Node.js、Java),或者 Agent 跑在别人的服务器上,内嵌 SDK 不方便,那就需要提供 HTTP API。
SDK 的核心优点是没有额外依赖,一个类文件就能搞定;缺点是只有 Python 能用。HTTP 服务的优势是跨语言、跨团队复用,但需要部署和运维成本。我建议你先用 SDK,等真正发现“其他语言也需要推送”时再单独部署服务端。
3.2 以 Server酱 / PushPlus 为例的核心代码
下面是我实际在用的 Python 推送客户端核心代码,兼容 Server酱 和 PushPlus 两个通道。您可以直接复制,替换 key 就能用。
python复制# notify.py
import time
import hashlib
import requests
from typing import Optional, Dict, Any
from dataclasses import dataclass, field
@dataclass
class PushMessage:
title: str
content: str
task_id: Optional[str] = None
run_id: Optional[str] = None
status: str = "info" # success / error / warning / info
channel: str = "serverchan"
@property
def fingerprint(self) -> str:
"""生成消息指纹,用于去重"""
raw = f"{self.task_id}:{self.run_id}:{self.status}"
return hashlib.md5(raw.encode()).hexdigest()
class WeChatPusher:
"""微信推送客户端,兼容 Server酱、PushPlus"""
def __init__(self, serverchan_key: str = "", pushplus_key: str = ""):
self.serverchan_key = serverchan_key
self.pushplus_key = pushplus_key
self._recent_fingerprints = {}
self._queue = []
def push(self, msg: PushMessage) -> bool:
"""统一入口:去重 -> 限流 -> 选通道 -> 发送 -> 带重试"""
fp = msg.fingerprint
if self._is_duplicate(fp):
print(f"[notify] 重复消息,丢弃: {fp}")
return True
self._record_fingerprint(fp)
# 按状态选择主通道
if msg.channel == "serverchan":
return self._send_via_serverchan(msg)
elif msg.channel == "pushplus":
return self._send_via_pushplus(msg)
else:
print(f"[notify] 未知通道: {msg.channel}")
return False
def _send_via_serverchan(self, msg: PushMessage) -> bool:
url = f"https://sctapi.ftqq.com/{self.serverchan_key}.send"
payload = {
"title": msg.title,
"desp": msg.content,
}
return self._post_with_retry(url, payload)
def _send_via_pushplus(self, msg: PushMessage) -> bool:
url = "https://www.pushplus.plus/send"
payload = {
"token": self.pushplus_key,
"title": msg.title,
"content": msg.content,
"template": "markdown",
}
return self._post_with_retry(url, payload)
def _post_with_retry(self, url: str, payload: Dict[str, Any], max_retry: int = 3) -> bool:
for attempt in range(max_retry):
try:
resp = requests.post(url, json=payload, timeout=10)
if resp.status_code == 200:
data = resp.json()
# Server酱 / PushPlus 都返回 code 字段,0 为成功
if data.get("code") == 0 or data.get("code") == 200:
return True
else:
print(f"[notify] 接口返回失败: {data}")
else:
print(f"[notify] HTTP {resp.status_code}: {resp.text[:200]}")
except Exception as e:
print(f"[notify] 请求异常: {e}")
time.sleep(1 * (attempt + 1))
return False
def _is_duplicate(self, fp: str, window_seconds: int = 600) -> bool:
now = time.time()
if fp in self._recent_fingerprints:
if now - self._recent_fingerprints[fp] < window_seconds:
return True
return False
def _record_fingerprint(self, fp: str):
self._recent_fingerprints[fp] = time.time()
# 简单清理,防止内存无限增长
if len(self._recent_fingerprints) > 1000:
now = time.time()
self._recent_fingerprints = {
k: v for k, v in self._recent_fingerprints.items()
if now - v < 1800
}
这个类我把关键的点都包进去了:指纹去重、POST 重试、超时控制。Server酱 的接口路径里需要拼上 SendKey,PushPlus 则是通过 token 字段认证。两者都支持 Markdown 格式的 content 或 desp 字段,所以消息模板可以通用。
另外要说一下,requests 库的 timeout 参数一定要写,不然网络异常时客户端会一直阻塞。10 秒是我调出来的最优值,既能覆盖大部分网络波动,又不会让 Agent 的调用线程等太久。
3.3 Agent 集成:在 LangChain / LlamaIndex / AutoGPT 等框架中接入
有了推送客户端,下一步就是把它接进 Agent。我发现很多人卡在这一步,不知道怎么改框架代码。这里分享几种我实践过的接入方式。
方式一:装饰器自动通知。适用于调用 Agent 入口函数时统一加通知。比如你有一个 run_agent(task_name) 函数,可以用一个装饰器包一层:
python复制def notify_on_complete(notifier: WeChatPusher):
def decorator(func):
def wrapper(*args, **kwargs):
task_id = kwargs.get("task_id", str(uuid.uuid4()))
run_id = str(uuid.uuid4())
start_time = time.time()
try:
result = func(*args, **kwargs)
elapsed = time.time() - start_time
msg = build_success_msg(
task_id=task_id,
run_id=run_id,
elapsed=elapsed,
summary=str(result)[:200],
)
notifier.push(msg)
return result
except Exception as e:
elapsed = time.time() - start_time
msg = build_error_msg(
task_id=task_id,
run_id=run_id,
elapsed=elapsed,
error=str(e),
)
notifier.push(msg)
raise
return wrapper
return decorator
装饰器的好处是侵入性小,你不需要修改 LangChain 框架内部代码,只要在你自己的业务入口函数上打一个 @notify_on_complete(notifier) 就行。这个方案我在多种框架上都验证过,包括 LangChain、LlamaIndex,只要 Agent 最终是在你自己的代码里被调用的,就适用。
方式二:Agent 工具调用。如果你的 Agent 本身有 tool / function calling 的能力,可以给它注册一个 send_wechat_notification 工具,让 Agent 自己在任务完成时调用。比如用 LangChain 的 @tool 装饰器:
python复制from langchain.tools import tool
@tool
def send_wechat_notification(content: str, status: str = "info") -> str:
"""任务执行完毕后,将结果推送到微信。content 为消息正文,status 为 success/error/warning/info。"""
msg = PushMessage(
title="Agent 任务通知",
content=content,
status=status,
)
ok = pusher.push(msg)
return "ok" if ok else "failed"
这种方式适合你想让 Agent“自主”决定是否通知、何时通知的场景。比如 Agent 判断任务遇到瓶颈需要人工介入时,自己调用这个工具发一条告警。这就是真正的 Agent 自动化闭环。
方式三:任务队列回调。如果你的 Agent 任务是通过 Celery、Arq、Dramatiq 之类的异步任务队列执行的,可以在任务完成回调或 after_return 钩子里调用推送。这种方式最可靠,因为任务无论成功失败都会进入回调。
3.4 企业微信群机器人 webhook 的另一种实现
Server酱 和 PushPlus 都是个人级别的推送,但团队协作场景里,很多人希望把 Agent 的任务状态同步到企业微信群里。企业微信群机器人是免费且稳定的方案,代码也很简单。
python复制def send_to_wecom_group(webhook_url: str, content: str, mentioned_list: list = None):
"""
发送 Markdown 消息到企业微信群
webhook_url 形如: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx
"""
payload = {
"msgtype": "markdown",
"markdown": {
"content": content,
},
}
if mentioned_list:
payload["markdown"]["mentioned_list"] = mentioned_list
resp = requests.post(webhook_url, json=payload, timeout=10)
data = resp.json()
if data.get("errcode") == 0:
return True
else:
print(f"[wecom] 发送失败: {data}")
return False
企业微信群机器人的 Markdown 格式和 Server酱略有不同,它支持 @ 成员,通过 mentioned_list 字段传成员 UserID。如果想让 @所有人,可以传 "@all"。团队内部我一般会设一个“Agent 任务通知”群,把每个 Agent 的关键状态都发到这个群里,方便大家统一查看。
这里有一个开发时容易忽略的点:企业微信群机器人的 Webhook 地址不要出现在代码仓库里,特别是如果仓库是公开的或者有外部协作者。建议放在环境变量或者配置中心里,密钥一旦泄露,恶意用户可以往你的群里发垃圾消息。
3.5 安全与密钥管理:webhook 泄露怎么办
我见过不少人在代码里硬编码 Server酱 SendKey 或企业微信 Webhook,这非常危险。SendKey 和 Webhook 就是“钥匙”,拿到它的人可以直接用你的通道发消息,不花你的钱,但会造成信息干扰甚至诈骗风险。
我现在的做法是:
- 环境变量存储密钥,比如
SERVERCHAN_KEY、PUSHPLUS_KEY、WECOM_WEBHOOK。Python 里用os.environ.get()读取,代码库不出现真实密钥。 - 生产环境使用密钥管理服务或
.env文件,但.env不要提交到 Git。 - 如果怀疑密钥泄露,马上到 Server酱 / PushPlus / 企业微信后台重置 key。企业微信群机器人可以一键换 Webhook 地址,Server酱可以在后台刷新 SendKey。
顺便提醒一句:企业微信群机器人的 Webhook 地址本身带一个 key 参数,只要拿到 key 就能向群里发消息。所以它的权限粒度很粗,只能“发”,不能“收”。如果你的需求是双向交互(比如在群里回复命令,让 Agent 执行任务),就需要用企业微信自建应用的方式,那种权限体系更完整,但配置复杂度也更高。我目前的通知场景只有单向推送,用 Webhook 就够了。
4. 部署与运维:让推送服务稳定跑在生产环境
4.1 Docker 部署与进程守护
如果上面 3.1 里的推送客户端是内嵌在 Agent 进程里的,那就不存在额外部署问题,Agent 进程活着推送功能就活着。但如果你听了我的建议,后续把推送服务独立成了一个 HTTP API,那部署就需要注意进程守护了。
我推荐用 Docker 部署,理由很朴素:环境隔离、启动一致、回滚方便。下面是一份最简单可用的 Dockerfile:
dockerfile复制FROM python:3.11-slim
WORKDIR /app
RUN pip install fastapi uvicorn requests
COPY my_notify_service.py /app/my_notify_service.py
EXPOSE 8080
CMD ["uvicorn", "my_notify_service:app", "--host", "0.0.0.0", "--port", "8080"]
如果你不想用 Docker,直接用 systemd 守护进程也可以。关键是不能裸跑一个 python app.py,因为终端一关进程就没了。用 nohup 也不行,进程崩溃后不会自动重启。要么 systemd,要么 supervisor,总之要有一个守护。
4.2 日志与可观测性:推送失败怎么排查
推送服务看似简单,但一旦出了问题,你如果连“这条消息到底发出去没有”都不知道,排查起来会很痛苦。所以日志一定要打全。
我自己的日志格式是这样的:
code复制2025-01-15 02:13:44 [INFO] task_id=task_8f7e run_id=run_uuid status=success channel=serverchan fp=abc123 action=push_result result=ok
2025-01-15 02:13:45 [WARN] task_id=task_9g8d run_id=run_uuid status=error channel=serverchan fp=def456 action=push_result result=retry_1 error="timeout"
2025-01-15 02:13:48 [ERROR] task_id=task_9g8d run_id=run_uuid status=error channel=pushplus fp=def456 action=push_result result=failed error="invalid token"
包含时间戳、任务 ID、执行批次、通道、动作、结果、错误信息。这样一条日志就能回答“这条消息通过哪个通道发的?成功没有?失败原因是什么?”。
另外,我强烈建议给推送服务加一个健康检查接口。如果你用的是 FastAPI,加一个 GET /healthz 接口,返回 {"status": "ok"}。这样无论你是用 Docker Healthcheck,还是接入 Prometheus 监控,都有一个最基础的可观测入口。
4.3 频控与渠道配额:微信渠道的真实限制
这一节是我实测下来的真实数据,不是官方文档的复读,因为文档写的限制和实际体验还是有区别的。
Server酱:免费版每天有一定的条数限制,官方政策会调整。我个人的经验是,每天几十条以内的通知是没问题的,但如果你的 Agent 很频繁(比如每 10 分钟跑一个任务),那免费额度很快会被耗尽,需要升级付费方案或者砍掉一些不重要的通知。
PushPlus:免费版同样有每日条数限制,也支持一对多推送,适合团队小规模使用。它的一个特点是内容支持 HTML,自由度更高,但 Markdown 也是没问题的。
企业微信群机器人:官方限制是每个机器人每分钟最多 20 条消息,这个限制是我实际测过的,突发连续发送到 20 条以上会返回 errmsg: "超过频率限制, 请稍后再试"。这个限制其实对大多数 Agent 通知来说绰绰有余。你不太可能每分钟有 20 条任务结束通知,除非你一次性批量跑了几十个 Agent。
如果你发现自己的通知量已经到了每天几百上千条,那就需要考虑分级通知策略:成功消息只在异常时或者关键节点推送,普通成功消息汇总成日报,失败消息实时推送。这也是我后面会讲到的一个实践心得。
4.4 成本:免费额度够用吗
直接给结论:个人开发者和中小团队场景,免费额度完全够用。前提是你做“分级通知”,而不是每条成功消息都推。
我自己现在每天跑 50 个 Agent 任务,其中大约 10 个是关键任务。我的通知策略是:
- 关键任务无论成功失败都实时推送
- 普通任务只在失败时推送
- 每天 23:00 发一条汇总日报,包含当天所有任务的执行情况
这样算下来,每天推送条数也就 20 条左右,Server酱 的免费额度绰绰有余,PushPlus 基本用不上。所以成本几乎为零。
如果你非要给每一个 Agent 的每一次成功执行都推送,那免费额度肯定是不够的,这时候我建议你算一笔账:是升级付费方案更划算,还是减少推送频率更划算。我的经验是后者更划算,因为“每条都推”和“关键才推”带来的用户注意力差别是显著的,消息太多反而会被用户屏蔽。
5. 常见问题与排查技巧实录
5.1 消息收到了但没有内容/格式错乱
这是最高频的问题。最常见的原因是微信通道对 Markdown 的支持有限制。Server酱 的 desp 字段虽然支持 Markdown,但某些 HTML 标签会被过滤;企业微信群机器人的 Markdown 也不支持所有语法,比如表格、图片、视频都不支持。
我把踩过的坑总结成一份速查表,你推送后格式不对时可以先对照排查:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 没有换行 | 只用了 \n,但频道要求 \n\n 才换行 |
统一用 \n\n 分隔段落 |
| 标题没加粗 | 用了 # 标题语法,频道不支持 |
改用 **标题** 加粗 |
| 链接打不开 | 链接中有空格或特殊字符 | 用 URL 编码 |
| 表格显示为纯文本 | 企业微信群机器人不支持表格语法 | 改用列表或普通文本 |
| 中文乱码 | 没有指定 UTF-8 | 在请求头加 Content-Type: application/json; charset=utf-8 |
我建议你在写推送模板时,不要套用完整的 GitHub Markdown 语法,只使用“加粗、斜体、链接、列表”这几个最基础的元素,兼容性最好。
5.2 手机收不到通知
手机收不到通知,第一反应不要怪代码,按这个顺序排查:
- 先看推送服务日志,确认消息是否发送成功。
- 如果日志显示成功,检查微信“服务号通知”是否被折叠并静音。在微信里搜索对应的服务号(Server酱 的消息来自“方糖”服务号),点进服务号设置,确保“接收消息”是开着的。
- 如果你的手机开启了专注模式或勿扰模式,微信消息可能被折叠到通知中心,但不会消失,下拉通知栏看看。
- 检查服务号是否被用户取消关注。如果取关了,推送会失败,Server酱 后台一般会显示发送失败或用户数异常。
还有一个我自己实际遇到的问题:Server酱 的免费版发送频率过高时,服务商会自动降级推送,消息延迟到达。如果延迟严重,先查服务状态页。
5.3 重试风暴:任务重跑导致重复通知
这是一个很容易被忽视但危害很大的场景。Agent 任务失败后,调度系统会自动重试。如果重试逻辑和通知逻辑没有做好联动,你就会收到“任务失败”三条、重试成功后又来三条“任务成功”,整个微信群被刷屏。
我的解决办法是加一个“通知抑制窗口”:任务重试时,同一个 task_id 的通知在 10 分钟内不重复发送。上面代码里的 fingerprint 就是干这个用的。把 task_id + run_id 作为指纹,run_id 在一次完整执行周期内保持不变,重试时 run_id 相同,指纹就相同,重试产生的通知会被拦截。只有真正进入下一次执行周期时 run_id 变化,才会发新通知。
如果你用的是 Celery 之类的任务队列,可以在任务执行前生成一个 run_id,存到任务上下文中,推送时带上。这样即使任务重跑,只要 run_id 不变,通知就能被正确去重。
5.4 通知服务本身挂了怎么办
最后聊聊一个终极问题:如果推送服务本身挂了,你的 Agent 任务状态是不是就彻底看不见了?
我的做法是“双通道 + 本地兜底”。双通道前面已经讲过,主通道失败走备用通道。本地兜底是指:当所有推送通道都失败时,把消息写入本地文件或数据库,之后可以通过定时任务补发。
最简单的实现就是在推送客户端里加一个“失败消息队列”,失败时把 PushMessage 序列化后追加到一个 pending_messages.jsonl 文件。然后写一个定时脚本,每隔 5 分钟扫描这个文件,尝试重新推送。
python复制import json
def save_pending(msg: PushMessage):
with open("pending_messages.jsonl", "a") as f:
f.write(json.dumps({...}) + "\n")
def flush_pending(pusher: WeChatPusher):
lines = open("pending_messages.jsonl").readlines()
remaining = []
for line in lines:
data = json.loads(line)
msg = PushMessage(**data)
ok = pusher.push(msg)
if not ok:
remaining.append(line)
with open("pending_messages.jsonl", "w") as f:
f.writelines(remaining)
这套兜底逻辑我理解为“飞机上的备用降落伞”——平时用不上,但真到关键时刻能救命。它不需要很复杂,一个文件加一个定时任务就够了。
最后再分享一个小技巧
我在实际使用中发现,给微信推送服务加一个“自动生成任务日报”的功能,收益远远大于把所有消息都实时推送。每天晚上 23 点,把当天所有 Agent 任务的执行情况汇总成一张表,推送到微信和企业微信群。这样白天不需要频繁被消息打扰,晚上复盘时一眼就能看到哪些任务成功、哪些失败、哪些耗时异常。这个习惯让我从“被动接收通知”变成了“主动掌握全局”,整个 Agent 集群的运行状态透明了很多。
如果你也在做 AI Agent 开发,我建议先不要盲目追求复杂的消息队列、消息总线,一个封装良好的微信推送 SDK 加一个简单的去重限流,就能解决 95% 的通知需求。等真正出现多团队、多渠道、高并发的需求时,再演进成独立服务也不迟。整套代码量不到 300 行,部署成本几乎为零,回报却是实打实的——你再也不用盯着终端屏幕等 Agent 跑完了。
