1. 为什么我要给 CI 流水线配一个会自己看日志的 AgentCI 挂了点开 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 [ rError:, rerror:, rFAILED, rTraceback \(most recent call last\), rException, rexit code \d, rnpm ERR!, rModuleNotFoundError, ] def extract_error_windows(log_lines, window5): 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_causestring一句话根因不超过 100 字categoryenum依赖问题 / 代码逻辑 / 环境配置 / 测试用例 / 超时 / 其他confidencefloat0 到 1模型对根因判断的置信度fix_suggestionstring具体修复建议要可操作fix_patchstring or null如果能生成 patch放这里否则 nullneeds_humanboolean是否必须人工介入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 里新增了某个 importAgent 可以尝试在 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把它的注意力集中在真正重要的失败上。如果你打算动手做我的建议是从最小闭环开始先只做拉日志 分析 评论不要碰自动修复。把这个闭环跑稳积累几十次真实分析结果你自然就知道该在哪里加自动化、在哪里设阈值。自动修复是最后一步不是第一步。