Appearance
长期在线 Agent 自动化的工程手记
第二部 · Agent 实践 · 第14章
撰写日期:2026-07-03
结论
让一套 Agent 自动化从"演示能跑"变成"长期在线",中间隔着一层工程纪律。我在维护一套跑在本机的团队自动化(知识库镜像、每日记忆筛选、早晚报)的过程中攒下这份手记。核心体会:
长期在线系统的故障几乎都不是模型问题,而是网络抖动、状态管理、多机冲突和告警设计问题。AI 产品经理如果只设计"AI 做什么",不设计"AI 挂了怎么办",系统活不过一周。
1. 单机部署原则:一套自动化只能有一个运行机
这是我踩过的最隐蔽的坑。换新电脑时用迁移助理整机复制,旧机器上的定时任务和事件监听进程原样存活——于是两台机器同时在跑同一套自动化。后果有两个,一明一暗:
| 后果 | 表现 | 危害 |
|---|---|---|
| 明:告警轰炸 | 旧机凭证失效,每 5 分钟发一条失败告警 | 打扰,但至少看得见 |
| 暗:事件被抢 | 两台机器用同一个应用凭证建事件长连接,事件被对方消费 | 本机的实时同步静默失效了 4 个小时,没有任何报错 |
暗的那个才致命:系统看起来一切正常,只是"恰好最近没有事件"。约定必须写进文档红线:事件驱动的自动化只能单机部署,其他人只读产物,不装运行时。 换机要有显式的交接检查单(更新代码 → 重新授权 → 验证一轮 → 旧机退役),而不是依赖整机迁移"应该没问题"。
2. 告警设计:告警通道是会被训练废掉的
告警的敌人不是漏报,是用户学会忽略它。三条实践:
| 原则 | 做法 | 反例的代价 |
|---|---|---|
| 只报失败 | 同步成功静默,git log 即审计 | 每次成功都通知 = 通知等于没通知 |
| 空天静默 | 无事可报的日子一个字不发 | "今日无事"的推送在训练用户划掉推送 |
| 带上来源 | 告警文案前缀机器名 | 多机场景下一样的文案完全无法溯源 |
第三条是这次多机事故里最实用的教训:两台机器发出的失败告警一字不差,排障时只能靠 API 查消息创建时间去反推来源。给告警加一个 [hostname] 前缀是一行改动,省掉的是整晚的排查。
3. 瞬时网络错误:重试要分阶段设计
跑在家用网络上的自动化,SSL 断连、连接重置是常态而非异常。加重试时有一个容易忽略的边界:
| 失败阶段 | 请求是否已到达服务端 | 重试策略 |
|---|---|---|
| 连接阶段(握手失败) | 否 | 任何方法都可以安全重试 |
| 读取阶段(响应中断) | 是,可能已处理 | 只重试幂等方法 |
区别在消息类接口上会变成用户可感知的 bug:读取阶段无脑重试 POST,用户就会收到重复推送。在 HTTP 客户端层统一挂一次重试适配器(指数退避),比在每个调用点写 try/retry 干净得多——一处修改,抓取、同步、告警三条链路同时受益。
4. 排障方法:查事实源,不做剧情推理
这次告警风暴的排查,走对的每一步都是"放弃推理,去查事实":
| 阶段 | 直觉推理(错) | 事实核查(对) |
|---|---|---|
| 收到重复告警 | "大概是网络抖动的延迟送达" | 调消息 API 查每条告警的真实创建时间 |
| 发现每 5 分钟一条 | "本地日志有失败记录,应该就是它" | 对账:本地日志只有 4 次发送成功,实际发出 49 条 |
| 怀疑另有发送方 | "可能是某个残留脚本" | 全盘 grep 告警文案 + 核对 crontab 与消息时刻的秒级对齐 |
| 定位到第二台机器 | "大概是队友装的" | 本地事件流有 4 小时空洞 + 本机定时任务与告警时刻互斥 |
通用形式:当日志和现象对不上时,优先怀疑"还有一个你不知道的运行副本",并用发送端的权威记录(服务端 API、消息创建时间)对账,而不是用接收端的观感(客户端展示时间、通知到达顺序)推理。 客户端时间戳、迟到的推送、聚合展示都会说谎,服务端创建时间不会。
5. 运行成本要实测并公开
"跑一套 AI 自动化很贵"是默认印象,实测后往往相反。这套系统的边际成本:
| 项 | 实测 |
|---|---|
| 知识库镜像同步 | 0 模型 token(纯 API 搬运 + git) |
| LLM 筛选 | 每天 ≤ 2 次调用,走订阅套餐,无单独账单 |
| 常驻进程 | 2 个 daemon,约 4 MB 内存 |
| API 调用 | 每天约 3k 次轮询级调用,远低于配额 |
把成本写进公开文档有两个作用:给团队一个"这东西不贵,放心跑"的确定性;也给自己一个基线——当调用量突然偏离基线,本身就是故障信号。
6. 面向读者的仓库文档结构
自动化的仓库文档很容易写成作者视角的流水账。有效的结构是先问"谁会打开这个 README",再决定放什么:
| 读者 | 想知道 | 对应内容 |
|---|---|---|
| 团队同事 | 这是什么、怎么用、别碰哪里 | README:影响说明、使用表、红线清单 |
| 未来的维护者 | 怎么部署、有哪些坑 | 子目录 README + 运维笔记(已知坑逐条记录,含"已解决"状态) |
| 排障时的自己 | 日志在哪、先看什么 | 排障入口一节,直接给路径 |
易变的细节(成本数字、坑列表)放运维笔记,稳定的契约(红线、语义约定)放 README——README 的更新频率应该远低于系统本身的迭代频率。
7. 降级路径纪律:可以晚,不能静默发次品
依赖 LLM 生成内容的推送管线,生成一定会失败(限流窗口、登录态、超时)。我踩过的版本是:生成失败就静默降级成"原文开头截取"照发——群里收到一张明显没消化过的次品卡,而日志里只有一个返回码,连失败原因都没留下。两个错要分开治:
| 错误 | 治法 |
|---|---|
| 降级不留痕 | 失败必须把子进程的 stdout/stderr 记进日志——限流和登录态失败的返回码一样,只有原始输出能区分 |
| 降级不可逆 | 推送是发出去就收不回的动作。失败的正确动作是留队重试,不是就地降级 |
宁可晚发五分钟,不发一张次品卡。降级路径一律要"留痕 + 可重试",静默兜底只配出现在有截止时间的最后一层。
8. 三层兜底:不丢、不哑、不烂尾
上一节的留队重试机制后来真的接住了一次两个半小时的认证故障——没发一张次品。但那次暴露了新缺口:卡了两个半小时,是负责人先发现的,不是系统上报的。 "人先于系统发现故障"本身就是故障。完整的防线是三层:
| 层 | 机制 | 兜住什么 |
|---|---|---|
| 不丢 | 失败任务留队,固定周期(如 5 分钟)自动重试 | 一切瞬时故障,自愈无感 |
| 不哑 | 卡队超过阈值(如 30 分钟)自动私聊负责人一次,附排障入口和手动放行命令 | 持续故障必须有人知道;告警只发一次防刷屏 |
| 不烂尾 | 超过截止(如 6 小时)允许降级兜底发出 | 长期故障不至于把产物永久压在队列里 |
这套结构下"漏发"在机制上不可能:结局要么是正常发出,要么是迟发且有人早就知道。另一个细节:告警文案里的"处方"必须实测。我第一版告警写的恢复方法是凭推理写的,后来实测无效——排障文案里每一条建议都应该被验证过,否则它在最需要它的时刻误导人。
9. Headless 认证:会话凭证撑不起无人值守
订阅型 CLI 工具的登录态通常存在系统钥匙串里,绑定用户的 GUI 会话。交互使用时 token 过期会自动刷新,你永远无感;但无人值守的定时任务(cron 类调度器跑在非 GUI 上下文)在 token 过期后读不到也刷不了凭证,从此每一轮都报"未登录"。更迷惑的是:此时人工在终端跑一次完全正常——故障只存在于调度器的上下文里。
| 方案 | 原理 | 局限 |
|---|---|---|
| 换调度器上下文 | 把定时任务从 cron 挪进用户会话级调度器(macOS 的 LaunchAgent 类),钥匙串访问与交互一致 | 仍依赖登录态,账号在别处登出照样失效 |
| 长期凭证(推荐) | 用官方 headless 授权流程(如 claude setup-token)生成一年期 token,600 权限文件存放,运行时注入环境变量 | 到期需人工轮换一次;走订阅额度,无额外账单 |
| 计费 API key | key 不过期,彻底无登录态 | 从订阅池切到按量付费,调用量大时是真实账单 |
配套两条实操教训:headless 抓取交互式 CLI 的输出时,终端 UI 的重绘会吞字符、凭证和提示文字之间可能没有分隔符——提取出的凭证必须先验证可用再销毁现场,顺序反了就要重跑整个授权流程;代码层给凭证文件做兜底(文件不存在时回退默认认证),迁移过程不产生断点。
10. 静默分支审计:每个 continue 都是未来的漏发
推送管线的"漏发"复盘到最后,往往不是异常炸了——异常有 traceback、有告警,反而安全。真正漏掉东西的是那些当初看起来合理的静默跳过分支:
| 静默分支 | 当初的理由 | 实际后果 |
|---|---|---|
| 元数据没找到 → 跳过 | "可能是脏数据" | 改名/同步间隙中的条目被永久丢弃 |
| 内容文件缺失 → 跳过 | "没内容播什么" | 上游镜像故障期间的条目被永久丢弃 |
第二条的实翻车值得记:两个同名文档在镜像里共享同一个文件路径,删除其中一个节点时文件被一起删掉,另一个活文档就成了"登记表里有、磁盘上没有"——推送管线读到空内容,走静默跳过分支,无声漏播。上游的一个边界 bug,靠下游的一个 continue 变成了用户可见的"怎么没推送"。
三条纪律:
- 白名单式静默:队列型管线里只允许语义明确的白名单跳过(如"自己发布的内容不重复播报"),其余一切异常一律留队 + 留痕,交给重试和告警兜底。
- 入出对账:入队数和播报数必须能对上,出现差值就说明有静默分支在漏——和第 4 节"查事实源"是同一个思路,对账优于推理。
- 走管线自愈,不走手工补发:修复方式是把该条目从状态登记表里摘掉、让同步把它当新条目重新走一遍全流程(重新导出 → 自动入队 → 正常播报)。手工补发绕过管线,修不了根因,还会污染"入出对账"的账本。
11. 从本机到云端:换的是位置,不是单机约束
常有人问这套长期在线的自动化"能不能搬到云端常驻"。能,而且往往更合适——会睡眠、会关机的个人电脑,本就不适合托管一条需要 7×24 保持的事件长连接;一台常开的云主机在这件事上天然更稳。但迁移时真正要守的不是"云比本地强",而是第 1 节那条单机约束在换了位置之后依然成立。
| 维度 | 个人电脑 | 常开云主机 |
|---|---|---|
| 事件长连接稳定性 | 受睡眠/关机影响,随手合盖就断 | 常开,明显更适合 |
| 单连接约束 | 成立 | 同样成立——新旧位置不能并存 |
| 无 GUI 环境的认证 | 依赖钥匙串,踩第 9 节的坑 | 无桌面钥匙串,反而绕开该坑,但要先验证长期凭证在目标环境可用 |
迁移三条纪律:
- 硬切换,不能双跑:有状态的单连接服务,迁移必须一刀切——旧位置的进程全部停掉,再在新位置起。新旧并存就是第 1 节的多机抢事件,而且这次是你亲手制造的。
- 分清可迁移与环境绑定:代码、凭证文件、状态数据可以带走;绑定局域网或特定硬件的部分(例如只在内网访问的展示屏)留在原地,云端帮不上也不该搬。
- 认证方式要能在目标环境复现:迁移前先在新环境跑通一次 headless 认证(见第 9 节),确认长期凭证可用,再做切换——别等切过去才发现"未登录"。