凌晨两点多,手机告警把我震醒:核心订单服务P99延迟从200毫秒直接飙到两秒。打开监控平台,屏幕上红黄黄一片,链路追踪里全是超时,日志里各种报错混在一起。这时候运维排查才开始:看拓扑、查数据库指标、翻变更记录、找日志异常。人来查这套链路,通常要过二十分钟甚至更久,期间不断切屏、猜关系、对时间线。
这就是我想解决的问题——运维故障定位能不能像一名老SRE一样,先理解“系统里每个组件之间到底是什么关系”,再沿着关系链路有据有循地排查,而不是靠人肉记忆拓扑、拍脑袋猜根因。过去一年我陆续做了一套基于本体语义网络的运维故障定位Agent,把CMDB、监控、链路追踪、变更记录沉淀成一张带约束、可推理的语义网,让Agent在里面走查问题。整个过程跑下来,故障定位的平均时间从人工的二三十分钟压到了三到五分钟,而且定位结论会带一条完整的证据链。这篇文章就把这个方案的思路、建模方法、Agent架构和落地过程中的坑完整梳理一遍,适合运维、SRE、后端以及正在折腾Agent工程化的同学参考。
1. 本体语义网络:让机器理解故障究竟是怎么“串”起来的
1.1 为什么不用普通知识图谱,也不用向量库
在做这套方案之前,我也试过两种常见路子:一种是向量知识库,把故障案例、操作手册塞进去做语义检索;另一种是普通知识图谱,把CMDB的数据导成图,然后在图上做查询。两种都能跑,但离“定位根因”都差一层。
向量库适合“模糊召唤”和“经验联想”。你可以问它“数据库连接打满一般是什么原因”,它能根据已有案例召回几条经验,但回答往往是“可能原因包括A、B、C”,排不出先后,也没法解释当前这套环境里究竟是哪条链路先出了问题。它擅长的是让Agent“想起来”,而不是让Agent“算出来”。
普通知识图谱比向量库更进一步,它至少有实体和关系。比如订单服务依赖支付服务、支付服务运行在db-03主机上,这些边画出来以后可以用图查询找到关联节点。但普通知识图谱有个问题:它只有结构,没有约束和规则。图上可以存在“支付服务依赖订单服务”这种反向关系,可以存在同一类型节点属性不一致,也可以存在语义冲突——“依赖”和“被依赖”被当成两种毫无关联的关系。机器能查图,但不知道这几条边放在一起意味着什么。
本体的价值就在这里。本体(Ontology)本质上是一套对领域概念、关系、属性、规则的显式约定。普通知识图谱像一本通讯录,记录了谁和谁认识;本体语义网络则像一套带族谱规则的家族档案,不仅记录关系,还约定“父子”“配偶”“继承”这些关系必须遵守的逻辑。映射到运维场景就是:服务之间的依赖是有方向的,告警事件是挂在资源节点上的,延迟指标是某个服务实例的观测属性,变更操作可能触发多个节点的状态变化。这些约束定义清楚后,机器不仅会查图,还能做推理——比如发现“某条依赖链路存在环”,或者“A的P99异常,且A依赖B,而B有严重告警,那么根因更可能指向B”。
所以我的选择很明确:底图用图数据库存储,但图之上必须有一层明确的本体Schema和推理规则。这才是“语义网络”而不是“一张图”的差别。
1.2 三层本体建模:物理层、逻辑层、事件层
运维领域的本体怎么建?我的做法是分三层,千万别把所有东西揉在一个平面上。
第一层是物理资源层,包括主机、容器、物理机、网络设备、存储。这一层的核心关系是“包含”和“连接”:主机包含容器,物理机承载虚拟机,交换机连接若干主机。属性方面重点关注资源容量,比如CPU核数、内存大小、磁盘大小,这些数据后面计算故障影响范围时用得上。
第二层是逻辑组件层,包括应用服务、中间件、数据库、消息队列、网关、DNS等。这一层是整个语义网的核心,关系也最丰富。典型关系包括:服务之间的depends_on(依赖),服务与中间件的uses(使用),数据库实例之间的replica_of(主从),以及应用与运行环境的runs_on(运行在)。这一层用于还原一条请求从网关到服务再到数据库的完整路径。
第三层叫事件与观测层,不建模系统组件,而是建模“发生了什么”。告警事件、指标异常、变更操作、发布单、日志关键字命中等,都是这层的实体。事件节点通过affects指向物理或逻辑层节点,并带上时间戳、严重级别、状态等属性。这一层主要负责帮Agent还原时间线索,区分“谁先出问题,谁后受影响”。
这三层建模完成后,故障定位的底层数据结构就齐了:物理层告诉你资源在哪,逻辑层告诉依赖怎么走,事件层告诉现在出了什么事。三者合在一起,Agent才能回答“支付服务超时,是因为它依赖的数据库慢了,而数据库慢是因为所在的宿主机磁盘IO出了问题,磁盘IO问题在今天凌晨的变更后开始恶化”。
在图数据库里的落地,我用的Cypher建模型大致长这样(Neo4j的写法,实际项目我还在部分场景接入了JanusGraph,逻辑类似):
// 逻辑层节点 CREATE (svc:Service {name: 'payment-svc', type: 'microservice', owner: 'pay-team', version: 'v2.1.0'}); CREATE (db:Database {name: 'payment-db', type: 'mysql', role: 'primary'}); // 依赖关系 CREATE (svc)-[:DEPENDS_ON {weight: 0.9}]->(db); // 物理层与逻辑层关联 CREATE (db)-[:RUNS_ON]->(host:Host {name: 'db-03', ip: '10.20.1.3'}); // 事件层 CREATE (alert:Alert {name: 'HostDiskUsage', severity: 'critical', status: 'firing', startTime: datetime('2024-06-18T02:13:00')}); CREATE (alert)-[:AFFECTS]->(host);实际投入使用时,节点规模在几万到几十万之间,Neo4j完全扛得住,关键在于索引要建好,尤其是按节点名称和事件时间戳做索引。后续所有图检索都必须说明索引命中的条件,否则图数据库全表扫描会把整个定位延迟拖到几十秒,那Agent再聪明也白搭。
1.3 规则层:把“老师傅的经验”变成机器推理约束
本体只有Schema还不够,要让Agent在图上“推理”,还需要一条规则层。我的规则层不追求复杂的OWL DL本体推理,而是用可读性很强的DSL规则和一部分Cypher片段表达。这里我挑几个实际最有价值的规则。
规则一:故障优先传播自下游依赖。如果服务A存在延迟或错误类告警,且A依赖B,那么把B的根因概率提升至A之前。这条规则用自然语言叙述,翻译成代码就是一次带方向的图遍历,优先找下游邻居的异常状态。
规则二:物理层故障优先级高于逻辑层。如果数据库P99升高,同时所在宿主机有磁盘告警或负载告警,优先把宿主机资源问题列为第一假设。因为逻辑层故障很多时候只是物理层故障的表现。这条规则体现了“先资源后应用”的排障习惯。
规则三:变更事件是强先验。如果某节点在故障窗口前30分钟到前5分钟内存在发布、重启、扩缩容等变更事件,则该节点成为根因候选的概率上调。这个规则在传统告警里几乎没法表达,但规则层可以做得很直接,因为事件层已经接入了变更记录。
规则层落到工程上,就是一组打分函数。Agent每次从图上拿到候选节点集后,会根据这些规则给每个假设打分,不同规则打分数值叠加,最终形成假设排序。这套做法比让LLM直接“拍脑袋”给结论稳定得多,因为规则层的输出是可复现的、可解释的,Agent要做的更多是围绕排序靠前的假设去收集证据验证,而不是从头生成可能原因。
2. Agent如何在这张语义网上“走查”问题
2.1 定位型Agent的五个工作阶段:解析、扩散、假设、验证、收敛
有了语义网络这张底图,接下来就是设计Agent的工作流。我给这个Agent定义的工作循环,一共五步,参考了故障排查手册里的思考路径,也参考了吴恩达那套Agent教程里讲的规划-工具-反思闭环。
第一步是解析。原始的告警消息往往是一行文本,比如“订单服务P99异常,当前2.1s,基线300ms”。Agent要把这条文本映射为图上的具体节点和观测属性,通过LLM做实体抽取再和图库里的节点名做模糊匹配。这一步出错会导致后面全错,所以一定要拿着抽取出的实体去图库里校验,匹配不到就退回人工确认。
第二步是扩散。从命中节点出发,按DEPENDS_ON、RUNS_ON、AFFECTS等关系做1到2跳的邻域扩展,拿到一张子图。这一步的目的是划定排查范围,让Agent后续的思考始终在一个可控的小图里进行。我一般控制在50个节点以内,太多会让推理质量明显下降。
第三步是假设生成。Agent结合子图上的告警事件、变更事件、指标状态,按1.3节说的规则给候选节点排序,生成Top 3的根因假设。每个假设都必须指向一个具体的节点,绑定至少一个可验证的观测指标或事件,杜绝“可能是某个服务出了问题”这种空话。
第四步是验证。针对每个假设,Agent调用外部工具去取数据。工具包括PromQL查指标、ES查日志、APM查链路追踪、发布平台查变更记录。所有工具返回结果必须带时间窗口,Agent要把数据和自己生成的假设放到一对比对。如果支付服务慢、数据库负载高、且变更平台显示数据库节点刚做过配置变更,那么“数据库节点变更导致性能下降”这个假设就算验证通过了。
第五步是收敛。Agent把验证结果整理成一份故障报告:根因节点、证据链、影响范围、建议动作、置信度。如果置信度低于阈值,Agent不会硬给结论,而是会输出“当前证据不足,需要人工介入,建议优先查看xxx”。
这五步是一个循环,第三步假设不通过会在第四步触发重新假设,最多循环两轮。设计上刻意限制循环次数,是为了防止Agent在图上无限发散,把简单的故障排查变成一场“推理马拉松”。
2.2 图检索与路径推理的工程实现
Agent的扩散和假设生成,本质上是图算法和提示词工程的结合。图算法负责把搜索范围收紧,提示词工程负责把图数据翻译成LLM能理解的证据链。
扩散这一步,不能直接让LLM去“想象”服务依赖什么,而要让代码先去图库精确查询,再把查询结果结构化成上下文交给LLM。我用的是Cypher查询,常写的一条大概是这样:
MATCH (start:Service {name: $serviceName}) OPTIONAL MATCH (start)-[dep:DEPENDS_ON]->(downstream) WHERE dep.weight >= 0.5 WITH start, collect(distinct downstream) AS dependencies OPTIONAL MATCH (start)-[:RUNS_ON]->(host:Host) OPTIONAL MATCH (host)<-[:AFFECTS]-(alert:Alert) WHERE alert.status = 'firing' AND alert.startTime >= $windowStart RETURN start {.name, .type}, [n IN dependencies | {name: n.name, type: n.type}] AS dependencies, host {.name, .ip}, collect(distinct alert {.name, .severity}) AS activeAlerts这条查询返回的就是Agent推理所需的“最小上下文”:当前服务依赖哪些关键节点,运行在哪台机器上,这台机器上正在报哪些严重告警。为什么要把查询结果固定成这么结构化?因为LLM对自由文本的利用率远低于结构化数据。把子图直接塞一大段JSON给模型,它会挑花眼,反而容易淹没关键信息。我后来把所有图查询接口都定义成统一的“实体-关系-事件”三元组格式,LLM的推理准确率明显上了一个台阶。
路径推理方面,我会用BFS但加约束:只走DEPENDS_ON、RUNS_ON、AFFECTS三类边,限制最大深度为3,每条边上还有权重,权重低于某个值直接剪掉。这个权重来自SPTA和接口调用量统计,耦合越紧密权重越高。设置权重和剪枝是为了防止Agent把两个只是偶尔调用的边缘服务也当成核心依赖,那种弱关系一旦被当成根因,定位就会偏得离谱。
2.3 从“会想”到“会查”:工具调用的可信闭环
生成假设只是推理的一半,另一半是让Agent具备“会查”的能力。这个Agent的平台里注册了四个核心工具:
- 指标查询工具:封装Prometheus的PromQL,输入是指标名、标签、时间窗口,输出是时序数据的摘要,包括均值、峰值、变化趋势和异常区间。
- 日志检索工具:封装Elasticsearch的查询接口,输入是索引名、关键词、时间窗口,输出是命中日志片段和关键字统计。
- 链路追踪工具:查询某个服务在特定时间段的Span信息,输出上下游调用耗时分布和错误码聚合。
- 变更查询工具:查询节点在窗口内是否有发布单、配置变更操作,输出变更类型、操作人和时间。
这些工具返回的结果,我强制要求带上“证据元信息”:时间戳、数据来源、查询条件。Agent在生成报告时必须逐条引用这些元信息,比如“根据PromQL查询,db-03主机磁盘使用率从6月18日02:10开始持续接近95%,数据来源:Prometheus,查询时间窗口02:00-02:20”。这一步强制措施基本杜绝了Agent瞎编数据的可能性。
这里还要提一下Agent的记忆设计。当前会话的短期记忆很简单,就是已经访问过的节点列表、已查过的工具结果、已验证过的假设集合,防止Agent在同一批数据上反复打转。长期记忆则是一张“故障案例表”,每次定位完成后把根因、证据链、图路径压缩成一条案例向量存入向量库。下次遇到相似告警时,Agent会先检索历史案例,把历史结论作为先验之一带入假设排序。这一设计参考了Agent记忆框架里短期、长期分层的思想,效果好于单靠LLM的上下文。
3. 实操复盘:一次订单超时故障的完整定位过程
3.1 故障场景与数据准备
为了说清楚这套系统怎么工作,我复盘一次最近实际发生的故障。场景是一家电商平台的订单服务整体超时,监控告警文本是:“订单服务P99延迟2.1s,历史基线300ms,告警级别P1,事件时间2024-06-18 02:15。”这个时间点正好在夜间,值班同学还没介入,Agent先收到事件并启动了定位流程。
在初始化这套系统的时候,我得先把数据铺好。CMDB导出的节点和关系导入图库,我单独跑了个脚本把Prometheus的告警事件同步为事件层节点,APM里面的服务依赖和调用量则用来初始化DEPENDS_ON边上的权重。知识准备阶段最耗时的是关系清洗:CMDB里的依赖关系经常是反的,比如把“被调用方”写成了“依赖方”,我写了一套校验脚本,按实际调用链的流量方向纠正边方向。这一步是纯粹的数据脏活,但跳过去后面全得还债。
3.2 Agent执行定位的每一步细节
Agent在02:15收到告警后,先是解析文本,把“订单服务”映射到图库里的order-svc节点。模糊匹配时它同时匹配到了order-svc和order-db,但根据告警消息里的“P99延迟”属性,它确定主体是服务而非数据库,这一步用了属性加权匹配,避免误挂节点。
扩散阶段,Agent从order-svc出发,按DEPENDS_ON向下游查。子图里出现了四个依赖:user-svc、inventory-svc、payment-svc、coupon-svc,其中payment-svc的DEPENDS_ON权重最高。继续向下扩散,payment-svc依赖payment-db,payment-db主库运行在db-03主机上,同时发现db-03正在报警:磁盘使用率95%,severity为critical。图扩散到这里,Agent就拿到了一条很关键的链路:order-svc -> payment-svc -> payment-db -> db-03(磁盘告警)。
假设生成阶段,规则引擎打分的输出让我印象深刻。规则一让下游优先级前移,于是支付链路排在用户、库存之前;规则二把物理层优先级抬升,于是db-03的磁盘问题压过了payment-db自身的性能问题;规则三检查了变更记录,发现db-03在当天凌晨01:58有一条配置变更,变更类型是“磁盘扩容调整”,操作状态为已完成。三个规则叠加之后,排在第一位的是“db-03磁盘空间不足、IO异常导致payment-db性能劣化,进而拖垮payment-svc连接池,最终引发order-svc超时”。
验证阶段Agent依次调用了指标查询工具和日志检索工具。PromQL查到db-03磁盘使用率在02:10后一路走高,磁盘读IO等待时间从0.5%涨到18%,数据完全支持“磁盘异常”假设。日志检索在payment-svc的日志里搜到了大量“connection pool exhausted”报错,错误集中发生在02:12到02:15,和数据库变慢的时间线吻合。链路追踪结果也显示,order-svc调用payment-svc的耗时中位数为1.7s,而payment-svc调用payment-db的耗时中位数为1.2s,慢在下游数据库。
到这里,Agent收敛了。它给出的报告结论是:根因节点为db-03,根因类型为磁盘空间耗尽导致的IO性能劣化,影响范围覆盖payment-db、payment-svc、order-svc三级链路;置信度87%。建议动作包括清理db-03磁盘、联系存储团队检查扩容是否生效,并提示凌晨的扩容操作疑似未正确刷新分区表导致容量未实际扩展。后来值班同学按这个线索一查,确实是扩容操作后文件系统没有重新读取分区表,新容量没生效,磁盘空间照旧告急。
3.3 Agent定位和人工排查的对比结果
这次故障如果走纯人工排查,按照以往经验大概需要多久?我的评估是至少25分钟,因为要跨监控平台查三个系统的指标,还要去发布平台翻变更记录,再把日志串起来。Agent实际用时3分42秒,而且报告结论直接指到了真正的根因节点。
我把Agent和传统告警做了个对比:
| 维度 | 传统告警定位 | 本方案Agent |
|---|---|---|
| 依赖关系获取 | 靠人脑记忆或静态拓扑图 | 从本体语义网络实时检索 |
| 证据搜集 | 人工切换多个平台 | Agent自动调用指标、日志、链路、变更工具 |
| 根因排序 | 凭经验拍优先级 | 规则引擎打分+LLM假设排序 |
| 结论可解释性 | 口头描述,难以回溯 | 输出完整证据链和置信度 |
| 平均定位耗时 | 20-30分钟 | 3-5分钟 |
这个对比不是想说明Agent取代SRE,而是把SRE从重复的“查这查那”里解放出来,让人把精力放在Agent给不出结论的复杂故障上。事实上这次故障里的“扩容未生效”这个深层判断,就是人看了Agent的证据链后迅速意识到的,Agent在证据组织上有价值,人则在行业知识和最终决策上有价值。
4. 落地过程的硬骨头与排查心得
4.1 数据冷启动:先建图,再谈本体
第一道坎是数据从哪来。很多团队连CMDB都没打理干净,更别提标准化关系。我的建议是:不要追求一步到位的完美本体,先用现有数据把图撑起来,再逐步加约束和规则。
最开始,我甚至直接用APM的服务依赖关系自动建图,边类型全部统一叫DEPENDS_ON,属性只保留调用量和依赖方向;主机关系从一套云资源管理平台的API拉取,告警从监控平台批量同步。Schema很简陋,但不影响跑通流程。跑了两个月之后,才逐步把节点细分成Service、Database、Queue、Host等不同类型,给边加上不同权重,并补齐RUNS_ON等关系。本体约束是在使用中慢慢长出来的,这一点和“先做出来再优化”的工程理念一致。
冷启动阶段另一个经验是,导入图库前一定要做关系方向校验。CMDB里十有八九存在方向错误,我写过一个校验脚本,用APM的调用链数据做真值来源:如果A在调用链上调用B,那么A -> B才是DEPENDS_ON,否则可能是反向。这道校验做完,Agent命中率提高非常明显。
4.2 Agent“想太多”和“瞎发散”的约束手段
Agent在图上跑起来后,最常见的毛病是“想太多”。给它一张50个节点的子图,它能把每个节点的各种可能性都列一遍,最后列出一堆看着都合理的假设,等于没给结论。我试过几种约束方法,最后留下两招特别管用。
第一招是强制假设绑定证据。生成假设时,模板要求每个假设必须包含节点名+异常类型+支持该假设的具体观测项。比如“payment-db主库慢查询(支持项:P99耗时1.2s,慢查询数增长5倍)”。如果一个假设写不出支持项,直接判为无效假设,不允许出现在排序里。这招过滤掉了大量“查不出证据的空想”。
第二招是限制每轮假设数Top 3,且必须按规则引擎分数排序,LLM不能自由乱排。规则引擎的分数是基于可复现的图数据和规则算出来的,LLM只是在分数基础上做语义层面的微调和组合。把“决策权”给规则引擎,把“解释和表达权”给LLM,这算是我在这类Agent上悟出的比较核心的一点。LLM负责把技术判断翻译成自然语言报告,而不是负责拍板根因,准确率会稳很多。
4.3 多Agent协同和本体演进的坑
故障定位范围一大,单Agent也会手忙脚乱。订单服务超时这种跨应用、跨数据库、跨主机的故障,靠一个Agent调五六个工具来回切换,上下文很容易被冲淡。后来我把它拆成了三个子Agent:应用Agent(查日志、查链路、查服务状态)、数据库Agent(查慢查询、查主从延迟、查锁等待)、基础设施Agent(查磁盘、查CPU、查网络)。三个Agent各自跑各自的领域,各自维护自己的短期记忆,最后汇总到一个主Agent做信息融合和结论收敛。
这种多Agent结构有两个好处:每个Agent的工具列表短,工具调用的选择更精准;各自的上下文窗口不用承载无关领域的噪声。代价是通信成本高了一点,父Agent需要从三个子Agent的报告里再做一轮证据对齐。我的经验是,不是所有场景都值得拆多Agent,只有当子领域的数据源之间确实差异巨大、一个Agent忙不过来时再拆,否则纯粹是给自己增加编排复杂度。
本体演进的坑则主要是变更同步跟不上系统的变化。服务上线、下线、改名、扩缩容,都会让图上的节点和关系过期。如果一张图的准确率跌到80%以下,Agent再聪明也会走错路。我最终的做法是上一套事件驱动同步机制:发布平台有任何上线完成事件,图同步脚本立刻触发,把涉及的服务节点、RNS ON关系做增量更新;每周末跑一次全量对比,找出CMDB和图库之间的差异。这套应对机制让图的准确率常年保持在95%以上,这其实才是Agent能稳定工作的根本前提。
一点真实体会
整套方案从无到有,我最大的体会是:Agent的能力边界不是模型推理,而是它脚下那张图准不准、规则层硬不硬。把“老师傅脑子里的依赖关系和排查经验”显式地落到本体语义网络上,Agent才能真正像老师傅一样按图索骥。如果你也想做类似的事情,我建议先挑一类高频故障,把对应的依赖链路和规则建出来,跑通一个端到端闭环,再谈扩展。把底图和规则做扎实,比把模型换得更强划算得多。