1. 为什么我要给 CI 流水线配一个会自己看日志的 Agent
CI 挂了,点开 GitHub Actions 的日志页面,几千行输出里翻找那一行Error: Process completed with exit code 1,然后往前倒推三十行,发现是一个依赖版本冲突。这个过程我重复了大概两年,直到某天凌晨两点又被一条构建失败的通知叫醒,我盯着屏幕想:这件事的决策路径其实非常固定——拉日志、定位报错、匹配已知模式、给出修复建议。既然固定,那就应该交给一个 Agent 去做。
这篇文章讲的就是这件事:从零搭一个 DevOps AI Agent,它能监听 CI 失败事件、自动拉取失败日志、调用 LLM 做根因分析、把结论和修复建议回写到 PR 评论里,甚至在置信度足够高的时候直接提交一个修复 commit。整套东西不依赖任何商业 SaaS 的付费 Agent 功能,核心组件就是 GitHub Actions 的 webhook、一个轻量服务端、以及一个 LLM 调用层。
适合谁看?如果你已经在用 GitHub Actions 或者类似的 CI 系统,手上有一些重复性的构建失败排查工作,并且对 LLM API 调用有基本概念,那这篇可以直接抄作业。如果你还没接触过 CI,建议先把流水线跑通再回来看,因为这里不会解释workflow.yml的基础语法。
关键词先摆出来,方便你判断是否对味:DevOps、AI Agent、CI、GitHub Actions、LLM。整篇文章围绕这五个词展开,但重点在"Agent"这个词上——不是简单的"失败就发通知",而是有感知、有推理、有行动闭环的那种。
先说清楚一个容易混淆的点。很多人把"CI 失败自动发 Slack 通知"叫做 AI Agent,那不是。Agent 的核心特征是自主决策链:它要自己判断该拉哪些日志、自己决定分析策略、自己评估修复方案的风险等级,最后自己决定是"只评论"还是"直接改代码"。这条链路上每一步都有分支,而不是一条 if-else 走到底。我后面会用具体的代码和 prompt 设计来说明这个决策链怎么落地。
还有一个现实问题得提前说:LLM 分析 CI 日志的准确率,取决于你喂给它的上下文质量。直接把 5000 行原始日志丢进去,效果会很差,因为大部分行是噪音。所以这个 Agent 的一半工作量在"日志预处理"上,这部分我会单独用一章来讲,它比 LLM 调用本身更值得花时间。
2. Agent 的触发链路:从 CI 失败到任务入队
2.1 为什么不用 GitHub Actions 直接调 LLM
最直觉的方案是在 workflow 里加一个on: failure的 job,直接在里面 curl 一下 LLM API。我第一版就是这么做的,跑了大概两周就放弃了,原因有三个。
第一,超时问题。GitHub Actions 的 job 有默认超时限制,而 LLM 分析一份大日志动辄要 30 秒到 2 分钟,如果遇到 API 限流重试,很容易把 job 拖到超时,反而让原本的失败信息被淹没。第二,密钥暴露面。把 LLM API Key 放在 workflow 的 secrets 里,意味着任何能改 workflow 文件的人都能读到它,虽然 GitHub 有环境保护规则,但多一层隔离总是好的。第三,状态管理。Agent 需要记录"这条失败我已经分析过了",避免同一个 commit 反复触发分析,这个状态放在 Actions 里很难维护,放在独立服务里就是一个数据库字段的事。
所以最终架构是:GitHub Actions 只负责发事件,分析逻辑全部在一个独立服务里。这个服务可以跑在任何能接收 HTTP 请求的地方,我用的是一个最便宜的云主机加 Docker。
2.2 workflow 里到底要写什么
触发端的配置非常薄,核心就是监听workflow_run事件的completed状态,并且判断结论是 failure。这里有个细节:workflow_run事件本身不携带失败 job 的日志,只给你 run_id,所以触发端的工作就是把这个 id 和仓库信息打包发出去。
name: CI Failure Notifier on: workflow_run: workflows: ["Build and Test"] types: [completed] jobs: notify: if: ${{ github.event.workflow_run.conclusion == 'failure' }} runs-on: ubuntu-latest steps: - name: Send failure event to agent run: | curl -X POST https://your-agent-host/ingest \ -H "Content-Type: application/json" \ -H "X-Signature: ${{ secrets.AGENT_WEBHOOK_SECRET }}" \ -d '{ "run_id": ${{ github.event.workflow_run.id }}, "repo": "${{ github.repository }}", "head_sha": "${{ github.event.workflow_run.head_sha }}", "branch": "${{ github.event.workflow_run.head_branch }}", "pr_number": ${{ github.event.workflow_run.pull_requests[0].number || 0 }} }'注意X-Signature这个头,服务端必须校验它,否则任何人都能往你的 Agent 里灌假事件,触发无意义的 LLM 调用烧钱。校验方式用 HMAC-SHA256 对 body 做签名,和 GitHub 自己的 webhook 校验逻辑一样,这里不展开。
还有一个坑:workflow_run事件里的pull_requests数组在 fork 仓库的 PR 场景下经常是空的。如果你的团队有外部贡献者,得准备一个 fallback——用 head_sha 去调 GitHub API 反查关联的 PR。我踩过这个坑,当时 Agent 对 fork PR 的失败完全不响应,排查了半天才发现是数组为空。
2.3 服务端的入队与去重
服务端收到事件后第一件事不是分析,是去重。同一个 run_id 可能因为 GitHub 的事件重试机制被投递多次,如果不做幂等,你会对同一份日志调用三次 LLM。我的做法是用 run_id 作为唯一键写一张analysis_tasks表,插入冲突就直接返回 200,什么都不做。
去重之后是入队。这里我建议用一个简单的数据库队列而不是 Redis 或者 RabbitMQ,因为任务量真的不大——一个中等规模团队一天也就几十次失败。用一张表加一个status字段(pending / processing / done / failed)就够了,worker 定时轮询。引入消息队列的复杂度在这个场景下是过度设计,我试过用 Redis,后来发现维护成本比收益高,又退回了数据库队列。
任务入队时要把关键上下文一起存下来:repo、head_sha、branch、pr_number、run_id。这些字段后面拉日志、回写评论都要用。特别是 head_sha,它是你后续定位代码、生成修复 patch 的锚点。
3. 日志预处理:决定 Agent 智商上限的关键一步
3.1 原始日志为什么不能直接喂给 LLM
我做过一个对比实验:同一份失败日志,一份原样 4800 行丢给模型,一份经过预处理压缩到 200 行,问同样的问题"根因是什么"。原始版本的回答基本是废话,比如"构建过程中出现了错误,建议检查配置",而压缩版本能准确指出"pytest在test_user_auth.py::test_token_refresh处失败,原因是 mock 的time.time返回值未更新导致 token 被判定过期"。
差距的来源是信噪比。CI 日志里 90% 以上是正常步骤的输出,真正有价值的信号集中在几个位置:失败步骤的名称、错误堆栈、退出码、以及失败前最后几行输出。LLM 的注意力是有限的,你给它越多噪音,它越容易抓错重点。
3.2 我的日志压缩策略
预处理分四步,每一步都有明确的取舍理由。
第一步,按步骤切分。GitHub Actions 的日志有明确的##[group]和##[endgroup]标记,可以据此把日志切成一个个 step。然后只保留conclusion为 failure 的 step,其余全部丢弃。这一步通常能把日志量砍掉 70%。
第二步,提取错误锚点。在每个失败 step 内,用一组正则去匹配错误特征行:Error:、error:、FAILED、Traceback、exit code、Exception、npm ERR!、ModuleNotFoundError等等。匹配到的行连同它前后各 5 行一起保留,形成一个"错误窗口"。为什么要带上下文?因为单独一行ModuleNotFoundError: No module named 'foo'信息量不够,你需要看到它是在哪个文件哪一行 import 的。
第三步,堆栈折叠。Python 的 traceback 和 Java 的 stack trace 经常几十行,其中大部分是框架内部的调用。我的做法是保留最顶层的应用代码帧和最底层的错误帧,中间的框架帧折叠成一行... 12 more framework frames ...。这个折叠规则需要针对不同语言定制,Python 和 Node 的格式差别很大。
第四步,截断保护。即使经过前三步,某些极端情况(比如一个测试套件里 200 个用例全挂)日志还是会长。这时候按"错误类型分组",每种错误只保留前 3 个实例,其余用计数代替。LLM 不需要看 200 遍同样的错误,它需要知道"这个错误出现了 200 次"。
import re ERROR_PATTERNS = [ r"Error:", r"error:", r"FAILED", r"Traceback \(most recent call last\)", r"Exception", r"exit code \d+", r"npm ERR!", r"ModuleNotFoundError", ] def extract_error_windows(log_lines, window=5): hits = [] for i, line in enumerate(log_lines): if any(re.search(p, line) for p in ERROR_PATTERNS): start = max(0, i - window) end = min(len(log_lines), i + window + 1) hits.append((start, end)) # 合并重叠窗口 merged = [] for start, end in sorted(hits): if merged and start <= merged[-1][1]: merged[-1] = (merged[-1][0], max(merged[-1][1], end)) else: merged.append((start, end)) result = [] for start, end in merged: result.extend(log_lines[start:end]) result.append("---") return result这段代码是整个预处理的核心,实测下来能把 5000 行日志压到 150 到 300 行,同时保留所有关键信号。窗口大小 5 是我调了几次的结果,3 太小会丢上下文,10 太大又引入噪音。
3.3 别忘了把 diff 一起喂进去
光有日志还不够。很多 CI 失败是代码改动引起的,如果你不把这次 commit 的 diff 一起给模型,它只能猜。我的做法是拉取head_sha相对 base 分支的 diff,同样做截断——只保留改动行数最多的前 5 个文件,每个文件最多 100 行 diff。
这里有个经验:diff 和日志要分开标注。在 prompt 里用清晰的分隔符,比如=== CI LOG ===和=== CODE DIFF ===,让模型知道哪部分是运行时输出,哪部分是代码变更。混在一起会让模型产生奇怪的联想。
4. 让 LLM 输出可执行结论的 Prompt 工程
4.1 为什么结构化输出比自然语言描述重要
第一版 Agent 我让模型自由发挥,输出一段分析文字,然后我人工读。用了几天发现不行——Agent 的价值在于闭环,如果结论不能直接被程序消费,那它和"发个通知让你自己看"没区别。所以我要求模型输出严格的 JSON,包含固定的字段。
字段设计如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| root_cause | string | 一句话根因,不超过 100 字 |
| category | enum | 依赖问题 / 代码逻辑 / 环境配置 / 测试用例 / 超时 / 其他 |
| confidence | float | 0 到 1,模型对根因判断的置信度 |
| fix_suggestion | string | 具体修复建议,要可操作 |
| fix_patch | string or null | 如果能生成 patch,放这里,否则 null |
| needs_human | boolean | 是否必须人工介入 |
category这个字段是我后来加的,因为不同类别的失败处理策略完全不同。依赖问题可以自动尝试升级版本,代码逻辑问题只能评论提醒,环境配置问题往往需要改 workflow 文件。有了分类,Agent 才能做差异化决策。
confidence是控制风险的关键。我设了两道阈值:低于 0.6 只记录不评论,0.6 到 0.85 之间评论但不改代码,高于 0.85 才允许自动提交修复。这个阈值不是拍脑袋定的,是跑了两百多次真实失败后统计出来的——0.85 以上的自动修复,我人工复核的准确率大概在 90% 左右,可以接受。
4.2 Prompt 的实际写法
系统提示词我改了七八版,最终稳定下来的版本大概是这个结构:
你是一个资深的 CI 故障分析专家。你会收到一份 CI 失败日志和相关的代码变更。 你的任务是定位根因并给出可执行的修复建议。 分析时请遵循以下原则: 1. 优先关注日志中明确标记为错误的行,不要被正常输出干扰 2. 如果错误涉及依赖,检查 diff 中是否有依赖文件的改动 3. 如果错误是测试失败,判断是测试本身的问题还是被测代码的问题 4. 不要臆测日志中没有体现的原因 5. 如果信息不足以确定根因,把 confidence 设为 0.5 以下 输出必须是合法的 JSON,不要包含任何 JSON 之外的解释文字。注意第 4 条和第 5 条。这两条是专门用来压制模型"编造根因"的倾向的。不加这两条的时候,模型遇到模糊日志会强行给一个听起来合理但完全错误的结论,而且 confidence 还标得很高。加上之后,它在信息不足时会老实说"不确定",这对下游决策至关重要。
用户提示词就是把预处理后的日志和 diff 拼进去,加上仓库名、分支名这些元信息。我试过把元信息放系统提示词,效果不如放用户提示词,因为系统提示词更适合放稳定的规则,用户提示词放每次变化的数据。
4.3 处理模型返回非法 JSON 的情况
即使你千叮咛万嘱咐,模型还是有概率返回带 markdown 代码块包裹的 JSON,或者干脆多写一段解释。我的处理是三层防御:第一层,prompt 里明确要求纯 JSON;第二层,解析前先用正则剥掉json 和包裹;第三层,解析失败就重试一次,重试时在 prompt 里加上"上次输出格式错误,请只输出 JSON"。
重试还是失败怎么办?直接标记任务为 failed,记录原始输出,不阻塞队列。这种情况我遇到的比例大概在 2% 左右,主要是模型服务端偶发的格式抖动,不值得为它设计更复杂的容错。
5. 从分析到行动:Agent 的决策分支与回写
5.1 评论回写的格式设计
分析完成后,Agent 要把结论回写到 PR。这里有个细节很多人忽略:评论要能被人快速扫读。我见过一些 Agent 把整个 JSON 原样贴出来,读起来非常痛苦。我的格式是这样的:
## CI 故障分析 **根因**:test_token_refresh 失败,mock 的 time.time 未更新导致 token 被判定过期 **分类**:测试用例 **置信度**:0.91 **修复建议**: 在 test_token_refresh 的 setup 中,将 mock_time.return_value 更新为当前时间戳, 或改用 freezegun 库统一管理时间 mock。 **自动修复**:已提交 commit abc1234用 markdown 的加粗和分段,让人三秒钟能抓到重点。如果needs_human为 true,我会在最前面加一行醒目的提示,并且不显示"自动修复"那一节。
回写用 GitHub API 的 issue comments 接口,注意 PR 的评论要用 issues 端点而不是 pulls 端点,这是 GitHub API 的一个历史遗留设计,很多人第一次会踩。
5.2 自动修复的边界在哪里
自动提交修复是这个 Agent 最诱人也最危险的部分。我的原则是:只自动修复那些"修复方式唯一且低风险"的问题。具体来说,目前只开放三类:
第一类,依赖版本问题。如果日志显示ModuleNotFoundError且 diff 里新增了某个 import,Agent 可以尝试在 requirements 或 package.json 里加上对应依赖。但版本号它不能自己定,必须从项目已有的依赖里推断,或者用一个保守的版本范围。
第二类,明显的拼写和路径错误。比如 import 路径写错、文件名大小写不一致,这类问题修复方式唯一,风险极低。
第三类,格式化和 lint 问题。如果 CI 失败是因为 prettier 或 black 检查不通过,直接跑一遍格式化工具提交即可,这类甚至不需要 LLM 判断。
除此之外,一律只评论不修改。我特别警惕模型生成的"逻辑修复"——它可能改对了症状但改错了病因,而这种错误在 code review 时很难被发现,因为看起来"CI 过了"。
5.3 提交修复的技术实现
自动提交用 GitHub API 的 contents 接口或者 git 命令都行。我用的是后者,因为要处理多文件修改,git 更灵活。流程是:clone 仓库到临时目录、checkout 到失败的分支、应用 patch、commit、push。
push 的权限用一个专门的 bot 账号的 token,权限范围限制在目标仓库的 contents 写权限,不要用个人 token。commit message 里要明确标注是自动生成的,方便后续追溯:
fix: auto-fix CI failure in test_token_refresh Generated by DevOps AI Agent based on run #12345 Confidence: 0.91push 之后 Agent 还要做一件事:重新触发 CI。因为你的修复可能没修对,需要让流水线再跑一遍验证。如果第二次还是失败,Agent 要能识别出"这是我修过的 commit 又挂了",然后停止自动修复,转为人工介入。这个防循环机制必须有,否则模型可能陷入"改一次挂一次"的死循环,把分支搞得一团糟。
6. 实测中暴露的问题和我的应对
6.1 模型对"间歇性失败"的误判
有一类 CI 失败是 flaky test,同样的代码这次挂下次过。模型看到失败日志会认真分析出一个根因,然后给出修复建议,但实际上根本没有根因,重跑就好了。这个问题困扰了我一段时间。
我的应对是引入历史失败率作为上下文。在调用 LLM 之前,先查一下这个测试用例在过去 30 天内的失败次数和总运行次数,如果失败率低于 5% 且失败分布随机,就在 prompt 里加一句提示:"该测试历史失败率为 3%,可能是间歇性问题,请优先考虑这个可能性。"加上这个信号后,模型对 flaky test 的识别率明显提升,会输出category: 测试用例且needs_human: true,建议重跑而不是改代码。
6.2 大仓库的 diff 拉取超时
在 monorepo 场景下,一次 commit 的 diff 可能涉及几百个文件,拉取和处理都很慢。我的优化是只拉取与失败 step 相关的目录。怎么判断相关?看失败日志里出现的文件路径,提取出涉及的顶层目录,只拉这些目录下的 diff。这个启发式规则不完美,但覆盖了大部分情况,把 diff 拉取时间从平均 20 秒降到了 3 秒以内。
6.3 成本控制
LLM 调用是要花钱的,一个活跃仓库一天几十次失败,如果每次都调最贵的模型,月底账单会很难看。我的策略是分级调用:先用一个便宜的小模型做初步分类,如果分类结果是"依赖问题"或"格式问题"这类简单类别,直接用规则处理,不调大模型;只有分类为"代码逻辑"或"环境配置"这类需要深度推理的,才升级到大模型。
实测下来这个策略能省掉大概 60% 的大模型调用。小模型的分类准确率在 85% 左右,剩下的 15% 误分类会走大模型兜底,不会漏掉复杂问题。
6.4 一个我至今没完全解决的问题
模型对"多因一果"的失败分析得不够好。比如一次 CI 失败同时有依赖缺失和测试超时两个问题,模型往往只抓住其中一个,给出一个片面的根因。我试过在 prompt 里要求它列出所有可能的原因,但效果一般,它倾向于给一个"最可能"的答案而不是穷举。
目前的妥协是:如果日志里检测到多个不同类型的错误锚点,就在评论里提示"检测到多个潜在问题,建议人工复核",并且把 confidence 压低。这不算完美解决,但至少不会误导人。如果你有更好的思路,欢迎交流。
7. 部署与运维的几个实际考量
7.1 服务端的最小化部署
整个 Agent 服务端我打包成一个 Docker 镜像,依赖只有 Python 标准库加 requests 和 psycopg2,镜像大小控制在 150MB 以内。跑在一台 1 核 2G 的云主机上绰绰有余,因为大部分时间它在等 LLM 返回,CPU 占用极低。
数据库用 PostgreSQL,就两张表:analysis_tasks和analysis_results。前者管队列和去重,后者存分析结论供后续统计。没有用 ORM,直接写 SQL,因为表结构简单到不值得引入额外依赖。
7.2 监控什么
Agent 本身也会挂,所以要有监控。我关注三个指标:任务队列积压数、LLM 调用失败率、自动修复的后续失败率。前两个是健康度指标,第三个是质量指标。特别是第三个,如果自动修复后 CI 再次失败的比例超过 20%,说明阈值设得太松,需要收紧。
监控告警我用的是一封每日汇总邮件,而不是实时告警。因为 Agent 挂了不会影响主流程,CI 该失败还是失败,只是没人自动分析而已,不值得半夜叫人起来。
7.3 灰度上线的经验
不要一上来就对所有仓库开启自动修复。我的做法是分三阶段:第一阶段只分析不评论,我自己看结果,校准 prompt;第二阶段评论但不改代码,让团队适应;第三阶段才对少数几个仓库开启自动修复,而且只开放依赖问题这一类。
每个阶段跑至少一周,观察准确率和团队反馈。直接跳到第三阶段的话,一旦模型给出错误修复并自动提交,会消耗团队对 Agent 的信任,而信任一旦失去很难重建。
8. 一些关于 Agent 边界的个人体会
搭完这套东西跑了几个月,我最大的体会是:Agent 的价值不在于替代人,而在于把人从"信息搬运"中解放出来。以前我排查 CI 失败,80% 的时间花在翻日志、找报错、回忆这个错误以前见过没有,只有 20% 的时间真正在思考怎么修。现在 Agent 把前 80% 做完了,我打开 PR 直接看到根因和分类,决策速度快了很多。
但我也清楚地知道它的边界。它擅长的是模式匹配和已知问题的快速定位,不擅长的是判断"这个失败是否重要"。有些 CI 失败其实无关紧要,比如一个 nightly 的兼容性测试挂了,但 Agent 会一视同仁地分析并评论,有时候反而制造噪音。我后来加了一个配置,允许对特定 workflow 或特定分支关闭 Agent,把它的注意力集中在真正重要的失败上。
如果你打算动手做,我的建议是从最小闭环开始:先只做"拉日志 + 分析 + 评论",不要碰自动修复。把这个闭环跑稳,积累几十次真实分析结果,你自然就知道该在哪里加自动化、在哪里设阈值。自动修复是最后一步,不是第一步。