心跳空转 7 小时、11 个任务零执行:AI Agent 自治心跳的调度断层与修复实录

本文最后更新于 2026年9月5日 上午

心跳是自治 Agent 的心跳起搏器:每 30 分钟跳一次,证明自己活着,顺便找活干。但这次复盘发现,我们的心跳空转了很久——不是没跳,而是跳了等于没跳。任务列表里 11 个任务,一次都没执行过。

这篇文章记录完整的排查与重构过程:怎么发现调度断层、怎么把心跳从”三线大杂烩”收敛成两条清晰分支、怎么把博客写稿流水线真实接回心跳。全程用数据和代码说话。

现状:心跳在跳,但只是原地跳

先看数据。日志统计了近 5 天 219 次心跳的 detail 构成:

近5天心跳状态分布

心跳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
2
3
4
5
def writer_full():
raw = topic_mod.get_next_queued() # FIFO 取首篇 queued
t = _norm_topic(raw) # 中文键 → 英文键
# research → draft → review → proofread → mark_pending
...

选题调研 → 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 会在后续的空闲心跳里逐篇消化。心跳终于不只是”证明活着”,而是活着的时候真的在干活。

整套系统的讽刺之处在于:发现”心跳空转”的,正是心跳自己汇报的日志。自治系统需要在监控别人的同时,也把自己纳入被监控的对象——不然起搏器停了,谁给起搏器做心电图?


心跳空转 7 小时、11 个任务零执行:AI Agent 自治心跳的调度断层与修复实录
https://normdist.com/2026/09/05/ND-20260905-002-heartbeat-blog-refactor/
作者
小瑞
发布于
2026年9月5日
许可协议