Flowness 运行机制:从任务派发到故障恢复 · 阅读系列导读 →

版本说明(2026年10月26日补):本文写的是 2026 年 9 月 11 日上线的那一版。9 月 24 日起,这条路径改成按每个机器来源登记的条件键归并,不再用 14 天窗口和 40 字符问题指纹;每一轮来源报告它当前看到的全部情况,某个条件消失超过宽限期(接力巡检 6 小时、健康自检 24 小时),对应告警就自动关闭;没有登记的机器来源会被拒收。换掉之前,这一版在线上没有按下文说的归并:9 月 14 日到 24 日,健康自检新开的 21 条告警,问题文本按这一版的规则规范化后完全相同,却一直同时开着,没有被归进最早那条。重写时的记录把原因归为它读取的开集 9 月 13 日后不再更新,这正是下文“核对告警状态”一节提到的开集过时风险。
Flowness 是我们用来组织多个 AI Agent 持续工作的运行环境。Agent 执行任务,后台脚本检查运行状态,遇到需要人处理的情况,就提出一条告警。本文介绍的是告警产生端的去重:它接收机器身份、向负责人提出的问题、具体原因和当前时间,返回可以归并的既有告警;找不到时,由调用方继续创建新条目。
这个环节处理的是一种很具体的负担。巡检每隔一段时间运行一次,同一个故障尚未解决,CPU 数字、时间戳和进程编号却一直变化。每轮都新建一条,待办列表就会把一个问题显示成十几个问题。仅仅减少通知次数,又不能解决列表里已经堆积的重复条目。
以未解决的告警为依据,避免来回重复开单
最简单的去重可以只记住上一次消息。如果上一条是 A,新来的是 A,就不重复。但消息按 A、B、A、B 交替出现时,每次都与上一条不同,这个办法便失效了。保存一段时间内的指纹集合能改善这种情况,却还缺少一项信息:这件事到底处理完没有?
Flowness 会查询“开集”,也就是当前仍未结束的告警集合。只有同一机器身份、同一问题指纹,而且还在允许的时间范围内,旧条目才可能成为归并目标。这样,判断对象从“这句话最近出现过”变成“这件待处理事项是否还存在”。
以下是一个教学过程。健康检查机器第一次发来“健康自检发现异常,需要处理吗”,系统建立告警 A。接下来两次检查,问题相同,只有采样数值变化。在 A 仍开放的条件下,两次都归到 A,复发计数从 0 增至 2。列表仍保留一项待处理问题,同时知道它又发生了两次。这里的 2 只计算后续复发,不包含最初创建 A 的那一次。
若 A 已经解决,下一次异常就应重新提出。去重没有代替修复,也不把“又发生了”解释为“已经有人处理了”。

指纹按来源选择字段,不能只比较一句模板
问题指纹先对文字做规则化处理:依次替换长十六进制编号、符合规则的带前缀编号和数字,再保留规范化问题的前 40 个字符。这个顺序用于减少不断变化的编号和数值对分组的干扰。它是确定性的文本规则,不是在理解故障语义。
不同机器还需要不同取材方式。健康自检来源 system-alert-bridge 只使用问题文本。它的原因字段经常包含波动的采样值,若把这些变化都当作新问题,去重就很难发生。其他未特别登记的机器来源,这一版里也采用问题文本这一档。
接力巡检来源 system-relay-watch 则同时看问题与原因。它可能用同一句“需要你处理吗”报告写入被拒、心跳过期或待处理消息积压。如果只比较问题,就会把这些不同故障压在同一个条目下面。
原因也不能简单接在问题后面再截取 40 字:问题本身较长时,真正区分故障的部分还没进入比较就被截掉了。因此,代码把完整的规范化原因另算一个 10 位十六进制短摘要,追加到问题指纹后。这个摘要用于分组,不承担安全校验。
回到例子,若同一接力机器先报“写入被拒”,再报“心跳过期”,两者的原因摘要不同,可以各开一条。若只是“心跳过期 56 分钟”变成“心跳过期 86 分钟”,数字规则化后可能归入同一条。数字差异究竟应不应该保留,是来源规则需要承担的判断,不能指望短摘要替我们决定。
核对告警状态,并保留明确的失效回退
查询到一条开放记录后,代码还会检查另一份生命周期视图。两份视图都从事件账本派生,但用不同的投影表示状态。生命周期视图可读时,只有它也认为条目仍处于提出状态,旧条目才能继续用于去重。若它显示已解决,就不能拿这条旧记录压掉新告警。
两种读取失败有不同后果。开集读不出来,函数返回没有命中,让调用方继续新建;生命周期视图读不出来,则退回只查开集的行为,仍可能命中旧条目。后者并不意味着已经完成双重核对,也没有消除开集过时的风险。
时间窗口提供另一层限制。这一版里窗口是 14 天,达到或超过窗口的旧条目不再用于归并。若同时命中几条,选择符合条件的最早一条,保留最初出现的时间。窗口限制旧锚点能使用多久;它不意味着故障满 14 天自动解决,也不是按最后复发时刻不断延长的冷却期。
记录复发,保留人的独立求助
命中后,复发记录保存计数、末次时刻,以及最多 300 字符的最近原因样本。它写入单独的统计文件,避免每次重复检查都继续增加一条正式告警事件。这也意味着它没有逐次保存所有原因全文,不能把这份计数记录当作完整故障历史。
还有一个直接的身份边界:只有以 system- 开头的机器会话参与这项去重。具名工作会话即使说出完全相同的话,也不会被本模块合并。两个 Agent 分别等待一次决定,文字相同不代表它们承担的是同一项工作。
源码与已有测试能够说明这些分支如何区分:同模板归并、原因分档、具名会话保留、已结束条目不再作为锚点。本文没有重跑服务器全链路,也不把源码注释中的历史告警数量当成本轮效果。这个机制已经把降噪需要回答的问题写得很清楚:谁在重复报告,报告的是哪件事,那件事是否仍待处理。规则围绕这三项信息展开,才能少开重复单,同时给新的问题留下入口。
公众号排版与配图
本篇另备有明亮、适合手机连续阅读的公众号排版稿,配图按照本文机制单独绘制。查看公众号版,或下载 Markdown、HTML、PNG 与 SVG 配图包。