
作者:Guangyi Zhao
日期:2026-10-14
主题:AI-Node-Sentinel(作者主导的开源项目,github.com/Ezra-Zhao/AI-Node-Sentinel),脚手架版本 v0.1,Python 3.12.3
环境:Linux x86_64 虚拟机,仓库代码未经修改直接运行;LLM(大语言模型)实验使用本地假 Provider 实现项目自有协议,未调用真实模型 API(应用程序接口)
最近折腾了 AI-Node-Sentinel 的故障注入实验,踩了几个坑,这篇把问题说清楚:真正容易出问题的不是模型够不够聪明,而是接入运维系统时的工程边界。
在 GPU(图形处理器)集群里,硬件开始出问题时,最先出现的信号通常只是内核日志中的一行:Xid。XID 是 NVIDIA 为 GPU 硬件和固件错误设计的编号体系——GPU 从 PCIe 总线上掉线、不可纠正的双位 ECC 错误、NVLink 故障、GSP 固件超时,每一种情况都有独立代码。
值班流程通常很机械:从上千块 GPU 里捞出 dmesg,逐个查询代码含义,再决定是隔离节点、重置 GPU、重启机器,还是发起 RMA(退货维修授权)。决策慢一点,训练任务就会继续在故障硬件上重试,消耗真金白银买的 GPU 小时。
最直接的想法自然是把这活儿交给 LLM Agent:让模型读取遥测数据、查询错误代码,再给出处置建议。智能体不就是干这个的吗?
我自己的开源项目 AI-Node-Sentinel,就是围绕这个思路搭起来的脚手架:接收遥测数据,提取 XID 事件,通过查表补充信息,执行分诊规则,最后输出结构化报告,同时预留一个基于 LangChain 的 LLM 推理接入位。先把真实状态说清楚,因为后面的结论都建立在这个前提上:项目目前是 scaffold v0.1,README 里也明确写了这一点。
分诊决策树目前只是有意保留的桩实现——所有事件都会得到 NEEDS_HUMAN_REVIEW,置信度为 0.0。LangChain Provider 同样只是桩实现,构造时会抛出 NotImplementedError。所有遥测数据都由项目内置模拟器生成,目前真正跑通的只有三步:数据接入、代码查询和报告渲染。
所以,这不是一个生产级诊断系统实践。相反,我把这套脚手架拆开检查了一遍,跑了六组故障注入实验,目的就是测清楚:等 LLM 真正接进来以后,这条流水线会在哪些地方断掉。
这些结果也适用于其他把 LLM 接到运维系统的人,因为六个断点都不是模型能力问题,而是接线问题:输出契约对不上、证据喂得太薄、一条坏记录拖垮整批数据、Prompt 路径绑定当前工作目录、单事件无状态导致近视,以及流水线自己把旁证丢掉。换一个更强的模型,这些问题一个都解决不了。
代码库不大:8 个核心 Python 文件,包含测试和 Demo 一共约 470 行代码,整体分为四部分。
agent/schemas.py 定义数据类型,包括 TelemetryEvent(带来源、类型和载荷的标准化事件)、XidFinding(经过查表补充的 XID 事件)以及 TriageVerdict(处置动作、置信度和理由)。
tools/xid_lookup.py 是一张静态表,把错误代码映射到名称、类别和严重级别,数据只来自 NVIDIA 公开的 XID 文档和 Google Cloud GPU 故障排查指南。查不到时返回 None,绝不猜测。
tools/triage_rules.py 保存决策树,目前也只是桩实现。agent/orchestrator.py 则定义 TriageAgent,负责接入数据、提取 XID 事件、逐条分诊并渲染报告。
LLM 接入点位于 agent/llm.py:这是一个最小协议 LLMProvider,只要求实现一个方法,reason(system_prompt, evidence) -> str。
Orchestrator 会先计算确定性结论;如果存在 Provider,它就拼接证据字符串、调用 Provider,再把返回文本以 [LLM] 前缀追加到结论的 rationale 后面。
prompts/triage_system.md 里的系统 Prompt 草案要求模型只能引用给定证据,遇到未知代码必须明确说明,必须返回一个动作和介于 0 到 1 之间的置信度,置信度低于 0.7 时强制给出 NEEDS_HUMAN_REVIEW,并使用 JSON(JavaScript 对象表示法)回答。
方法是在不修改仓库代码的前提下跑五组实验,实验脚本和原始 JSON 数据与本文一并归档。
xid 键、xid 非数字以及 xid=None 的事件混入正常批次。模拟器会为每块 GPU 生成一条温度基线事件和一条 ECC 基线事件,然后在大约 35% 的 GPU 上注入一到两条 XID 事件:
| 规模 | 事件数 | XID 事件数 | 结论数 | 未知结论数 | 严重级别(critical/high/unknown) | 流水线耗时 |
|---|---|---|---|---|---|---|
| 4 节点 × 8 GPU(seed 42) | 76 | 12 | 12 | 1 | 10 / 1 / 1 | ~0.1 ms |
| 16 节点 × 8 GPU(seed 42) | 313 | 57 | 57 | 5 | 39 / 13 / 5 | ~0.2 ms |
| 64 节点 × 8 GPU(seed 42) | 1,285 | 261 | 261 | 34 | 157 / 70 / 34 | ~0.7 ms |
| 64 节点 × 8 GPU(seed 7) | 1,295 | 271 | 271 | 35 | 177 / 59 / 35 | ~0.7 ms |
| 64 节点 × 8 GPU(seed 123) | 1,282 | 258 | 258 | 28 | 167 / 63 / 28 | ~0.7 ms |
这里有三点观察。
第一,所有结论都是置信度 0.0 的 NEEDS_HUMAN_REVIEW。这是桩实现的行为,不是模型智能水平的测量结果,因此把它作为后续实验的对照组。
第二,大约 1,300 条事件在 1 毫秒内处理完毕。按单次运行的 perf_counter 原始计时计算,吞吐约为每秒 177 万条事件;表格中的耗时四舍五入到了 0.1 ms,亚毫秒数字只反映数量级,不是精确基准测试。速度快的直接原因是查表只是一次字典读取。以本文的规模来看,确定性部分几乎免费。流式接入和窗口状态需要后续单独测量,但未来大部分延迟和成本显然会来自 LLM 环节。
第三,别把 11% 到 13% 的未知结论占比误读成真实故障率。模拟器的注入池只有 8 个代码,其中一个是故意设置的未知代码 999,因此未知比例按设计就是八分之一,属于注入池结构造成的假象,并不是真实 GPU 集群中的未知代码现场数据。
直接调用时,仓库内置的 6 个测试全部通过,覆盖确定性、已知代码查询、未知代码不做猜测、桩实现转人工审核以及报告结构。当前机器没有安装 pytest,所以测试函数是直接执行的,并非通过测试运行器执行,这里如实说明。
查表里只有 7 个代码:13、31、48、74、79、119、120。
额外探测的 10 个表外代码——43、45、56、63、64、68、92、94、109、154——全部返回 None,处理方式与 999 完全相同:严重级别为 unknown,known=False,并转交人工审核。
路由行为没有问题。拒绝猜测,是这条流水线最重要的安全属性。问题在覆盖率:任何不在表内的 XID,最终只能给值班人员一条信息——“出现了未知错误,自己查”。
这张表是根据公开文档手工整理的,扩充它并不炫技,却是实打实的苦活。4.3 节会说明,即便如此,它也不该成为第一个修复项。
Orchestrator 通过 int(event.payload["xid"]) 提取错误代码,没有校验,也没有逐事件隔离。把异常事件混入正常批次后,结果如下:
| 注入方式 | 结果 |
|---|---|
| 一条格式正确的事件 | 正常完成,生成 1 条结论 |
| 正常事件 + 缺少 xid 键的事件 + 正常事件 | 整次运行抛出 KeyError;已经完成的结论也全部丢失 |
| 正常事件 + xid 为非数字字符串的事件 | 整次运行抛出 ValueError |
| 正常事件 + xid=None | 整次运行抛出 TypeError |
run() 使用列表推导式处理事件,因此第一个异常会丢掉整个窗口里的所有结论。
换成生产环境,这相当于 dmesg 解析器产出了一条字段不完整的事件。最终结果不是“某条事件无法诊断”,而是“整个窗口的结论全部消失”,并且以进程级异常结束,而不是在报告中留下错误记录。
对于这种流水线,逐事件隔离和坏记录计数器属于基本配置,而当前实现里完全没有。
这组实验正是本文存在的理由。
我实现了一个完全遵守系统 Prompt 的假 Provider。针对 XID 79,它返回建议执行 DRAIN_NODE、置信度为 0.95 的 JSON。接入 Orchestrator 后,结论的真实状态如下:
| 字段 | 假 Provider 输出 | 最终结论 |
|---|---|---|
| action | DRAIN_NODE | NEEDS_HUMAN_REVIEW(未改变) |
| confidence | 0.95 | 0.0(未改变) |
| rationale | 完整 JSON | 原样追加为文本,并添加 [LLM] 前缀 |
代码已经把原因写得很清楚:协议返回字符串,Orchestrator 只负责拼接,全程没有代码解析 JSON,也没有代码覆盖确定性结论。
Prompt 中约定的输出格式和置信度门槛,实际上只是与模型之间的一份君子协定,程序一侧完全不执行。即使明天就实现真正的 LangChain Provider,而且模型每次都完美遵循格式,最终分诊动作仍然 100% 由桩实现决定。LLM 对系统的全部影响,只是往 rationale 里粘贴一段文字。
实验的另一半看证据厚度。Orchestrator 实际拼出的证据字符串只有:
node=node-00 gpu=0 xid=79 name=GPU has fallen off the bus severity=critical ts=. 就这些。
查表结果里明明还有 category 和 notes 字段,其中包含处置提示和来源说明,但它们都没有进入证据。同一块 GPU 的温度和 ECC 读数也进不来,因为证据是在单事件分诊函数内部拼装的,而这个函数的签名里根本没有窗口上下文。
Prompt 的第一条规则要求模型只能引用给定证据。流水线先把证据削减成错误名称的复述,再禁止模型越过证据边界。如果模型仍然给出有信息量的答案,它只能靠违反规则实现。这个矛盾来自接线决策,不是模型缺陷。
第三点是故障传播。把 Provider 换成始终抛出 RuntimeError 的实现,triage_event 会让异常直接向上抛出,整次运行随即终止,和 3.3 节的死法完全一样。
Provider 是外部依赖,超时、限流和网络抖动都很正常。可选的推理环节应该降级到确定性结论,而不是把整批数据一起拖下水。当前实现没有任何降级路径。
Orchestrator 使用相对路径加载 Prompt:open("prompts/triage_system.md")。
从仓库根目录运行没有问题;换成从 /tmp 启动,同样的代码会立即抛出 FileNotFoundError。LangChain Provider 在构造时抛出 NotImplementedError 属于诚实的桩实现,不算缺陷。
这类相对路径 Bug 最擅长在上线时咬人:由 systemd、容器或调度器启动的服务,很少会把仓库根目录设为当前工作目录;但开发者的终端通常会。因此,这个问题在开发机上几乎不会复现。
重复故障不会升级。
同一块 GPU 在 3 小时内出现 3 次 XID 79,最终得到的 3 条结论彼此毫不知情:动作相同、置信度相同、理由也相同。
如果把因果关系说精确:这些结论相同,直接原因就是决策树仍是桩实现,今天换成任何架构都会得到同样结果。但架构问题在下一层。
桩实现自己的注释已经勾勒出预期规则——同一块 GPU 在 24 小时内出现两次 XID 79,就应当隔离节点并重启——但 Orchestrator 每次只处理一个事件,而且完全无状态。即使将来把规则写出来,流水线也没有地方保存和聚合所需状态。
所以这不只是缺少一条规则,而是缺少状态层。
旁证则被流水线自己过滤掉了。
我手工构造了一个包含 3 条事件的窗口:同一块 GPU 先报告 ECC 双位错误计数为 17,随后温度达到 104 °C,最后出现 XID 13。
Prompt 的第二条规则说,XID 13、31 这类应用层故障,在没有旁证时不应建议隔离节点。这本身就意味着,出现旁证时应该改变判断。
实际结果是:输入 3 条事件,只输出 1 条结论。两条非 XID 事件在提取阶段被丢弃,既不生成结论,也没有作为上下文附加到 XID 13 的结论上。
在基线规模下,64 节点窗口里的 1,285 条事件,有 1,024 条属于这种情况。Pipeline 在 Prompt 里要求模型进行旁证感知推理,却又在数据路径里把旁证删掉了。
这套脚手架最值得肯定的判断,是它的分层方式:查表和规则放在确定性核心里,LLM 作为下游推理补充;在决策树尚未实现之前,所有事件都交给人工,置信度诚实地保持为 0。
原因很简单:隔离节点、重置 GPU、重启机器和发起 RMA 都是会作用在生产硬件上的真实操作。凡是表格或规则能够确定的事情,都不应该在凌晨三点交给概率模型临场发挥。LLM 的价值是信息归纳和解释,不是最终处置权。
3.4 节还带来了一个反直觉的推论:当前损坏的输出契约,意外成了一层安全网。
因为模型返回的 JSON 从未被解析进结论,今天即使模型产生幻觉,也只能污染 rationale,不能隔离节点。但这是意外,不是设计。
正确的修复方式不是保留这个断裂,而是增加解析和校验:动作必须落在实际站点 Runbook 的动作词表内;置信度低于 0.7 时,必须由代码强制改写为人工审核;解析失败时,必须降级为只输出确定性结论。
Prompt 已经写明置信度门槛,现在要做的只是把它从文字规则搬进程序。只接上 JSON 解析、不做校验的人,等于亲手拆掉了这层安全网。
这六种故障模式,没有一种需要更好的模型或更好的 Prompt。它们全是普通工程问题,我会按下面的顺序处理:
category 和 notes,并向分诊函数传入单 GPU 窗口摘要,包括近期同类代码计数、温度极值和 ECC 极值,让 Prompt 中“只能引用给定证据”的规则真正可执行。扩充查表刻意没有放进这份清单。
这是数据字典工作,不是故障模式。7 个代码的覆盖确实太少,查表应该继续扩充,但必须放在这 6 项之后。否则,即使查表扩充到 70 个代码,只要遇到第一条异常事件,整批数据照样会一起完蛋。
这套脚手架里,我最愿意为哪些设计选择辩护?恰恰是它拒绝做的事情。
未知代码不会被赋予编造出来的含义;尚未完成的决策树不会发出处置动作;模拟数据从生成器到报告头都被标记为 SIMULATED;桩实现直接抛出 NotImplementedError,而不是返回一段看似合理的虚假理由。
这些拒绝让每一行输出都可以审计:你能判断它来自查表、规则,还是模型。
而当 LLM 被直接挂到运维系统上时,恰恰是这种可审计性最先消失。模型流畅的叙述一旦覆盖确定性核心的真实状态,值班工程师就再也分不清哪些内容来自查表,哪些是模型编出来的,报告也就失去了被信任的资格。
六种故障模式最终都指向同一件事:模型的权限边界、证据边界和失败行为,都必须通过代码明确表达,不能只靠 Prompt 暗示。
这些局限都很关键。
第一,所有遥测数据都由仓库内置模拟器生成,其分布取决于注入概率和注入池。因此,本文中的所有比例只能验证流水线行为,无法说明真实集群中的故障分布。
第二,LLM 实验使用本地假 Provider,只验证接线契约和故障传播,没有调用任何真实模型。因此,本文不对任何模型的推理质量下结论。真实模型拿到目前这么薄的证据后,究竟多大概率会越过证据边界编造内容,需要真正接入模型后单独测量。
第三,当前实验的规模上限是单台虚拟机上的 64 个节点、约 1,300 条事件。并发接入、持久化和告警通道都还没有测试。
第四,查表覆盖率只统计了表内规模和表外探针,没有把表与 NVIDIA 完整公开目录逐项对比,因此本文不声称任何真实覆盖率分母。
目前还有三个开放问题。
状态层应该按什么粒度聚合——单 GPU、单节点,还是单个 NVLink 域——会直接影响升级规则和误报成本,而校准这些规则需要真实故障序列。
Prompt 草案里的 0.7 置信度门槛是拍脑袋选出来的,并没有经过校准。接入真实模型后,应该回放历史工单来确定阈值。
一旦旁证进入分诊流程,基线温度和 ECC 数据流的规模大约是 XID 事件量的 4 倍。等真实模型接入后,还需要测量证据组装在 Token 消耗和上下文长度上的成本。
agent/orchestrator.py(编排与证据组装)、agent/llm.py(LLMProvider 协议与 LangChain 桩实现)、agent/schemas.py(数据类型)、tools/xid_lookup.py(查表)、tools/triage_rules.py(决策树桩实现)、prompts/triage_system.md(系统 Prompt 草案)和 telemetry/simulator.py(模拟器)。data/2026-10-14-sentinel-bench.py 和 data/2026-10-14-sentinel-bench.json,与本文一并归档;重新运行脚本即可复核每一个数字。tools/xid_lookup.py 的文件头也引用了它。作者:Guangyi Zhao。本文涉及的代码、实验和文字均由作者独立完成。实验对象是作者维护的开源脚手架项目,所有遥测数据均来自该项目的模拟器,全部实验均在作者自己的本地环境运行,不涉及任何客户信息、客户系统或第三方非公开数据。
文中的所有数字均来自本文开头列出的测量结果;正文和第 5 节已经明确说明数据的模拟属性及结论边界,不会把脚手架的当前状态包装成生产系统。