巡检我不交给模型判

ops-agent 建造笔记第 2 篇:巡检采集路径为什么不能依赖 LLM 可用性,确定性巡检如何做到没拿到证据就不报告正常。

巡检我不交给模型判

《ops-agent 建造笔记》第 2 篇 代码基线:v0.1.0-rc.1,提交 d897d2c95e70(2026-10-25 发布,候选版) 主要代码:agent/checks.py、agent/schedule.py、agent/tools.py


0. 如果 key 欠费了,早上的巡检会怎样

W2 之前,巡检长这样:定时任务到点,把"检查一下磁盘内存服务"这句话发给模型,模型调几个 bash 命令,写一段结论。

顺着这个流程往下想一步:key 欠费了,或者网络断了,或者返回 401,总之 LLM 不可用。定时巡检会发生什么?如果没有别的采集路径,模型就不会调用探针,检查事实也无从产生。而恰恰在 LLM 不可用的时候,待查机器可能正在出问题——这两件事没有必然关系,巡检系统却把它们绑死了。这是我设计时要拆开的依赖,不是一段线上欠费事故。

还有一种更隐蔽的。模型可用,但它今天状态不好,看一眼 df -h 输出里的 87%,写了个"磁盘正常"。阈值是 85%。模型不是判错,它根本没有"阈值"这个概念,除非我每次都在 prompt 里重复一遍。而 prompt 里的规则是软约束,模型遵不遵守没法验证。

这两件事凑在一起,W2 的核心决定就很自然地出来了:检查项固定写在代码里,模型只做解读。opsagent inspect 不需要 API Key 也能出结论;定时任务可以声明 --checks all,每轮先在调用模型之前跑确定性探针,结果直接落盘。这样即使 LLM 从第一轮就不可用,规则异常仍然会上报、仍然会通知。


1. 三组检查,五种状态

检查分三组(agent/checks.py),默认全跑,缺任一组都不算完整:

分组 采集内容 判定
connectivity uname -s 的 stdout(顺带确定探针平台) 命令失败或无法确定平台记 error
resources df -P -k、/proc/meminfo 或 vm_stat、/proc/loadavg 或 sysctl 与阈值比:≥警告 warn、≥异常 critical
services systemctl show、docker inspect --format 期望运行的单元 failed/inactive 为 critical

判定规则里我花心思最多的是状态集:ok / warn / critical / unknown / error,五种,没有更多。前三种是“有足够观测,可以比较”;后两种承认观测不足或探针失败。具体怎么归类由各探针决定,例如连通性失败记 error,因连通失败而没跑到的资源/服务项记 unknown,不能只凭错误听起来是哪一类去猜。

unknown 是这套设计里我最在意的部分。常见做法是采不到就当正常,或者干脆不出现在报告里。我的口径是反的:采不到,不等于健康。报告结论只有在"无异常 + 三组全跑 + 无未采集项"三者同时成立时才是「正常」,否则分别是「警告」「异常」「不完整」。一台完全联系不上的机器,巡检结论必须是「不完整」,不能让它在报告里消失。

这个口径的副作用是,巡检系统自己的毛病也藏不住了。探针挂了、权限丢了、输出格式变了,都会以 unknown/error 的形式浮出来,而不是伪装成一切安好。

完整性和异常程度还得分开看。一个已采到的磁盘 critical 加一项未采集服务,可以同时给出“异常”和“完整性不完整”;不能因为顶层已经是异常,就把缺证据的部分抹掉。反过来,首次只检查本机、还没有配 services,会得到“不完整”而不是“安装失败”。明确停用的目标会列在覆盖说明里,但不计入已启用目标的完整性分母。

最近一次复审还找到一个小而实际的误判:uname 的 stdout 为空、stderr 却有文本时,不能把 stderr 第一行当成平台名。现在 _connectivity() 先过 _stdout_only(),没有平台就报错。退出码是 0,也不能替代输出本身。

阈值和管理对象都写在 targets.json 里,和 SSH 目标是同一份注册表——巡检清单就是资产清单:

{
  "web1": {
    "host": "10.0.0.11", "user": "ops", "tags": ["test", "web"],
    "services": [{"type": "systemd", "name": "nginx"},
                 {"type": "docker", "name": "api", "expect": "running"}],
    "checks": {"disk_paths": ["/", "/data"], "disk_warn_pct": 85,
               "disk_crit_pct": 95, "timeout": 20, "budget_seconds": 120}
  }
}

配置解析是严格的:未知字段、非法阈值、非法目标顶层字段一律报错。拼错的键不应该静默生效,disk_warn_pcnt: 85 这种 typo 如果被忽略,我会一直以为阈值是 85,实际跑的是默认值。


2. 时间和数量都要封顶

探针可以在本机跑,也可以经 SSH 打到已注册目标上。两边都可能卡,例如 df 等待一个不响应的挂载。没有预算的话,一台卡住的主机会拖住整轮巡检,后面的目标也跟着等;我不想让一个探针的等待时间决定整份报告还能不能产出。

所以我加了两层预算:

  • 单目标 budget_seconds(默认 120s):这台机器的所有探针加起来不能超过它,超了的检查项记 unknown,不会静默漏项。
  • 每目标 60 次探针的硬上界(PROBE_BUDGET):防配置失误导致探针数量爆炸,比如服务清单里配了几百项。

放进调度任务时,还要与整任务 deadline 取更紧的一边。这不是保证主机绝不卡住,而是到预算边界后停止继续启动探针,把尚未采集的项明确留在结果里。

被预算砍掉的检查项以 unknown 出现在报告里,附带原因。报告全文落盘 ~/.ops-agent/reports/*.md(0600,落盘前统一脱敏,保留 100 份),终端只回显摘要,但异常清单无论多少条都完整列出。我不能接受报告里出现"以下省略 47 条异常"。

退出码设计成脚本可以直接判定:0 正常、2 有警告或异常、3 不完整、1 检查本身无法执行。cron 里一行就能分流。


3. 模型只看不判

检查架构定下来之后,模型在巡检里能干什么就很清楚了:

确定性探针(不依赖模型,先跑)
  → 规则结果产出,报告落盘;Agent/调度路径另存 signal
  → 模型解读(可选,--explain;失败不影响结论)

opsagent inspect --explain 会把规则报告喂给模型,让它写一段人话解读。但解读只是附在报告上的注释,规则异常不会被模型的"正常"结论覆盖。workflow 的汇总段也由程序生成——列出规则检查的全部异常与每步状态,模型不能改写;输出过长被截断时也会优先保留这一段。这是 W1 复审修掉的问题:早期版本最后一步模型总结会把前面发现的异常"总结没了"。

候选版前的补查让我再往前走了一步:如果探针跑完了,但 save_signal() 或完整报告写入失败,光在工具结果里提醒“保存失败”还不够,模型的最后一句仍可能是“正常”。现在 ToolContext.evidence_errors 留下采集/保存错误,schedule._run_entry() 在模型完成后由程序追加“不完整”标记;即使已有一个正常 signal,缺完整报告也不能被当成证据齐全。独立 CLI 的探针结论、调度执行是否成功、证据是否保存完整,是不同层的结果。

纯文本兜底也收紧了:结论:正常/异常 不再只截取前两个字;_detect_verdict() 要求独立结论整行匹配,并排除引用、缩进代码以及反引号/tilde 围栏里的旧结论。signal 负责保住确定性事实,文本解析不能再偷偷替“正常”放宽条件。

定时巡检那边,--checks all 声明任务级规则检查,每轮先跑探针再决定要不要调模型。模型只剩一件事可做:对着结构化结果说"这个 87% 的磁盘使用率,结合这台机器上周扩过容,我建议……"。建议可以错,事实不能丢。

我愿意保留这个分工:采集、比较、计数这些规则能从代码和回归里直接核对,模型负责把结构化事实组织成建议。固定探针不产生模型 token 费用,但仍会消耗主机资源和 SSH 请求,不是没有运行成本。


4. 每条结论都要能追到命令

巡检报告里每个检查项都带:目标、采样时间、观测值、阈值、状态、证据引用(探针命令与输出片段,脱敏后)。这是给"不信任"准备的——我看到一个 warn,第一反应是能立刻核对它是不是判错了,而不是只能接受。

unknown 项同样给证据命令,让人可以手动复跑。README 里写得很直白:unknown 只说明"没采到",不说明"坏了",需要人按报告里的证据命令复核。巡检不替代监控,探针只覆盖连通性、磁盘/内存/负载和 systemd/Docker。这条边界也写在 README 里,主要是防止我自己对它产生不切实际的依赖。


5. 阈值还是拍出来的

规则巡检落成代码后,哪些动作依赖模型就很清楚了。W7 加了调用预算之后这个分离更有用:预算耗尽时,确定性 inspect 和场景的规则探针仍能执行;需要模型的解读会失败并留下原因。不能把它写成“整个调度任务仍然成功”,任务运行记录仍要如实反映模型调用失败。

但阈值本身没解决。现在阈值是静态配置,磁盘默认 85%/95% 是起点,不是某种普适健康标准。不同用途的机器可能需要不同余量,我留出的办法是按目标在 targets.json 里分别配,本质上还是手工调参。候选版没有做基线学习或自动异常检测,也没有真实环境长期数据替这些默认值背书。

本版验证能明确说的是:309 项自检、223 项独立回归与四组发布 CI 通过,本地恢复演练 10/10。本版 W8 的真实 SSH/LLM/通知外部验收及连续 7 天观察仍未完成。我还没有理由把“测试里能分开判定”写成“长期使用已经稳定”。

下一篇聊巡检发现异常之后的事:通知。我除了担心漏报,也担心重复消息磨掉人的信任。fingerprint 怎么去重,哪些状态变化值得再发一条,我把这些条件接着展开。

这篇对应的代码


系列总览 · 目录 · 上一篇:给 AI 一个 bash 之前,我先想清楚了自己怕什么 · 下一篇:告警发多了,我就再也不会点开