给公众号装上"双引擎":一套 7-Agent AI 写作管线从单号到多账号分线的完整实操
一个人运营的微信公众号矩阵是怎么炼成的:git 基线 → 配置化分线改造 → 错峰定时 → 凭证验证 → 端到端实测 → 性能归因。这篇文章把整个过程的命令、配置和踩过的坑完整记录下来——所有代码都在生产环境跑通过。
先交代背景:我有一套每天自动运行的公众号写作管线,系统 crontab 触发后,依次调用 7 个 AI Agent 完成流水线:
选题 → 研究 → 风格策划 → 写作 → 配图 → 审核 → 推送(只进草稿箱,人工确认后发布)
以前它只服务一个号(社会民生类)。这次的目标是:同一个清晨,两个号各自收到一篇定位完全不同的文章——一号继续写民生追问体,二号"马见寻常"专注日常生活美学。
一、先立回滚点:给存量管线建 git
改造生产环境第一件事永远不是写代码,而是建立可回滚的基线:
cd /root/wechat_pipeline
git init -q
cat > .gitignore << 'EOF'
.env
accounts.json # 含 appid/secret,绝不能进版本历史
run_daily.log
orchestrator_state.json
.style_state*.json
workspace/
backups/
__pycache__/
EOF
git add .gitignore orchestrator.py publish_article_cloud.py prompts/ themes/
git -c user.name=hermes -c user.email="hermes@local" \
commit -q -m "基线:改造前快照"
此后每个逻辑变更单独提交,出问题 git checkout <sha> -- . 一条命令回滚。整个改造结束时 log 长这样:
a36eb8f Phase2: prompts 领域参数化 + 账号主题强制 + 单号定向推送
001ffcc Phase1: orchestrator 多线循环(run隔离/风格状态拆分/跨线查重)
dd6d8de 基线:单线管线(改造前快照)
踩坑记录:.gitignore 必须把 accounts.json、.env 挡在外面。公众号 appsecret 一旦进了 git 历史,删文件都没用,得改写历史。
二、配置即档案:vertical 字段设计
新架构的核心思想:加一个号 = 加一份内容档案,零代码改动。账号表 accounts.json 里每个号带一个 vertical 块:
{
"name": "马见寻常",
"enabled": true,
"appid": "wx…",
"secret": "***",
"theme": "lifestyle",
"author": "马见寻常",
"vertical": {
"category": "日常生活与生活美学",
"topic_seeds": ["极简居家打理", "日常情绪养护", "低成本生活美学",
"作息与习惯养成", "城市轻生活方式"],
"style_bias": "温和务实·落地感强·给出可直接上手的小方案,少鸡汤多真实体验",
"theme": "lifestyle",
"freshness_min": 4
}
}
五个字段各管一段:
| 字段 | 作用 | 生效位置 |
|---|---|---|
category |
领域边界,选题/防串味/审核依据 | 选题 Agent、审核 Agent 的 prompt 头部 |
topic_seeds |
优先选题池,选题飘忽时的抓手 | 选题 Agent |
style_bias |
文风倾向,框定风格轮换的象限 | 风格策划 Agent + 写作 Agent |
theme |
排版主题;写死具体值=强制,auto=每篇自选 |
发布环节选择 CSS |
freshness_min |
时效门槛(1-10),低于它整条线跳过不发 | orchestrator 校验闸门 |
民生号没有 vertical 时走 default_vertical() 缺省档案(category=中国社会民生、追问体、freshness_min=5),保证改造前行为完全等价——这是回归兼容的关键设计。
三、orchestrator:单线 main 重构为多线循环
原来 main() 直接跑 7 步;现在抽出一个 run_line(account),一个账号 = 一条完整生产线:
def run_line(account, ts):
safe = re.sub(r"[^\w\u4e00-\u9fff-]+", "_", account["name"])
run_id = f"{ts}-{safe}" # 工作区按 日期-账号 隔离
run_dir = WORKSPACE_DIR / run_id
ctx = {"run_id": run_id, "step": 0, "account": account}
for step_name, step_func in STEPS:
ok, err = step_func(run_dir, ctx)
if not ok:
return {"status": "failed", "error": f"{step_name}: {err}", ...}
...
return {"status": "success", "media_id": ..., ...}
def main():
ts = new_run_id()
records = []
for account in load_pipeline_accounts(): # 读 accounts.json
try:
rec = run_line(account, ts)
except Exception as e: # 单线崩溃不影响其他线
rec = {"account": account["name"], "status": "failed", "error": str(e)}
records.append(rec)
save_state(records)
return 0 if any(r["status"] == "success" for r in records) else 1
配套四个机制:
1. 注入账号上下文——每个 Agent 的 prompt 头部自动生成:
【当前账号】马见寻常
【内容定位】日常生活与生活美学
【风格倾向】温和务实·落地感强…
【本线要求】选题、写作、审核都必须服务于该账号定位;串味内容一律不合格。
2. 风格历史按账号拆分——.style_state_马见寻常.json,每号独立做 30 期风格轮换,互不污染:
def style_state_path(account_name):
p = PIPELINE_DIR / f".style_state_{account_name}.json"
if not p.exists() and LEGACY.exists():
p.write_text(LEGACY.read_text()) # 旧数据自动迁移
return p
3. 跨线防撞题——recent_topics.json 记录全矩阵近 14 天写过的话题,民生号写过"饭店宰客",生活号想写同一事件会被闸门直接打回换题:
for it in load_recent_topics(days=14):
if topic in it["topic"] or it["topic"] in topic:
return False, f"与 {it['date']} 账号[{it['account']}]撞题"
4. 发布定向——推送脚本带 --account 马见寻常,每号只进自己草稿箱;图片素材按号隔离上传(两号的 media 库互不相通)。
另外给马见寻常新建了一套 lifestyle 排版:themes/lifestyle.css,鼠尾草绿 #4a6b5d 主色、行距 2.0、圆润引用块、重点句浅绿荧光底——生活美学文章的视觉调性,并注册进风格策划 Agent 的主题库。
四、定时任务:改到夜间半价窗口
阿里云 Token Plan 夜间(22:00–08:00)token 半价,而管线凌晨跑完全不影响早上看稿:
# crontab -e:06:00 启动
0 6 * * * bash /root/wechat_pipeline/run_daily.sh >> /root/wechat_pipeline/run_daily.log 2>&1
# Hermes 内的对话总结任务:8:00 → 7:00
# (hermes cron update 615bc7a41ffe --schedule "0 7 * * *")
双线 45 分钟的跑批从 06:00 开始,07:00 前后两篇稿子全部进草稿箱,人起床即收稿——半价窗口整整 10 小时,只用掉 7%。
五、验证凭证:先测 token 再上线
新号凭证填完,别等明早跑批才发现 secret 错了。10 秒钟的预检:
r = requests.get("https://api.weixin.qq.com/cgi-bin/token", params={
"grant_type": "client_credential", "appid": appid, "secret": secret})
if "access_token" in r.json():
print("✅ 凭证有效,且 IP 白名单已包含本机")
else:
print("❌", r.json()["errcode"], r.json()["errmsg"])
errcode 40164 就是忘配 IP 白名单(公众号后台把服务器 IP 加进白名单);41004/40013 则是 secret 填错。这一步同时验证了凭证和白名单两件事,只耗一个 GET。
六、端到端实测:一晚抓出三个真 bug
dry-run 全绿的代码,实跑第一次就崩了。这正是为什么要真人时段跑测试而不是直接等凌晨 cron:
Bug 1:import re 缺失。 run_line() 里用了 re.sub,但文件头没导入。py_compile 只查语法不查名字解析,静态检查看不见它,一运行就 NameError。修复:补一行 import,提交 git。
Bug 2:子进程用了错的 python。 orchestrator 调配图兜底脚本时写死 python3(系统解释器),而系统 python3 没装 requests——两条线全死在配图环节。修复是双保险:orchestrator 改用 venv 绝对路径 + 给系统 python3 补装 requests(防 cron 裸环境)。
Bug 3:theme 强制语义漏洞。 账号显式指定 theme: lifestyle,但代码仍让风格 Agent 的选择覆盖它。修复:显式指定即强制。
修复后的完整跑批结果(end lines=2 success=2):
=== 人间观察笔记(17分钟)===
选题 《一秒点1000下抢专家号,他判了一年》 热度8 时效10 → 民生追问体
=== 马见寻常(28分钟)===
选题 《收假第一天撞上寒露:咱把作息慢慢掰》 热度8 时效10 → 反差体·冷静克制
排版 lifestyle(Agent 自选也是它,强制一致)
同一个早上、同一个热点池,两个号从选题、文风到排版完全分叉,且互相同题互斥——分线目标达成。
七、性能账本:两篇文章为什么跑了 44 分钟
逐 Agent 打点拆出来的时间线:
| 阶段 | 民生线 | 生活线 | 耗时都花在哪 |
|---|---|---|---|
| 选题 | 1'56" | 3'11" | 5-15 次 web 搜索 + 候选评估 |
| 研究 | 4'35" | 5'37" | 逐条搜证、打开来源页、交叉核实 |
| 风格 | 1'49" | 2'12" | 读历史 + 选风格 + 选主题 |
| 写作 | 3'17" | 6'01" | 长文生成,生活稿结构更复杂 |
| 配图 | 2'12" | 2'27" | 提取来源 og:image + 图库搜索兜底 |
| 审核 | 1'34" | 7'23" | 生活线自己去查了气象资料核实"寒露交节时间" |
| 推送 | 0'32" | 0'46" | 上传素材 + draft/add |
四个根因:
- 每步都是全量 Agent 会话:7 步 = 14 次进程冷启动 + system prompt 加载,固定开销约 3-5 分钟。最夸张的是 publisher——实际干活 10 秒,整个会话花 40 秒
- 步骤内部是多轮工具循环:researcher 一次要执行 20-40 个搜索/读页动作,这是"该花的时间"
- 线间串行是设计决定:为规避 API 限流和并发排障复杂度,两线总时长 = 17'+28'
- 质量换时间:生活线比民生线慢 11 分钟,慢在审核 Agent 对无权威来源的话题自发加深了核查
结论:旧单线跑一篇要 38 分钟,现在双线 44 分钟——每线的边际成本其实更低。而且它是无人值守凌晨跑的,44 分钟只占半价窗口的 7%,快慢根本不是矛盾点。真想压时间,线间并行(改 15 行代码,44'→28')和发布环节去 Agent 化(省 1.5')都是现成选项。
八、留给下次的命令备忘
# 回滚任意阶段
cd /root/wechat_pipeline && git checkout dd6d8de -- orchestrator.py prompts/
# 只重推某一号的草稿(先手动生成好 md+三图)
python publish_article_cloud.py --md article_daily_马见寻常.md \
--cover … --body1 … --body2 … --account 马见寻常 --theme lifestyle
# 新增第三号 = 只在 accounts.json 加一段带 vertical 的配置,次日生效
# 查近 14 天全矩阵撞题库
cat recent_topics.json | python3 -m json.tool
整套改造实际写的核心代码不到 200 行,剩下的全是配置和 prompt 措辞。多 Agent 管线的可维护性大概就是这样:逻辑收敛在一个 orchestrator,变化全在数据文件里。
评论 (0)