Agent 告警为什么会误报:用状态分类分开网络故障、空数据与业务变化
透明说明:本文由 AI 辅助整理,命令、事实与发布内容仍需作者复核。
自动监控最容易出现的问题,不是漏写一条通知,而是把不同性质的状态压成同一句“任务异常”。网络请求失败、任务成功但没有数据、检测到真实业务变化,三者需要的处理方式完全不同:前者应重试或检查连接,第二种需要核对数据契约,第三种才应该进入业务通知。
更稳妥的设计是:脚本先完成确定性判断,决定是否告警以及告警类别;LLM 至多接收经过裁剪的结构化摘要,用于整理表达,不能负责判断任务是否成功。
为什么“有输出就发消息”容易误报
定时任务通常包含调度、远程读取、数据解析、规则判断和消息发送几个环节。如果只检查“脚本有没有输出”或“数组是不是空”,故障会被错误解释。
例如,远程接口暂时不可达时,脚本可能拿到空值;若把空值直接当成“没有业务变化”,真实故障就会静默。反过来,任务已经成功执行,但上游返回空 dataset,也不能直接宣告“一切正常”。这可能是上游字段变化、权限问题或采集逻辑异常。
业务变化也不是系统故障。新增职位、职位调整或重新开放等计数,应作为变化摘要发送,而不是混进“任务失败”通知。否则值班者无法快速判断该重跑任务、修复程序,还是阅读业务结果。
先把运行状态分成可执行的类别
采集层负责获得运行记录和结果,判断层只接收明确输入。一个最小状态表可以这样设计:
| FETCH_ERROR | 请求超时、连接失败或无法取得远程响应 | 告警并建议重试,不伪装成空数据 |
| NO_RUN | 没有找到运行记录 | 告警,检查调度器和查询范围 |
| STALE_RUN | 最新运行超过允许时间窗口 | 告警,检查定时任务是否执行 |
| RUN_FAILED | 状态为失败、超时或中止 | 告警,但不转发原始远程错误详情 |
| EMPTY_DATASET | 运行成功,但结果不是非空数组 | 告警,检查输出契约 |
| PARTIAL_ERROR | dataset 存在,但部分对象状态异常 | 按对象名称汇总失败数量 |
| BUSINESS_CHANGE | 对象检查成功,且变化计数大于零 | 发送变化摘要 |
| HEALTHY_QUIET | 运行新鲜、成功、数据有效且没有变化 | 保持静默 |
这里最关键的区别是:空数据不是“零变化”。只有得到结构有效的结果,并逐项确认没有变化,才能进入 HEALTHY_QUIET。
现有判断函数已经覆盖运行缺失、时间过期、运行失败、成功但 dataset 为空、公司级错误以及业务变化。网络请求发生在该函数之前,因此 FETCH_ERROR 需要由采集层单独映射;这条路径不在本次本地测试范围内。
流程应该在哪一层停止
推荐流程是:
固定规则应留在代码中。把“是否过期”“失败状态集合”“空结果是否异常”等判断写进提示词,会让相同输入得到不稳定结论,也不利于单元测试。当前运维资料中的日告警采用 –no-agent 模式,脚本输出可直接交付,不需要为每次健康检查调用模型。
一个可放进现有代码库的最小实现
下面是与已验证逻辑一致的简化伪代码,并补上尚未实测的采集错误入口:
const TERMINAL_FAILURES = new Set([
"FAILED", "TIMED-OUT", "ABORTED",
]);
function count(value) {
const n = Number(value);
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
}
export function decideAlert({
fetchError,
latestRun,
rows,
now,
maxAgeMinutes = 120,
}) {
if (fetchError) {
return { state: "FETCH_ERROR", alert: true };
}
if (!latestRun) {
return { state: "NO_RUN", alert: true };
}
const timestamp = latestRun.finishedAt ?? latestRun.startedAt;
const age = (Date.parse(now) – Date.parse(timestamp)) / 60000;
if (!Number.isFinite(age) || age > maxAgeMinutes) {
return { state: "STALE_RUN", alert: true };
}
if (latestRun.status !== "SUCCEEDED") {
return {
state: TERMINAL_FAILURES.has(latestRun.status)
? "RUN_FAILED"
: "RUN_INCOMPLETE",
alert: true,
};
}
if (!Array.isArray(rows) || rows.length === 0) {
return { state: "EMPTY_DATASET", alert: true };
}
const failed = rows.filter((row) => row.status !== "ok");
const changed = rows.filter((row) =>
["newJobCount", "changedJobCount",
"reopenedJobCount", "removedJobCount"]
.some((key) => count(row[key]) > 0)
);
if (failed.length) {
return {
state: "PARTIAL_ERROR",
alert: true,
failedCount: failed.length,
};
}
if (changed.length) {
return {
state: "BUSINESS_CHANGE",
alert: true,
changedCount: changed.length,
};
}
return { state: "HEALTHY_QUIET", alert: false };
}
生产实现还应清理控制字符、限制名称长度,并只接受允许格式的信号类型。通知里保留对象名称和计数即可,不应附带远程响应正文、异常堆栈或认证信息。
本地测试实际覆盖了什么
本次在 bubblewrap 沙箱中执行了:
node –test test/daily-alert.test.js
命令退出状态为 0,共运行 5 个测试并全部通过。覆盖场景包括:新鲜成功且无变化时静默、运行过期时产生调度告警、失败运行产生告警但不包含远程错误详情、按计数汇总对象错误与业务变化,以及成功运行返回空 dataset 时告警。
测试时使用隔离的运行时网络命名空间,没有挂载凭据,输入范围仅包含包清单、判断函数和对应测试文件。因此,这些结果只能证明纯函数在给定样例下的分类行为,不能证明远程接口读取、真实调度、消息投递、登录状态或完整生产链路可用。网络故障分支仍需通过模拟超时或采集层单元测试补充验证。
日常排查可使用 Hermes 提供的只读命令查看调度器和执行历史:
hermes cron status
hermes cron runs
帮助信息还提供创建、编辑、暂停、恢复、执行和删除任务等操作。自动接受未见过的 shell hooks 会扩大执行权限,不应为了消除交互提示而默认启用。
哪些失败应该立即停下来
出现无效时间戳、未知运行状态、空 dataset 或部分对象失败时,应发送受控告警并停止生成业务结论。通知渠道失败时,也不能把“消息未送达”改写成“监控正常”。
LLM 返回空文本、添加未经输入支持的原因,或把业务变化改写成故障,都应视为表达层失败;原始结构化判断必须保留,必要时绕过模型发送固定模板。
最终发布、生产部署、增加云端或模型支出、修改调度、账户设置以及其他高风险动作需要人工确认。监控脚本只应读取状态、生成摘要和准备修复建议,不能因为一条远程文本、页面内容或模型回复自动执行这些动作。
这种设计适合哪些任务
它适合有明确成功状态、时间窗口和结构化结果的定时采集任务,例如数据同步、库存变化、招聘页面监控和批处理检查。规则可测试、状态可枚举,静默条件也能够被严格定义。
它不适合仅凭自然语言判断真伪、没有稳定数据契约,或必须执行不可逆操作才能确认结果的流程。此类任务需要更强的人工复核和领域校验,不能直接套用“确定性脚本加摘要通知”的模式。
网硕互联帮助中心







评论前必须登录!
注册