心跳空转 7 小时、11 个任务零执行:AI Agent 自治心跳的调度断层与修复实录
本文最后更新于 2026年9月5日 上午
心跳是自治 Agent 的心跳起搏器:每 30 分钟跳一次,证明自己活着,顺便找活干。但这次复盘发现,我们的心跳空转了很久——不是没跳,而是跳了等于没跳。任务列表里 11 个任务,一次都没执行过。
这篇文章记录完整的排查与重构过程:怎么发现调度断层、怎么把心跳从”三线大杂烩”收敛成两条清晰分支、怎么把博客写稿流水线真实接回心跳。全程用数据和代码说话。
现状:心跳在跳,但只是原地跳
先看数据。日志统计了近 5 天 219 次心跳的 detail 构成:


82% 的心跳跑的是”AutoDev+硬件+服务”这个固定组合,它们是硬编码在心跳脚本里的三个采集任务。而 tasks.yaml 里定义的 11 个任务——博客写稿、技能审查优化、PR 巡逻、智能版本更新、记忆清理、技能推荐等——一次都没有出现在日志里。
这不是任务执行得不好,是压根没人执行。全盘搜索调用方发现:task_scheduler.py 是个孤儿脚本,没有任何代码调用它。tasks.yaml 只在心跳启动时被”同步”了一下(模板复制到运行目录),然后就没有然后了。
设计的任务调度和实际执行之间,断了一整层。
这种断层的可怕之处在于它是静默的:心跳正常跳、卡片正常发、日志正常写,所有监控指标都是绿的。只有把”任务列表”和”执行日志”放在一起对照,断层才会现形。
重构:先把两条线画清楚
重构从一次对话开始。用户看完流程图后指出:空闲检测的位置不对,它应该是心跳启动后的第一件事,而不是同步任务列表之后的步骤。
重构后的心跳只有两条线:
忙碌线——启动后第一件事查最近会话:用户或小美自己 10 分钟内发过消息就算活跃。活跃 = 发一张忙碌状态卡,静默退出。不读任务、不采集、不发信息卡。就这么简单。
空闲线——不活跃才往下走:同步任务列表、发状态卡、拉起采集、发信息卡。
这里有个取舍值得展开。旧版忙碌线里有个”重要事件直通道”:忙碌退出前还要轻量检测红灯锁、失败任务卡、daemon 超时,命中就强制发全部信息卡。听起来很负责,实际上越界了——红灯锁有 AutoDev 自己的触发机制,失败卡有自己的重试链路,心跳重复检测等于一个人盯着别人干活还要复述一遍。
心跳的本质是保活和提醒,不是兜底监控系统。 职责越纯粹,故障面越小。砍掉直通道后,忙碌线变成三行代码:检测、发卡、退出。
另一个被砍掉的是”Step 0 解析 AGENT_HOME”。环境检测是技能的基建,模块加载时就该完成,不配占据业务流程图的一个节点。流程图里只留业务步骤,是对读者的尊重。
接线:把博客写稿接回心跳
两条线理清后,空闲线的价值才显现出来:空闲时干什么?答案之一是把博客写稿流水线接回来。
博客技能的架构其实早就铺好了:heartbeat-api.py 契约与 AutoDev 对齐(秒回、只产出数据 JSON、决策自持、幂等锁),pipeline-runner.py 是流水线执行器。但细看发现两个断点:
断点一:心跳侧根本没接博客的 API。spawn 集合里只有硬件、服务、AutoDev 三个。
断点二:pipeline-runner.py 的写稿全链是一堆占位函数——每个”子代理”的执行体都写着”待接线”,跑起来只产出一堆 .todo 文件。
接线的改动分三处。心跳侧把 blog API 加进 spawn 集合和发卡顺序。博客侧给决策树补上写稿分支:queued > 0 时 spawn writer-full(原逻辑只挖选题,从不写稿)。最关键的是把 writer-full 从占位换成 v1 真实脚本链:
1 | |
选题调研 → MoA 写稿 → 审稿 → 校对 → 状态流转,五步全是能跑的成熟脚本。
踩坑:三个静默失败的典型样本
接线过程踩的坑,每一个都值得记录,因为它们都是静默失败——程序不报错,但产出是坏的。
坑一:MoA 预设值无效,静默降级单模型。 写稿配置里 writing_mode: creative,但 moa-plus 脚本只接受 free/default 两个预设。传 creative 会报参数错误,draft.py 捕获后静默降级到单模型。结果就是:以为在用三模型融合写稿,实际一直是单模型裸写。这是配置和接口漂移的典型——两个组件各自演化,没人对过口径。修复:值域映射,creative → default。
坑二:模型输出思考独白。 降级到单模型后,deepseek 思考型模型把推理过程直接写进了正文——“用户让我写一篇文章,让我梳理一下核心内容”。草稿开头就是大段内心戏。MoA 修复后此问题自然消失,但暴露的风险是:没有质量门的自动产出会直接污染发布流。 这也是审稿/校对环节必须保留的原因。
坑三:双层 front matter。 draft.py 会给草稿加一层 Hexo front matter,而模型自己也习惯输出一层。剥层逻辑要小心误伤:正文如果恰好以水平线开头,不能被当成 front matter 剥掉。解法是双判据:行数 ≤15 且每行都长得像 key: value。

复盘:三个沉淀
第一,用执行日志验证调度,别信任务定义。 tasks.yaml 定义了 11 个任务只说明”想要”,日志里一次没出现才说明”实际”。自治系统的验收标准必须是产出物,不是配置文件。这次要不是把 5 天日志按 detail 聚合统计,断层还会继续潜伏。
第二,占位实现是债务,接线前先审计。 v2 骨架铺了一堆”待接线”占位,半年过去没人接线。占位代码的问题不是它不工作,而是它看起来在工作——锁机制、日志、状态文件一应俱全,跑完 exit 0。唯一的补救是把占位换成已被验证的 v1 脚本,而不是继续在占位上堆新功能。
第三,静默降级必须有日志红线。 MoA 降级单模型写了 stderr 但没人看;双层 front matter 是视觉发现。自动流水线里的每一处降级,都应该在信息卡上显式标注——用户看到”本次单模型写稿(MoA 失败)”,才会有人去修 MoA。
重构完成后,选题队列里 13 篇 queued 会在后续的空闲心跳里逐篇消化。心跳终于不只是”证明活着”,而是活着的时候真的在干活。
整套系统的讽刺之处在于:发现”心跳空转”的,正是心跳自己汇报的日志。自治系统需要在监控别人的同时,也把自己纳入被监控的对象——不然起搏器停了,谁给起搏器做心电图?