凌晨两点十七分,告警声把我从椅子上拽起来的时候,我第一反应是看一眼监控大屏上的曲线——搜索引擎服务的可用性正在以肉眼可见的速度往下掉,从 99.8% 一路栽到 94%,错误率从 0.2% 飙升到 17%,P99 延迟从 180ms 涨到了 2.3s。这个增速和斜率,不是偶发抖动,是那种不马上处理就会演变成 P0 事故的持续恶化。这次排障和以往不太一样的地方在于,坐在“前端”带着我梳理问题的,不是一摞日志文件和一个疲惫的大脑,而是两个 AI 智能体。一个负责埋进日志里重建时间线、找异常调用链,一个负责做对照实验、验证每一个可疑点。整个过程有透明的决策记录,每一步都有证据和时间戳,最后真把根因挖了出来——连接池参数回退,服务在二十分钟内恢复正常。这篇博客就是那次排障的完整实录,包括任务拆解、Agent 的角色设计、具体的判断过程、复现实验和后续的坑,适合对 AI Agent 落地感兴趣、同时也经历过凌晨排障的运维和研发同学。
1. 故障现场:一个搜索引擎被“卡脖子”的全过程
1.1 十分钟内的指标恶化:告警单看不出的东西
先说监控数据,这是所有后续排查的出发点。故障从出现到全面爆发大约持续了十分钟,期间四个核心指标都在同步恶化:
- 可用性从 99.8% 降至 94%
- 调用错误率从 0.2% 拉升至 17%
- P99 延迟从 180ms 恶化到 2300ms
- 活跃连接数触及集群配额上限,且持续打满
这三组数字合在一起,其实已经能说明一些事情了。如果只是网络抖动或者某个机房光缆出问题,延迟会涨但错误率通常不会这么整齐地上去;如果是存储层慢查询,错误率会在服务端表现为超时,但连接数打满这个特征没那么明显。真正让我警觉的是“活跃连接数打满”这个监控点——它指向的是服务进程内部资源耗尽,而不是外部依赖变慢。
当时的时间背景也很要紧:白天刚做了一次配置变更,流量在晚上逐步回切,凌晨正好处于流量高峰后的爬坡期。故障发生在回切完成后的第三分钟,这个时间点恰好卡在连接池预热和新流量同时到达的窗口上。拿着这个信息再去翻告警单,可以看到告警是从一小批节点开始扩散的,不是全网齐刷刷地挂掉——这说明有一些重放与回归的东西在局部被放大,而不是全局性故障。
1.2 为什么这次没有先扎进日志堆,而是把排障权交出去一半
按我过去的习惯,这种故障第一反应肯定是打开日志平台,搜 5xx 错误码,然后顺着调用链往下追。但这次我克制住了。原因很简单,也很有现实考量:第一,日志量太大了,单节点一小时产生 4GB 多日志,人工扫一遍等于大海捞针;第二,链路太长,从网关到检索、排序、缓存、底层存储,每一层都有自己的日志和可能的瓶颈,我即便想从头到尾过一遍,至少得花半小时;第三,跨团队认知偏差——值班的同学觉得是存储慢,存储的同学觉得是网关重试,各有各的假设。
所以当时我做了个判断:先把一部分排查权交给两个 AI 智能体。我给它们的定位很明确——Agent A 去当“日志矿工”,把全量日志吃进去,做特征提取和时间线重建;Agent B 去当“验证者”,拿着 Agent A 的怀疑对象,在系统里做配置对比、压测复现、依赖健康检查。这不是撒手不管,而是把“从海量信息里缩小范围”这个最耗时的步骤外包出去,我保留最终裁决权。
这里要说明白:不是所有故障都适合这套流程。适合的前提有三个——有相对完整的日志、链路可观测、存在可对照的正常样本。这三个条件决定了智能体能不能拿到足够的上下文去做推理。缺了任何一条,Agent 很容易变成“猜”,那就不是排障,是算命了。
1.3 为了“透明”,我们给智能体定下的三条纪律
因为要做“全程透明”的排障,所以在让 Agent 介入之前,我先给整个任务定了几条硬性纪律。这些纪律不是口头约定,而是直接在编排框架里写成了约束条件。
第一条:任何结论必须关联到具体证据和时间戳,不允许出现没有出处的判断。比如 Agent A 说“连接池出现获取超时”,它必须同时给出日志样例、时间窗口、涉及节点范围,这样我才能立刻回溯核对。
第二条:任何变更类操作,在真正执行前必须先输出一份评估摘要,告诉我它准备执行什么命令、影响哪些节点、回滚方式是什么。这一步是为了防止智能体在复现实验过程中误操作线上环境。
第三条:所有交互记录和中间结论写入共享决策日志。A 和 B 各自的工作独立,但结论都落到同一份日志里,谁在什么时候说了什么、依据是什么,全程留痕。后面验证的时候也方便。定完这三条,我才让它们动手。
2. 任务拆解与设计:两个智能体怎么避免“抢工具”
2.1 Agent A:从日志里重建时间线的“排障矿工”
Agent A 的核心任务是处理日志。它不像人一样一页一页翻,而是通过工具接口批量拉取指定时间窗口的日志流,然后做摘要、聚类、特征提取。我给它的提示词里写得很细:不要一上来就做根因判断,先回答三个问题——什么现象在大量发生、从哪个节点开始、时间线如何展开。
它的实现逻辑大致是这样的:先按分钟级窗口切分日志,统计错误码分布,把高频错误聚成几个候选簇;然后对每个簇做一次抽样上下文分析,提取典型错误信息、信息来源节点和时间戳;最后把各个簇按时间轴排列,用于判断故障是从哪里扩散开来的。核心代码长这样:
def extract_log_features(log_batch): # 按错误码聚合,找出高频告警 error_groups = defaultdict(list) for line in log_batch: parsed = parse_log_line(line) # 解析时间、节点、错误码 error_groups[parsed.error_code].append(parsed) # 对每个高频错误做聚类统计 features = {} for code, items in error_groups.items(): if len(items) > 50: # 只保留高频项,减少无关噪音 features[code] = { "error_sample": items[0].raw[:200], "count": len(items), "nodes": sorted(set(i.node_id for i in items)), "time_range": [min(i.ts for i in items), max(i.ts for i in items)], } return features这段代码本质上是在做一个“降噪 + 找出最值得怀疑的错误簇”。因为日志量巨大,Agent A 不可能逐行理解所有内容,它首先要把海量文本变成结构化的特征表。得到特征表后,Agent A 再针对候选错误簇做深度分析,比如提取连接池超时错误出现过多少次、涉及的节点比例、时间分布是否与流量回切吻合。
实际操作中最让我满意的是它没被局部现象带偏。第一轮特征提取结束后,它同时看到两个高频错误:一个是“获取连接超时”,另一个是“缓存读取 miss 导致回源”。如果是我自己排,可能会先顺着缓存回源去查,因为那个错误码看着更紧张。但 Agent A 结合了时间分布后判断,“连接超时”才是更接近根因的一环,因为它比缓存回源更早出现。这个判断不是靠直觉,而是靠它把错误的时间序列对齐之后得出的先后关系。
2.2 Agent B:拿着放大镜做“对照实验”的验证者
Agent B 的定位和 Agent A 正好互补。A 的任务是“看”,B 的任务是“做”。当 A 给出一个可疑对象之后,B 不会直接说“对,就是它”,而是老老实实跑一遍验证:查询相关节点的运行参数,对比正常节点和异常节点的差异,再尝试用压测流量复现问题。
第一次协作时,B 拿到的怀疑对象是“连接池资源耗尽”。它做的第一件事是拉取一批正常节点和异常节点的 JVM 参数、连接池配置和内存指标。很快它就发现一个明显的差异:异常节点上连接池的 maxTotal 参数是 50,而正常节点是 200;异常节点空闲连接回收阈值是 60 秒,正常节点是 30 秒。这个发现很关键,但 B 没有停下来,因为参数差异只能说明“可能有问题”,不一定能解释“为什么会出问题”。
接着 B 开始做对照实验。它构造了一个简化版调用链,模拟故障节点在峰值流量下的连接申请和释放行为。复现结果在流量超过每秒 800 个并发请求时出现明显转折:旧的连接池参数下,连接耗尽错误率暴涨;新的参数下,同样流量表现平稳。以下是它验证用的核心逻辑,简化之后其实就是一个“配置差异”对比器:
def compare_configs(normal_nodes, abnormal_nodes): diff_keys = ["maxTotal", "maxIdle", "minIdle", "maxWaitMillis", "timeBetweenEvictionRunsMillis", "numTestsPerEvictionRun"] result = {} for key in diff_keys: normal_val = get_config(normal_nodes[0], key) abnormal_val = get_config(abnormal_nodes[0], key) if normal_val != abnormal_val: result[key] = {"normal": normal_val, "abnormal": abnormal_val} return resultB 的第二项工作是验证恢复方案。当人工确认要调整参数后,B 先去查了配置中心的最新版本,确认目标值,再在预发环境跑一轮压测,确保新的参数组合在峰值流量下不会触发其他问题。这一步看似保守,但它避免了“改完连接池又发现内存不够”的二次故障。
2.3 协作机制:共享上下文、分工但不分家
两个 Agent 不是孤立作业的,它们之间有一个共享上下文区域,可以互相读取对方的中间结论。后续排查过程中,A 会把新发现的时间线节点写入共享区,B 会去读取 A 的怀疑对象,再补充自己的验证结果。下面是两者职责和产出的对照表:
| Agent | 职责定位 | 输入 | 核心输出 | 关键约束 |
|---|---|---|---|---|
| Agent A | 根因追踪、日志分析 | 全量日志、监控指标、时间窗口 | 错误特征表、时间线、可疑对象列表 | 结论必须附带证据与时间戳 |
| Agent B | 验证者、实验执行 | Agent A 的怀疑对象、线上参数 | 配置差异表、复现实验结果、恢复验证 | 命令执行前必须输出评估摘要 |
除了任务边界,我还在编排框架里加了一道“置信度门槛”:Agent 每个结论要给自己打一个置信度值,低于 0.85 的结论默认不进入共享决策日志,只作为草稿保留。这个机制最开始是为了减少噪音,后来发现它还有一个额外好处——Agent 在给自己打分的时候会明显更谨慎,胡说八道的次数少了很多。
最终的人工决策环节保留在我这边。两个 Agent 可以给出建议,但涉及变更执行、流量切换、回滚操作,必须由人批准。理由很现实:Agent 能处理信息,但线上环境变更的责任还是要人扛起来。
3. 排障实录:从报警到恢复的每一步
3.1 第一阶段:Agent A 的第一次报告
故障发生后的第 6 分钟,Agent A 给出了第一份结构化报告。这不是一大段自然语言描述,而是一张特征表加一段时间线说明。它发现了三类值得注意的日志,按出现频率排序:
- 第一高频错误是“从连接池获取连接超时”,出现次数超过 7300 次,时间跨度从故障开始到报告时刻一直没有中断
- 第二高频错误是“等待连接池可用线程时被阻塞”,与前一错误在时间上高度重叠
- 第三类错误是“缓存读取 miss 触发回源”,数量虽然不少,但时间上比前两类晚了约 40 秒
看到这份报告的瞬间,我基本明白了它想表达什么:连接池是第一个被压垮的,缓存回源是后续连锁反应。但 A 没有直接下这个结论,它在报告里还画了一条简单的时间轴,上面标注了关键节点——配置变更下发时间、流量回切开始时间、错误率抬升时间、连接池打满时间。这四个时间点非常清晰地显示,错误率抬升和连接池打满,发生在流量回切之后的第 20 秒左右。
这个阶段给我的启发是,Agent 做“时间线重建”这件事,效率确实比人高。它读完了过去一小时三个节点共 12GB 日志,我让它在报告里只保留“有解释力”的事实,它确实做到了。没有把无关的慢查询、零星告警塞进来,输出很干净,这本身就节省了大量排查时间。
3.2 第二阶段:Agent B 的对照实验
拿到 Agent A 的报告后,我把怀疑对象转给了 Agent B:重点核查“连接池参数”和“连接池容量”是否存在异常。B 的第一轮输出是一份配置差异表,对比了 10 个正常节点和 10 个异常节点的关键参数。
差异非常明显:异常节点连接池 maxTotal 为 50,正常节点为 200;异常节点空闲连接回收频率为每 60 秒一次,正常节点为每 30 秒一次;异常节点的“最大等待时间”设置较短,在压力下更容易抛出超时异常。这些参数落到一起,结论已经呼之欲出——异常节点在流量高峰时连接池能提供的并发连接数只有正常节点的四分之一。
但 Agent B 没有把“参数差异”当成最终结论,它接着做了一件很重要的事:复现实验。它用压测工具模拟了峰值流量下的请求行为,连续打了几轮不同并发数的流量,结果在并发数达到 800 时,旧的连接池配置出现了大量“获取连接超时”,错误模式和线上故障日志几乎一模一样。新配置在同样流量下表现稳定,无错误。到这里,根因基本锁定了:连接池参数回退导致容量不足,流量回切后把连接池打爆。
3.3 第三阶段:快速恢复与验证
根因确认后的恢复动作是明确的:把异常节点的连接池参数调整为目标值,然后执行节点滚动重启,让新参数在全部节点生效。这里有一个细节需要注意——全量同时重启会影响流量,所以我选择了分批滚动,每次重启 20% 的节点,观察一分钟指标稳定后继续下一批。整个滚动过程大概花了四分钟,从监控上看,错误率在第一批节点重启后就出现了断崖式下降。
Agent B 在重启过程中同时做恢复验证。它持续拉取错误率和连接池监控,对比恢复前后的数据:错误率从 17% 降到 0.5% 以下;P99 延迟从 2300ms 回落到 190ms 左右;活跃连接数稳定在配额上限的 70%,不再打满。B 给出的恢复验证结论是“故障已解除”,同时附上了“连接池水位已恢复正常”的监控截图。下面是当时决策日志的关键片段,可以直观看到整个透明过程:
| 步骤 | 执行者 | 输入/操作 | 输出结论 | 置信度 | 人工裁决 |
|---|---|---|---|---|---|
| 01 | Agent A | 全量日志 | 连接池超时是首要特征 | 0.92 | 采用 |
| 02 | Agent B | 参数对比 | 异常节点 maxTotal 参数值偏低 | 0.95 | 采用 |
| 03 | Agent B | 压测复现 | 旧参数在 800 并发时触发超时 | 0.90 | 采用 |
| 04 | 人工 | 滚动重启节点 | 逐步生效,监控持续观察 | 人工确认 | 执行 |
| 05 | Agent B | 恢复验证 | 错误率、延迟恢复正常 | 0.98 | 关闭告警 |
3.4 故障的最终定义与后续补丁
故障解除之后,我们做了根因复盘。最终定义是:一次配置中心下发过程中,部分节点的连接池参数被意外回退到了旧版本;这些旧参数在没有重新校验的情况下被灰度批次节点加载;流量回切后,峰值请求超出旧连接池容量上限,导致连接池耗尽,进而引发大面积超时和回源风暴。
事后我们做的第一件事是在配置下发流程里加了一道校验脚本:任何连接池参数变更,必须与基准值比对,超出允许范围则直接拦截并告警。这个脚本是和 Agent A 一起梳理参数依赖后生成的,现在已经成为配置中心的常驻检查项。第二件事是把这次排障中的两份关键报告沉淀为模板,后续再出现同类故障,Agent 可以更快速地比对历史模式。
4. 排障中的真实经验:这些坑你也会遇到
4.1 智能体也会“一本正经地胡说八道”:怎么把幻觉压到最低
排障过程中我最担心的事情,其实是 Agent 给出一个看起来逻辑通顺、但实际并不存在的结论。智能体基于语言模型生成内容,天然存在“一本正经胡说八道”的可能性。这次我们用了几个办法来控制幻觉,实测下来有效。
第一是强制证据检索。Agent 在输出结论前,必须先调用日志查询或监控查询工具,拿到真实数据,不允许只凭内部知识回答。这一步直接把大量“想当然”的结论挡在了门外。第二是连续追问机制。当 A 提出一个观点,B 在验证时会要求 A 提供更细的时间窗数据,A 拿不出来或者数据对不上,这个观点就会被自动打回。第三是给 Agent 一个“我不知道”的选项,这一点很重要——在提示词里明确写了“当无法确认时,请直接说无法确认,不要猜测”,实际跑下来,它确实会比默认模式更谨慎,也更愿意承认信息不足。
4.2 三个让我印象深刻的坑
这个排障整体顺利,但中间也踩了几个坑,写出来给大家提个醒。
第一个坑是时区不一致导致的时间线错位。Agent A 在第一次重建时间线时,把日志平台的时间戳当成了统一的 UTC 时间,但部分节点输出的是本地时间。虽然只有八小时的偏差,却导致“故障扩散顺序”整体错乱。如果不是 Agent B 在做对照验证时发现了时间线矛盾,我们就会沿着一个错误的方向查下去。现在我的处理办法是,在让 Agent 分析日志前,先做一次时间标准化,把不同来源的时间戳统一转换成同一时区再交给它。
第二个坑是“隧道效应”——Agent 很容易顺着第一个看似合理的假设一路跑偏。第一轮排查时 A 曾把“缓存回源”列为高度可疑项,如果我们不加上 B 的交叉验证,按照那个方向走会浪费很多时间。解决方式是在协作机制里强制要求:每个阶段必须有一个“质疑角色”,即 B 的工作不仅是验证 A 的发现,还要主动尝试推翻它。
第三个坑是置信度偏差。Agent 给自己打分普遍偏高,0.8 甚至 0.9 的置信度经常出现,但结合证据链看,很多结论撑不起这个分数。后来我们调整了规则,要求 Agent 在给出高置信度时必须额外列出至少两条支持证据,否则把置信度上限压到 0.8 以下。这个调整看起来简单,但对结论质量的改善非常明显。
4.3 可以直接抄作业的工具和脚本
排除整个过程之后,我发现有一些检查项和脚本可以沉淀成模板,下次即便是人工排障也可以用。
首先是“连接池参数检查清单”,不管用什么技术栈,这几个参数都是重点:maxTotal、maxIdle、minIdle、maxWaitMillis、空闲连接回收周期、连接泄漏超时时间。对搜索引擎这类高并发服务,maxTotal 的设置要考虑“单节点并发峰值乘以节点数,再留出 20% 的余量”。
其次是配置变更后的自动校验脚本。现在我们的配置中心会把每次变更的参数列表推送到一个校验服务,比对基准值并输出差异报告。这个脚本的核心逻辑是从配置中心拉新值、拉基准值、算差异、按规则拦截或放行,整个过程可以在 Agent 工具链里直接调用。
最后是恢复验证脚本。故障处理完后,Agent B 跑的那套监控对比逻辑也可以固化成函数:输入故障时间段和目标时间段,自动拉取错误率、延迟、连接数三项指标,计算恢复程度并生成报告。有了这个脚本,以后恢复验证就不用再手动写命令了,Agent 直接调用就行。
5. 这套排障模式的边界在哪里,能复用多少
5.1 什么故障适合让智能体先接手
经历了这次排障,我认为“双 Agent 排障”模式不是万能药,它有自己的适用边界。适合的场景有几个共同特征:日志和监控数据充足;故障可以通过对比异常样本和正常样本来缩小范围;问题大概率出在配置、参数、依赖关系这类“可描述、可比较”的环节。
反过来,如果故障涉及基础设施层面的物理问题,比如机房断电、硬件损坏,或者需要现场操作才能恢复,Agent 能做的事情就很有限了。它顶多帮你把故障范围框出来,但没法拧螺丝。所以我的经验是:把 Agent 当作“强力的信息处理工具”,而不是“全能的运维替代者”,它帮我省掉的是大量筛选和对比的时间,但它给出的结论一定是“参考”,最终决策仍然需要人来做。
5.2 后续还可以怎么扩展
这次排障留下了不少可以复用的资产。最直接的做法,是把完整的排障过程沉淀成运维知识库,让下一次同类故障出现时,Agent 可以直接检索历史处理方案,把定位时间从二十分钟压缩到五分钟以内。另外,每隔一段时间跑一次故障预演,用历史故障数据回放,检验两个 Agent 在新的编排配置下能不能复现同样的结论,这其实也是对 Agent 能力的一种回归测试。
我还在想的一个扩展方向,是把这套“双 Agent 协作”机制搬去容量规划和性能测试。日常做压测时,经常需要对比不同参数下的表现,这和排障中的“对照实验”本质上是同一类工作,如果把 Agent B 的验证能力拓展到压测场景,就能在平时更早发现类似连接池容量不足这种隐患,而不必等到线上报警。
说实话,这次排障给我最大的触动,不是“AI 救活了搜索引擎”这种戏剧性,而是两个智能体通过互相校对、证据留痕,真正做到了“结论可以被追溯、被质疑、被验证”。这种透明性比单兵作战的聪明更让人放心。如果你也想在日常排障中引入智能体,我的建议是先从小故障开始,给 Agent 划定清晰的任务边界和置信度门槛,保留人工裁决权,跑顺几轮之后再把它的权限一点点放大。